diff --git a/migrations/1786643346.sh b/migrations/1786643346.sh index 48cdb40b..cc197a91 100644 --- a/migrations/1786643346.sh +++ b/migrations/1786643346.sh @@ -122,10 +122,36 @@ unverified_repairs_exist() { return 1 } -# Only this user's browsers matter — another user's cannot rewrite these -# Preferences. -browsers_running() { - pgrep -x -u "$UID" 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1 +# A browser only rewrites its own Preferences on exit, so a repair can only be +# reverted by a browser attached to a profile this migration has to touch. +# Whether a profile is open is mechanical: a running Chromium-family browser +# holds a SingletonLock (and socket) inside its user-data-dir. +profile_open() { + # -L catches a SingletonLock left as a dangling symlink; -e covers a plain + # file, and -S the socket — any of them means a browser is attached. + [[ -L $1/SingletonLock || -e $1/SingletonLock || -S $1/SingletonSocket ]] +} + +# Gate on a pending — or to-be-verified — profile actually being open, not on +# browsers in general. Waiting on every browser process deadlocks the update on +# machines whose main browser is effectively never closed: the pending ghosts +# commonly sit in a stale profile nobody has open, yet the update blocks on the +# always-open one. +affected_profile_open() { + local preferences backup profile_root + + for preferences in "${pending[@]}"; do + profile_open "$(dirname "$(dirname "$preferences")")" && return 0 + done + + for profile_root in "${profile_roots[@]}"; do + for backup in "$profile_root"/*/Preferences.omarchy-copy-url-repair.bak; do + [[ -f $backup ]] || continue + profile_open "$(dirname "$(dirname "$backup")")" && return 0 + done + done + + return 1 } find_pending @@ -134,16 +160,16 @@ if (( ! ${#pending[@]} )) && ! unverified_repairs_exist; then fi # A running browser holds Preferences in memory and rewrites the file on exit, -# reverting the repair, so ask for the windows to be closed first. gum draws -# the prompt on stderr, so it must stay attached: suppressing it leaves gum -# waiting for a keypress behind an unpainted screen. Without a terminal to ask -# in, gum fails; then — as on decline — fail so the migration stays pending, -# and the login notifier keeps prompting until a rerun goes through with -# browsers closed. -while browsers_running; do - if ! gum confirm "Close all browser windows to repair the Copy URL shortcut, then continue"; then +# reverting the repair, so ask for the affected windows to be closed first. gum +# draws the prompt on stderr, so it must stay attached: suppressing it leaves +# gum waiting for a keypress behind an unpainted screen. Without a terminal to +# ask in, gum fails; then — as on decline — fail so the migration stays +# pending, and the login notifier keeps prompting until a rerun goes through +# with the affected profiles closed. +while affected_profile_open; do + if ! gum confirm "Close the browser windows to repair the Copy URL shortcut, then continue"; then echo "A running browser would undo the Copy URL shortcut repair." >&2 - echo "Close all browser windows, then run: omarchy-migrate" >&2 + echo "Close the browser windows, then run: omarchy-migrate" >&2 exit 1 fi done @@ -159,9 +185,9 @@ done # A browser that started mid-repair read the stale Preferences and will write # them back on exit; stay pending so the next browser-free run can verify the # repair stuck. -if browsers_running; then +if affected_profile_open; then echo "A browser started during the Copy URL shortcut repair and may undo it on exit." >&2 - echo "Close all browser windows, then run: omarchy-migrate" >&2 + echo "Close the browser windows, then run: omarchy-migrate" >&2 exit 1 fi @@ -172,7 +198,7 @@ fi for preferences in "${pending[@]}"; do if python3 -c "$repair_py" "$preferences" "$pinned_id" check; then echo "A browser undid the Copy URL shortcut repair on exit." >&2 - echo "Close all browser windows, then run: omarchy-migrate" >&2 + echo "Close the browser windows, then run: omarchy-migrate" >&2 exit 1 fi done diff --git a/test/shell.d/copy-url-shortcut-migration-test.sh b/test/shell.d/copy-url-shortcut-migration-test.sh index 2783abeb..f8cf3497 100644 --- a/test/shell.d/copy-url-shortcut-migration-test.sh +++ b/test/shell.d/copy-url-shortcut-migration-test.sh @@ -12,7 +12,8 @@ test_dir=$(mktemp -d) trap 'rm -rf "$test_dir"' EXIT home="$test_dir/home" -preferences="$home/.config/chromium/Default/Preferences" +profile_root="$home/.config/chromium" +preferences="$profile_root/Default/Preferences" mkdir -p "$(dirname "$preferences")" # Any id Chromium once derived from the extension's keyless load path; the @@ -27,23 +28,38 @@ write_stale_preferences() { stub_bin="$test_dir/bin" mkdir -p "$stub_bin" +REAL_PYTHON=$(command -v python3) +export REAL_PYTHON + run_migration() { HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 } -# A running browser prompts for the windows to be closed; declining (or having -# no terminal to ask in) defers the repair so a rewrite-on-exit cannot revert -# it. -printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep" +# A running Chromium-family browser marks its profile root with a SingletonLock +# symlink to -, a target that never exists on disk. That lock — +# not the mere presence of a browser process — is what the migration waits on. +open_browser() { + mkdir -p "$profile_root" + ln -sfn "test-host-1234" "$profile_root/SingletonLock" +} +close_browser() { + rm -f "$profile_root/SingletonLock" +} + +# The affected profile being open prompts for the windows to be closed; +# declining (or having no terminal to ask in) defers the repair so a +# rewrite-on-exit cannot revert it. printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" -chmod +x "$stub_bin/pgrep" "$stub_bin/gum" +chmod +x "$stub_bin/gum" write_stale_preferences +open_browser + before_hash=$(sha256sum "$preferences" | cut -d' ' -f1) -run_migration && fail "migration defers while a browser is running" +run_migration && fail "migration defers while the affected profile is open" [[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] || - fail "migration leaves preferences alone while a browser is running" -pass "migration defers the repair while a browser is running" + fail "migration leaves preferences alone while the affected profile is open" +pass "migration defers the repair while the affected profile is open" # gum paints its prompt on stderr, so that stream has to stay attached: # suppressing it leaves gum reading keys behind an unpainted screen, which @@ -59,34 +75,47 @@ HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2 grep -q "gum-prompt-painted" "$prompt_stderr" || fail "migration keeps the browser prompt visible" pass "migration keeps the browser prompt visible" -# Confirming the prompt after closing the browser lets the repair proceed. +# A browser holding a different profile root cannot revert this repair, so it +# must not hold the update: the repair goes through without ever reaching the +# prompt, which the still-declining gum stub would otherwise fail. +close_browser +mkdir -p "$home/.config/google-chrome" +ln -sfn "test-host-1234" "$home/.config/google-chrome/SingletonLock" +write_stale_preferences +run_migration || fail "migration repairs while a different profile root is open" +jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null || + fail "migration repairs the shortcut while a different profile root is open" +pass "migration ignores a browser on a different profile root" +rm -f "$home/.config/google-chrome/SingletonLock" "$preferences.omarchy-copy-url-repair.bak" + +# Closing the affected profile and confirming the prompt lets the repair +# proceed. +write_stale_preferences +open_browser cat >"$stub_bin/gum" <<'STUB' #!/bin/bash +"$CLOSE_BROWSER" touch "${GUM_CALLED:?}" exit 0 STUB -cat >"$stub_bin/pgrep" <<'STUB' +cat >"$stub_bin/close-browser" <<'STUB' #!/bin/bash -count_file="${PGREP_COUNT_FILE:?}" -count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 )) -printf '%s\n' "$count" >"$count_file" -(( count == 1 )) && exit 0 || exit 1 +rm -f "$HOME/.config/chromium/SingletonLock" STUB -rm -f "$test_dir/pgrep-count" -GUM_CALLED="$test_dir/gum-called" PGREP_COUNT_FILE="$test_dir/pgrep-count" \ +chmod +x "$stub_bin/gum" "$stub_bin/close-browser" +GUM_CALLED="$test_dir/gum-called" CLOSE_BROWSER="$stub_bin/close-browser" \ HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 || - fail "migration proceeds once the browser prompt is confirmed" + fail "migration proceeds once the profile is closed and the prompt confirmed" [[ -e $test_dir/gum-called ]] || fail "migration asks before repairing under a running browser" jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null || fail "migration repairs after the browser prompt is confirmed" pass "migration asks to close the browser and repairs on confirmation" rm -f "$preferences.omarchy-copy-url-repair.bak" -printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" -printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" -write_stale_preferences -# With browsers closed the ghost registration moves to the pinned id. -printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +# With the affected profile closed the ghost registration moves to the pinned id. +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" +close_browser +write_stale_preferences run_migration || fail "migration repairs the shortcut when no browser is running" jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" ' @@ -99,15 +128,15 @@ jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" ' pass "migration rebinds the Copy URL shortcut to the pinned extension id" # A repaired profile has no ghost registration left, so nothing is pending — -# even while a browser is running. +# even while that same profile is open. rm "$preferences.omarchy-copy-url-repair.bak" repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1) -printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep" +open_browser run_migration || fail "migration reruns cleanly after the repair" [[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$repaired_hash" && ! -e $preferences.omarchy-copy-url-repair.bak ]] || fail "migration is idempotent after the repair" pass "migration is idempotent after the repair" -printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +close_browser # A remapped shortcut keeps the user's chosen key while moving to the pinned id. jq -n --arg ghost "$ghost_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences" @@ -127,60 +156,64 @@ jq -e --arg pinned "$pinned_id" ' pass "migration never double-binds the pinned extension" # A browser starting mid-repair may write stale Preferences back on exit, so -# the migration must stay pending for a later browser-free run to verify. +# the migration must stay pending for a later browser-free run to verify. A +# stub hands the repair call through and opens the profile right after it. write_stale_preferences -cat >"$stub_bin/pgrep" <<'STUB' +close_browser +rm -f "$preferences.omarchy-copy-url-repair.bak" +cat >"$stub_bin/python3" <<'STUB' #!/bin/bash -count_file="${PGREP_COUNT_FILE:?}" -count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 )) -printf '%s\n' "$count" >"$count_file" -(( count >= 2 )) && exit 0 || exit 1 +# Called as `python3 -c