The migration asked a user to close every running browser before repairing the Copy URL shortcut, but a browser only ever rewrites its own Preferences on exit. Waiting on all browsers deadlocks `omarchy update` for anyone whose main browser is effectively never closed: the pending ghosts commonly sit in a stale profile nobody has open, yet the migration blocks on the always-open browser until the prompt is declined, failing the whole update. A running Chromium-family browser holds a SingletonLock (and socket) inside its user-data-dir, so whether the profile being repaired is open is mechanical. Gate on that instead of on the sheer presence of a browser process — the repair proceeds where the affected profile is closed, and browsers attached to other profiles no longer hold the update hostage. The gate stays conservative while an affected profile actually is open, and the existing post-repair verification still catches a browser that starts mid-repair and restores stale Preferences on exit. The migration test now simulates an open profile with its SingletonLock instead of a pgrep stub; every prior scenario still passes.
205 lines
7.3 KiB
Bash
205 lines
7.3 KiB
Bash
echo "Repair the Copy URL shortcut for profiles that predate its pinned extension id"
|
|
|
|
# Chromium never hands a suggested shortcut to one extension while another —
|
|
# even a long-gone one — still holds the registration. Profiles that first
|
|
# loaded Copy URL before its id was pinned registered Alt+Shift+L under an id
|
|
# derived from the extension's load path at the time, so the pinned extension
|
|
# never receives the shortcut and the keypress does nothing. Those historical
|
|
# ids are unknowable in general (they hash long-gone absolute paths), but the
|
|
# registration itself names the command: rebind any copy-url command that
|
|
# points away from the pinned id, unless that id belongs to an extension that
|
|
# is actually installed.
|
|
|
|
pinned_id="bgpiichlckmfanooecilcjemknkcpngb"
|
|
|
|
repair_py=$(cat <<'PY'
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
pinned_id = sys.argv[2]
|
|
mode = sys.argv[3]
|
|
|
|
try:
|
|
preferences = json.loads(path.read_text())
|
|
except (OSError, ValueError):
|
|
sys.exit(1)
|
|
|
|
extensions = preferences.get("extensions", {})
|
|
commands = extensions.get("commands", {})
|
|
settings = extensions.get("settings", {})
|
|
|
|
def ghost_id(command):
|
|
if command.get("command_name") != "copy-url":
|
|
return None
|
|
extension = command.get("extension")
|
|
if not extension or extension == pinned_id:
|
|
return None
|
|
# An installed extension records its install path in settings; only the
|
|
# ghosts of Copy URL itself may be rebound.
|
|
install_path = settings.get(extension, {}).get("path", "")
|
|
if install_path and not install_path.endswith("/default/chromium/extensions/copy-url"):
|
|
return None
|
|
return extension
|
|
|
|
if mode == "check":
|
|
sys.exit(0 if any(ghost_id(c) for c in commands.values()) else 1)
|
|
|
|
# Chromium keeps one shortcut per command, so a ghost may only be rebound
|
|
# when the pinned extension holds no copy-url binding of its own; further
|
|
# ghosts are dropped rather than doubled up.
|
|
pinned_bound = any(
|
|
c.get("command_name") == "copy-url" and c.get("extension") == pinned_id
|
|
for c in commands.values()
|
|
)
|
|
|
|
changed = False
|
|
for accelerator in list(commands):
|
|
ghost = ghost_id(commands[accelerator])
|
|
if not ghost:
|
|
continue
|
|
if pinned_bound:
|
|
del commands[accelerator]
|
|
else:
|
|
commands[accelerator]["extension"] = pinned_id
|
|
pinned_bound = True
|
|
pinned_command = settings.get(pinned_id, {}).get("commands", {}).get("copy-url", {})
|
|
if pinned_command:
|
|
pinned_command["was_assigned"] = True
|
|
settings.pop(ghost, None)
|
|
changed = True
|
|
|
|
if changed:
|
|
shutil.copy2(path, path.with_name(path.name + ".omarchy-copy-url-repair.bak"))
|
|
path.write_text(json.dumps(preferences, separators=(",", ":")))
|
|
PY
|
|
)
|
|
|
|
profile_roots=(
|
|
"$HOME/.config/chromium"
|
|
"$HOME/.config/google-chrome"
|
|
"$HOME/.config/google-chrome-beta"
|
|
"$HOME/.config/google-chrome-unstable"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
|
|
"$HOME/.config/BraveSoftware/Brave-Browser-Nightly"
|
|
"$HOME/.config/microsoft-edge"
|
|
"$HOME/.config/microsoft-edge-beta"
|
|
"$HOME/.config/microsoft-edge-dev"
|
|
"$HOME/.config/vivaldi"
|
|
"$HOME/.config/opera"
|
|
"$HOME/.config/helium"
|
|
)
|
|
|
|
find_pending() {
|
|
pending=()
|
|
for profile_root in "${profile_roots[@]}"; do
|
|
[[ -d $profile_root ]] || continue
|
|
|
|
for preferences in "$profile_root"/*/Preferences; do
|
|
[[ -f $preferences ]] || continue
|
|
python3 -c "$repair_py" "$preferences" "$pinned_id" check || continue
|
|
pending+=("$preferences")
|
|
done
|
|
done
|
|
}
|
|
|
|
# The backup a repair leaves next to Preferences marks it as attempted but not
|
|
# yet verified: a browser that was open during the repair still holds the
|
|
# stale registration in memory and restores it when it exits, possibly after
|
|
# the disk was already seen clean.
|
|
unverified_repairs_exist() {
|
|
local profile_root backup
|
|
|
|
for profile_root in "${profile_roots[@]}"; do
|
|
for backup in "$profile_root"/*/Preferences.omarchy-copy-url-repair.bak; do
|
|
[[ -f $backup ]] && return 0
|
|
done
|
|
done
|
|
|
|
return 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
|
|
if (( ! ${#pending[@]} )) && ! unverified_repairs_exist; then
|
|
exit 0
|
|
fi
|
|
|
|
# A running browser holds Preferences in memory and rewrites the file on exit,
|
|
# 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 the browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# A browser that just closed may have restored ghosts an earlier repair had
|
|
# already removed, so only now is the pending list authoritative.
|
|
find_pending
|
|
|
|
for preferences in "${pending[@]}"; do
|
|
python3 -c "$repair_py" "$preferences" "$pinned_id" repair
|
|
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 affected_profile_open; then
|
|
echo "A browser started during the Copy URL shortcut repair and may undo it on exit." >&2
|
|
echo "Close the browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# One that started and already exited mid-repair wrote its stale in-memory
|
|
# Preferences back over the repair; verify every file again. A browser
|
|
# starting after this point reads the repaired file, so it can no longer
|
|
# restore the ghost.
|
|
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 the browser windows, then run: omarchy-migrate" >&2
|
|
exit 1
|
|
fi
|
|
done
|