Prevent clipboard capture hangs (#9488)
* Bound clipboard capture reads * Bound the watched clipboard read and drop copies cut off at the deadline In watch mode capture.sh reads the copy from the owner's pipe on stdin, to EOF, with no bound. An owner that stalls without closing its end keeps the callback alive, and wl-paste --watch handles no further clipboard events until it exits. That matches #9443, whose stuck capture.sh had no wl-paste child: --list-types never reads from the owner. Every read now goes through one bounded reader into a temporary file, and the copy is recorded only when the read finished, so a snapshot that stalls halfway no longer records its first half. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+73
-12
No files matched your search
@@ -10,25 +10,31 @@ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
|
||||
IMAGE_DIR="$STATE_DIR/clipboard-images"
|
||||
mkdir -p "$IMAGE_DIR"
|
||||
|
||||
types=$(wl-paste --list-types 2>/dev/null || true)
|
||||
types=$(timeout 2s wl-paste --list-types 2>/dev/null || true)
|
||||
|
||||
if [[ ${CLIPBOARD_STATE:-} == "sensitive" ]] || grep -qx 'x-kde-passwordManagerHint' <<<"$types"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=
|
||||
trap 'rm -f -- "$tmp"' EXIT
|
||||
|
||||
# The clipboard owner streams the copy and can stall without closing its end, so
|
||||
# every read is bounded, and a copy cut off at the deadline is dropped, not kept.
|
||||
read_copy() {
|
||||
timeout 2s "$@" >"$tmp" 2>/dev/null && [[ -s $tmp ]]
|
||||
}
|
||||
|
||||
emit_image() {
|
||||
local mime="$1"
|
||||
local ext tmp hash file
|
||||
local ext hash file
|
||||
shift
|
||||
|
||||
ext=${mime#image/}
|
||||
[[ $ext == jpeg ]] && ext=jpg
|
||||
|
||||
tmp=$(mktemp --tmpdir="$IMAGE_DIR" clipboard.XXXXXX) || return 0
|
||||
cat >"$tmp"
|
||||
if [[ ! -s $tmp ]]; then
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
read_copy "$@" || return
|
||||
|
||||
hash=$(sha256sum "$tmp" | awk '{print $1}')
|
||||
file="$IMAGE_DIR/$hash.$ext"
|
||||
@@ -43,6 +49,9 @@ emit_image() {
|
||||
}
|
||||
|
||||
emit_text() {
|
||||
tmp=$(mktemp --tmpdir="$STATE_DIR" clipboard.XXXXXX) || return 0
|
||||
read_copy "$@" || return
|
||||
|
||||
perl -MEncode=decode,FB_CROAK,LEAVE_SRC -MJSON::PP=encode_json -0777 -e '
|
||||
my $raw = <STDIN>;
|
||||
exit unless length $raw;
|
||||
@@ -86,21 +95,21 @@ emit_text() {
|
||||
}
|
||||
$text = decode("UTF-8", $raw) unless defined $text;
|
||||
print "{\"type\":\"text\",\"text\":", encode_json($text), "}\n";
|
||||
'
|
||||
' <"$tmp"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
text) emit_text; exit 0 ;;
|
||||
image/*) emit_image "$1"; exit 0 ;;
|
||||
text) emit_text cat; exit 0 ;;
|
||||
image/*) emit_image "$1" cat; exit 0 ;;
|
||||
esac
|
||||
|
||||
for mime in image/png image/jpeg image/webp image/gif image/bmp image/tiff; do
|
||||
if grep -qx "$mime" <<<"$types"; then
|
||||
timeout 2s wl-paste --type "$mime" 2>/dev/null | emit_image "$mime"
|
||||
emit_image "$mime" wl-paste --type "$mime"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -q '^text/' <<<"$types" || grep -qx 'UTF8_STRING' <<<"$types" || grep -qx 'STRING' <<<"$types"; then
|
||||
wl-paste --type text --no-newline 2>/dev/null | emit_text
|
||||
emit_text wl-paste --type text --no-newline
|
||||
fi
|
||||
@@ -255,8 +255,14 @@ SH
|
||||
cat >"$TMPDIR/bin/wl-paste" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $1 == "--list-types" ]]; then
|
||||
[[ ${WL_PASTE_STALL:-} == "list-types" ]] && sleep 10
|
||||
printf '%b' "${WL_PASTE_TYPES:-text/plain\n}"
|
||||
elif [[ $1 == "--type" && $2 == "text" ]]; then
|
||||
[[ ${WL_PASTE_STALL:-} == "text" ]] && sleep 10
|
||||
if [[ ${WL_PASTE_STALL:-} == "text-midway" ]]; then
|
||||
printf 'first half '
|
||||
sleep 10
|
||||
fi
|
||||
printf '%s' "${WL_PASTE_TEXT:-terminal copy}"
|
||||
fi
|
||||
SH
|
||||
@@ -292,6 +298,52 @@ capture_output=$(printf 'closing app copy' | WL_PASTE_TEXT="stale read" XDG_RUNT
|
||||
[[ $capture_output == '{"type":"text","text":"closing app copy"}' ]] || fail "clipboard capture records watched text from stdin"
|
||||
pass "clipboard capture records watched text from stdin"
|
||||
|
||||
if capture_output=$(printf 'watched copy after stalled types' | WL_PASTE_STALL="list-types" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh" text); then
|
||||
capture_status=0
|
||||
else
|
||||
capture_status=$?
|
||||
fi
|
||||
[[ $capture_status -eq 0 ]] || fail "clipboard watched text survives a stalled type query" "capture exited with status $capture_status"
|
||||
[[ $capture_output == '{"type":"text","text":"watched copy after stalled types"}' ]] || fail "clipboard watched text remains intact after a stalled type query"
|
||||
pass "clipboard watched text survives a stalled type query"
|
||||
pass "clipboard watched text remains intact after a stalled type query"
|
||||
|
||||
if capture_output=$(WL_PASTE_STALL="text" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh"); then
|
||||
capture_status=0
|
||||
else
|
||||
capture_status=$?
|
||||
fi
|
||||
[[ $capture_status -eq 124 ]] || fail "clipboard snapshot text read uses its internal timeout" "capture exited with status $capture_status"
|
||||
[[ -z $capture_output ]] || fail "clipboard snapshot timeout emits no partial entry" "actual: $capture_output"
|
||||
pass "clipboard snapshot text read uses its internal timeout"
|
||||
pass "clipboard snapshot timeout emits no partial entry"
|
||||
|
||||
if capture_output=$(WL_PASTE_STALL="text-midway" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh"); then
|
||||
capture_status=0
|
||||
else
|
||||
capture_status=$?
|
||||
fi
|
||||
[[ $capture_status -eq 124 ]] || fail "clipboard snapshot text read stalled midway uses its internal timeout" "capture exited with status $capture_status"
|
||||
[[ -z $capture_output ]] || fail "clipboard snapshot stalled midway emits no partial entry" "actual: $capture_output"
|
||||
pass "clipboard snapshot text read stalled midway uses its internal timeout"
|
||||
pass "clipboard snapshot stalled midway emits no partial entry"
|
||||
|
||||
for watched_mime in text image/png; do
|
||||
if capture_output=$({ printf 'copy whose owner never closes'; sleep 5; } | XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh" "$watched_mime"); then
|
||||
capture_status=0
|
||||
else
|
||||
capture_status=$?
|
||||
fi
|
||||
[[ $capture_status -eq 0 ]] || fail "clipboard watched $watched_mime read is bounded when the owner stalls" "capture exited with status $capture_status"
|
||||
[[ -z $capture_output ]] || fail "clipboard watched $watched_mime cut off by a stalled owner emits no entry" "actual: $capture_output"
|
||||
pass "clipboard watched $watched_mime read is bounded when the owner stalls"
|
||||
pass "clipboard watched $watched_mime cut off by a stalled owner emits no entry"
|
||||
done
|
||||
|
||||
leftover=$(find "$TMPDIR/state/omarchy" -name 'clipboard.*' -print)
|
||||
[[ -z $leftover ]] || fail "clipboard capture removes copies it dropped" "left: $leftover"
|
||||
pass "clipboard capture removes copies it dropped"
|
||||
|
||||
capture_output=$(printf '%s' 'UTF-16 clipboard - fixed' | iconv -f UTF-8 -t UTF-16LE | XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" "$ROOT/shell/plugins/clipboard/capture.sh" text)
|
||||
[[ $capture_output == '{"type":"text","text":"UTF-16 clipboard - fixed"}' ]] || fail "clipboard capture decodes UTF-16LE text"
|
||||
pass "clipboard capture decodes UTF-16LE text"
|
||||
|
||||
Reference in new issue
Block a user