Skip to content

fix(seedbox): give the watchdog a bus to systemd, and stop passing off a daemon we watched die - #149

Merged
ralyodio merged 2 commits into
masterfrom
fix/seedbox-watchdog-systemd
Jul 31, 2026
Merged

fix(seedbox): give the watchdog a bus to systemd, and stop passing off a daemon we watched die#149
ralyodio merged 2 commits into
masterfrom
fix/seedbox-watchdog-systemd

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Follow-up to #139#141, which each tried to keep torlink alive and each left the box still answering ECONNREFUSED. Two concrete defects, both verified.

1. The watchdog could never reach systemd

.torlnk-watchdog.sh runs from cron. cron has no login session, so systemctl --user fails with "Failed to connect to user scope bus" — and the call is >/dev/null 2>&1, so it failed silently on every run. Reproduced locally:

$ env -u XDG_RUNTIME_DIR -u DBUS_SESSION_BUS_ADDRESS systemctl --user is-system-running
Failed to connect to user scope bus via local transport:
$DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined

So every 5 minutes the watchdog skipped the systemd restart it exists to perform and fell through to launching a bare, unsupervised --daemon — which then fought the still-enabled Restart=always unit for the serve port. Whichever lost crash-looped forever against a port it could never bind. The supervision added in #139 was being bypassed by the watchdog meant to back it up.

Fix: export XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS pointing at the lingering user manager. $(id -u) is escaped so it resolves when the watchdog runs, not when the heredoc is written (a bare $(id -u) in the unquoted heredoc would bake in the provisioning shell's uid).

Also: systemctl restart exiting 0 does not mean the port is bound — restarts are async. The old restart … && exit 0 short-circuit treated a queued restart as success. Now it re-probes /health for ~12s and only falls back to an unsupervised daemon once systemd has demonstrably not taken over, so the two can't race.

2. Provisioning reported success for a daemon it watched die

#141 added the stable step — re-probe after ~15s and report if torlink is already gone. But provisionTorlink derived ok from the serve step alone, and RESULT|ok is echoed unconditionally. So install-torlink returned 200, wired the fresh token into the account config, and rendered a green "installed" even when the script had just reported the daemon was dead — and the next status poll said ECONNREFUSED. That's the reinstall-says-OK-then-unreachable loop.

Fix: let stable fail veto success, so the route 502s with the steps (including the journal tail explaining what killed it) instead of saving a dead box. A missing stable step — older script output — still passes. The probe now retries 3× as well, since it gates the whole install and one dropped packet must not fail a healthy box.

Testing

  • 8 new tests: bus env present and ordered before the real systemctl call (comment-stripped, so it asserts on commands not prose), uid resolved at run time, health re-probe between restart and fallback, retry loop, and provisionTorlink returning ok:false/token:null on stable fail (+ back-compat when the step is absent).
  • Full suite green: 178 files, 2465 passed, typecheck and eslint clean.

Note

This makes recovery work going forward; it does not revive a box that is down right now. That still needs systemctl --user restart torlink-serve torlink-files on the seedbox, or a re-run of the installer.

…f a daemon we watched die

Two defects kept leaving the box answering ECONNREFUSED despite #139#141.

1. The watchdog could never reach systemd. `.torlnk-watchdog.sh` runs from cron,
   which has no login session, so `systemctl --user` failed on every invocation
   with "Failed to connect to user scope bus" — silently, since the call was
   `>/dev/null 2>&1`. The watchdog therefore skipped the restart it exists to
   perform and fell through to launching a bare unsupervised `--daemon`, which
   then fought the still-enabled `Restart=always` unit for the serve port; the
   loser crash-looped forever against a port it could never bind. Export
   XDG_RUNTIME_DIR/DBUS_SESSION_BUS_ADDRESS (resolved at run time via `id -u`,
   not baked in when the heredoc is written) so it talks to the lingering user
   manager.

   Also stop treating `systemctl restart` exiting 0 as proof the port is bound —
   restarts are async. Re-probe /health for ~12s and only fall back to an
   unsupervised daemon once systemd has demonstrably not taken over.

2. Provisioning reported success for a daemon it had already seen die. The
   script emits `stable fail` when torlink is gone within ~15s, but
   provisionTorlink derived `ok` from the `serve` step alone, so install-torlink
   returned 200, wired the fresh token into the account config, and showed a
   green "installed" — and the next status poll said ECONNREFUSED. Let the
   `stable` step veto success. Absent (older script output) still passes, and
   the probe now retries so one dropped packet cannot fail a healthy box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio marked this pull request as ready for review July 31, 2026 05:14
@ralyodio
ralyodio merged commit 5605a55 into master Jul 31, 2026
8 checks passed
@ralyodio
ralyodio deleted the fix/seedbox-watchdog-systemd branch July 31, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant