Detach the pending-migrations toast from the notifier

omarchy-migrate-notify handed its notification to `systemd-run --scope`,
which is synchronous: the calling process becomes the payload, so
systemd-run does not return until the toast has been answered -- and if
the toast is clicked, not until the migration terminal it opens has been
closed.

omarchy-migrate-notify.service is Type=oneshot, which defaults to
TimeoutStartUSec=infinity, so the unit sat in activating for exactly that
long. On a machine that had not yet picked up c7e327b0, which orders the
notifier after graphical-session.target, that held the target open --
and wayland-wm-app-daemon.service is ordered after the same target, so it
never started. Every keybinding goes through uwsm-app, which waits ten
seconds for that daemon's pipes and then reports "App failure -- Timed
out waiting for pipes!" instead of launching anything. Keybindings were
dead for two minutes and seventeen seconds, until a pacman hook ran
`systemctl reload user@*.service`, whose re-exec of the user manager
broke the scope's bus connection and let the oneshot finish. That also
made systemd-run exit non-zero, so the notifier fell through to the
terminal fallback meant for having no user manager at all, and printed
the migration list into the journal.

Hand the notification to a transient service instead. systemd-run
returns once the unit has started rather than once the payload is done,
so the oneshot completes in milliseconds and nothing ordered after the
target waits on a toast. Put it in background-graphical.slice, which
systemd ships with PartOf=graphical-session.target, so an unanswered
toast ends at logout rather than outliving the session.

The terminal a clicked toast opens is unaffected: uwsm-app re-registers
it into app-graphical.slice, outside this unit's cgroup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-27 11:19:20 -07:00
co-authored by Claude Opus 5
parent 1472fc1b38
commit b6e1a43278
2 changed files with 52 additions and 7 deletions
+6 -3
View File
@@ -40,11 +40,14 @@ if update_in_progress; then
exit 0 exit 0
fi fi
# A transient service rather than a scope, which would block here until the
# toast was answered and keep this oneshot activating for that whole time. The
# graphical slice ends an unanswered toast at logout.
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)" unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0 systemd-run --user --collect --slice=background-graphical.slice --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
# Reached when there is no user manager to run the scope under, such as a # Reached when the notification could not be handed off, so fall back to telling
# non-graphical shell, so fall back to telling the user in the terminal. # the user in the terminal.
print_pending_migrations() { print_pending_migrations() {
echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:" echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:"
while IFS= read -r migration; do while IFS= read -r migration; do
+46 -4
View File
@@ -29,7 +29,13 @@ if [[ ${OMARCHY_TEST_SYSTEMD_RUN:-run} == "fail" ]]; then
fi fi
command=${!#} command=${!#}
bash -c "$command"
# --scope blocks on the command, a transient service detaches it.
if [[ " $* " == *" --scope "* ]]; then
bash -c "$command"
else
setsid bash -c "$command" >/dev/null 2>&1 &
fi
SH SH
chmod +x "$stub_bin/systemd-run" chmod +x "$stub_bin/systemd-run"
@@ -61,7 +67,18 @@ chmod +x "$stub_bin/omarchy-notification-wait"
cat >"$stub_bin/omarchy-notification-send" <<'SH' cat >"$stub_bin/omarchy-notification-send" <<'SH'
#!/bin/bash #!/bin/bash
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS" # Written whole so a reader polling for the file never sees half the arguments.
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS.partial"
mv "$OMARCHY_TEST_NOTIFY_ARGS.partial" "$OMARCHY_TEST_NOTIFY_ARGS"
# Stands in for a toast nobody has answered yet, until the test releases it.
if [[ -n ${OMARCHY_TEST_NOTIFY_HOLD:-} ]]; then
for _ in {1..200}; do
[[ -e $OMARCHY_TEST_NOTIFY_HOLD ]] || break
sleep 0.05
done
: >"$OMARCHY_TEST_NOTIFY_ANSWERED"
fi
SH SH
chmod +x "$stub_bin/omarchy-notification-send" chmod +x "$stub_bin/omarchy-notification-send"
@@ -77,9 +94,20 @@ run_notify() {
OMARCHY_TEST_SYSTEMD_RUN="${2:-run}" \ OMARCHY_TEST_SYSTEMD_RUN="${2:-run}" \
OMARCHY_TEST_LOCK_DURING_WAIT="${OMARCHY_TEST_LOCK_DURING_WAIT:-0}" \ OMARCHY_TEST_LOCK_DURING_WAIT="${OMARCHY_TEST_LOCK_DURING_WAIT:-0}" \
OMARCHY_TEST_LOCK_HOLDER_PID="$test_tmp/lock-holder-pid" \ OMARCHY_TEST_LOCK_HOLDER_PID="$test_tmp/lock-holder-pid" \
OMARCHY_TEST_NOTIFY_HOLD="${OMARCHY_TEST_NOTIFY_HOLD:-}" \
OMARCHY_TEST_NOTIFY_ANSWERED="$test_tmp/notify-answered" \
"$ROOT/bin/omarchy-migrate-notify" "$ROOT/bin/omarchy-migrate-notify"
} }
# The notification outlives the notifier, so its arguments land after it exits.
wait_for_notify_args() {
for _ in {1..200}; do
[[ -s $test_tmp/notify-args ]] && return 0
sleep 0.05
done
return 1
}
run_notify 0 >"$test_tmp/not-pending.out" 2>"$test_tmp/not-pending.err" run_notify 0 >"$test_tmp/not-pending.out" 2>"$test_tmp/not-pending.err"
[[ ! -s $test_tmp/not-pending.out ]] || fail "migration notifier stays quiet on stdout without pending migrations" [[ ! -s $test_tmp/not-pending.out ]] || fail "migration notifier stays quiet on stdout without pending migrations"
[[ ! -s $test_tmp/not-pending.err ]] || fail "migration notifier stays quiet on stderr without pending migrations" [[ ! -s $test_tmp/not-pending.err ]] || fail "migration notifier stays quiet on stderr without pending migrations"
@@ -91,6 +119,7 @@ grep -q '200-migration.sh' "$test_tmp/pending.err" || fail "migration notifier l
pass "migration notifier reports pending migrations" pass "migration notifier reports pending migrations"
run_notify 1 >"$test_tmp/notified.out" 2>"$test_tmp/notified.err" run_notify 1 >"$test_tmp/notified.out" 2>"$test_tmp/notified.err"
wait_for_notify_args || fail "migration notifier sends a notification for pending migrations"
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || fail "migration notifier uses pending migrations title" grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || fail "migration notifier uses pending migrations title"
grep -Fx 'Click to run 1 pending migration.' "$test_tmp/notify-args" >/dev/null || fail "migration notifier describes the pending migration" grep -Fx 'Click to run 1 pending migration.' "$test_tmp/notify-args" >/dev/null || fail "migration notifier describes the pending migration"
grep -Fx '' "$test_tmp/notify-args" >/dev/null || fail "migration notifier includes the large-slot glyph" grep -Fx '' "$test_tmp/notify-args" >/dev/null || fail "migration notifier includes the large-slot glyph"
@@ -114,7 +143,8 @@ pass "migration notifier stays quiet while omarchy update holds its lock"
exec {update_lock_fd}>&- exec {update_lock_fd}>&-
run_notify 1 >/dev/null 2>&1 run_notify 1 >/dev/null 2>&1
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || wait_for_notify_args &&
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
fail "migration notifier resumes notifying once the update lock is released" fail "migration notifier resumes notifying once the update lock is released"
pass "migration notifier resumes notifying after the update releases its lock" pass "migration notifier resumes notifying after the update releases its lock"
@@ -142,8 +172,20 @@ exec {foreign_lock_fd}>"$foreign_lock"
flock -n "$foreign_lock_fd" || fail "test could not hold the foreign update lock" flock -n "$foreign_lock_fd" || fail "test could not hold the foreign update lock"
run_notify 1 >/dev/null 2>&1 run_notify 1 >/dev/null 2>&1
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || wait_for_notify_args &&
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
fail "migration notifier ignores update locks outside its own runtime directory" fail "migration notifier ignores update locks outside its own runtime directory"
pass "migration notifier ignores update locks outside its own runtime directory" pass "migration notifier ignores update locks outside its own runtime directory"
exec {foreign_lock_fd}>&- exec {foreign_lock_fd}>&-
# The notifier is a Type=oneshot with no start timeout, so blocking on the toast
# leaves it activating until the user answers.
rm -f "$test_tmp/notify-args" "$test_tmp/notify-answered"
: >"$test_tmp/notify-hold"
OMARCHY_TEST_NOTIFY_HOLD="$test_tmp/notify-hold" run_notify 1 >/dev/null 2>&1
wait_for_notify_args || fail "migration notifier sends the notification it detaches"
[[ ! -e $test_tmp/notify-answered ]] ||
fail "migration notifier waited for the toast to be answered before exiting"
rm -f "$test_tmp/notify-hold"
pass "migration notifier exits while the toast is still unanswered"