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:
authored and GitHub committed 2026-10-05 14:58:07 +02:00
1 parent 5a0e7348af
commit b9e0ac4f1d
2 files changed
+73 -12

No files matched your search

+21 -12
View File
@@ -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
+52
View File
@@ -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"