feat: default Freenet install to a supervised, auto-updating service#4590
Conversation
|
I have all the information needed. Let me synthesize the full review. Rule Review: No blocking issues; one minor style noteRules checked: Checklistgit-workflow.md
code-style.md
testing.md (scope:
WarningsNone. Info
Rule review against |
Multi-model review: Codex (external, non-Claude)Ran Findings and resolutions:
Risk tier: Full (touches deploy/install + CI config). New decision logic is covered by [AI-assisted - Claude] |
Problem ------- A typical Linux Freenet install ends up unsupervised, so it never auto-updates. scripts/install.sh defaulted the "install as a service?" prompt to No, and a node with no service manager catching its exit-42 "update needed" signal exits to update and never restarts on the new version - it silently stops updating. With unsupervised being the dominant default on Linux, much of the network freezes on old releases. Solution -------- Make a supervised install the DEFAULT (issue #4073): - install.sh now sets up supervision unless the user explicitly opts out (FREENET_NO_SERVICE=1). The interactive prompt defaults to Yes ([Y/n]); a non-interactive curl|sh run sets up supervision automatically. - On Linux it prefers a SYSTEM service when it can elevate (already root, or sudo) - most reliable on the servers/VPS that dominate the node population (runs at boot, survives logout). When it cannot elevate it falls back to a USER service. A node is only left unsupervised as a last resort, with a loud warning explaining it will not auto-update. - The binary's user-service install now enables systemd lingering (`loginctl enable-linger <user>`) by default so a --user service runs without an active login session (the headless-server footgun: without linger it stops at logout and never auto-updates). New `--no-linger` flag opts out. System services are unaffected (they start at boot). - The decision honors an existing install so a re-run refreshes the same service type instead of creating a duplicate (idempotent + safe). The generated systemd units are unchanged - this reuses the existing unit generation, so the StartLimit/exit-45 work from #4570/#4588 is preserved. NOTE: this changes default install behavior (unsupervised -> supervised). Testing ------- - New scripts/test-install-sh.sh smoke-tests the system-vs-user decision (root / sudo / existing-unit / interactive permutations) by sourcing install.sh and overriding the environment probes. Wired into CI along with shellcheck on install.sh/uninstall.sh and the existing (previously unwired) uninstall smoke test. - New linux.rs unit test pins the lingering policy (system never lingers; user lingers unless --no-linger). - shellcheck clean; cargo fmt / clippy -D warnings / service tests green. Refs #4073 [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
Two issues found by Codex review of install.sh: - P1: a `curl | sudo sh` run installs the binary under /root/.local/bin (HOME=/root), so a system unit running as $SUDO_USER pointed ExecStart at a path that user cannot traverse - the service installs but fails to start. Now verify the service user can execute the binary (`sudo -u $SUDO_USER test -x`) before creating the unit; otherwise warn and tell them to re-run as the non-root user without sudo. - P2: when a system unit already exists, an unprivileged rerun without passwordless sudo failed the `sudo ... --system` refresh and then fell back to installing a USER service, leaving BOTH services installed. Now suppress the user-service fallback when a system unit already exists (the binary is already updated in place; the unit refresh just needs sudo) - avoiding the duplicate the existing-install routing prevents. [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
Codex round-2 finding: when a system unit already exists, install.sh auto-refreshed it via `sudo freenet service install --system`, and the binary derives User=/home/log/ExecStart from the CURRENT sudo user. So a rerun by a different sudo-capable account silently rewrote the unit to run as that account, orphaning the original node's data/identity. Add a same-user refresh guard: read the existing unit's `User=` and only refresh when it matches the user the refresh would run as (or can't be determined). Otherwise skip the refresh with a clear warning - the new binary is already on disk and is picked up on the next restart. New pure helper should_refresh_system_unit (unit-tested) plus existing_system_unit_user to read the current User=. [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
Codex round-3 finding: the post-install success message told users to run `sudo freenet service start --system`, but sudo's secure_path usually excludes ~/.local/bin, so a bare `sudo freenet` fails with "command not found" - users would think the freshly installed service can't start. Pass the absolute binary path into print_service_success and use it for the system (sudo'd) start command, plus offer `sudo systemctl start freenet` as an equivalent. The user (non-sudo) start hint is unchanged. [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
beecc2c to
e280ce2
Compare
Codex review (P2) on the default-supervised install flow: the root / SUDO_USER branch of `setup_service` verified only that the service user can EXECUTE the installed binary (`test -x`), not that it can REPLACE it. The auto-update path (ExecStopPost -> `freenet update`, run as $SUDO_USER) swaps the binary in place via `replace_binary`, which writes a temp file in the binary's directory and renames it over the binary. That needs WRITE permission on the directory (rename/unlink are governed by directory perms, not the file's mode/owner), which the exec check does not establish. A root-owned but world-executable install dir (e.g. FREENET_INSTALL_DIR=/usr/local/bin under `curl | sudo sh`) therefore passed the exec check, installed a system service, and then failed EVERY auto-update -- the exact silent "stops updating" failure default supervision exists to prevent. Add a directory-writability probe (`test -w` as the service user) after the exec check; when it fails, warn with remediation (install to a user-writable location) and leave the node unsupervised rather than standing up a service that can never update itself. shellcheck clean; scripts/test-install-sh.sh green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
Codex review (P2, round 2) on the default-supervised install flow: when an existing system service belongs to a DIFFERENT user, setup_service correctly skips re-templating the unit (refreshing as another user would re-point User=/home/log/ExecStart and orphan the original node's data/identity). But it then printed "The updated binary is in place; restart the service to use it", which is wrong with the default per-user install dir: this run downloaded the new binary into the CURRENT user's home, while the existing unit's ExecStart still points at the original user's binary -- so a restart keeps running the OLD version. Read the existing unit's ExecStart path and only claim the binary is in place when it matches where this run installed (e.g. a shared FREENET_INSTALL_DIR). When the paths differ, tell the operator the service was NOT updated and to re-run the installer as the service user so the binary lands where the unit looks for it. shellcheck clean; scripts/test-install-sh.sh green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGjU1Q6Vpk2dm4sUf4pdU
|
Reviewed + merging. Rebased onto the post-#4591/#4593 main (clean — install logic vs unit-template changes touch disjoint regions); verified every required unit directive survived (StartLimit* in [Unit], StartLimitAction=none, exit-45 marker, ExecStopPost catch-all firing update on 42/45/any-crash, RestartSteps/RestartMaxDelaySec, SuccessExitStatus) via grep + [AI-assisted - Claude] |
Problem
A typical Linux Freenet install ends up unsupervised, so it never auto-updates.
scripts/install.shdefaulted the "install as a service?" prompt to No, and a node with no service manager catching its exit-42 "update needed" signal exits to update and never restarts on the new version - it silently stops updating. Since unsupervised is the dominant default on Linux, much of the network freezes on old releases.Solution
Make a supervised install the default (part of the auto-update work, #4073):
install.shsets up supervision by default, unless the user explicitly opts out withFREENET_NO_SERVICE=1. The interactive prompt now defaults to Yes ([Y/n]); a non-interactivecurl | shrun sets up supervision automatically (the worse default is silently-never-updates).loginctl enable-linger <user>) by default, so a--userservice runs without an active login session. This is the essential headless-server fix: without lingering, a user service stops at logout and never catches exit-42, so it never auto-updates. A new--no-lingerflag opts out. System services are unaffected (they start at boot regardless).The generated systemd units are unchanged - this reuses the existing unit generation, so the
StartLimit/exit-45 work from #4570/#4588 is preserved.Important
This changes default install behavior (unsupervised -> supervised). New installs (and re-runs over an existing install) will now set up a systemd service by default.
Warning
freenet.org/install.sh mirror must be updated in lockstep.
scripts/install.shis mirrored athugo-site/static/install.shinfreenet/web(served from https://freenet.org/install.sh). The website copy is what actualcurl | shusers get, so this change does not reach users until the mirror is synced. I can't deploy the website - @sanity to update the mirror after merge.Testing
scripts/test-install-sh.shsmoke-tests the system-vs-user decision (root / passwordless-sudo / existing-unit / interactive permutations) by sourcinginstall.shand overriding the environment probes - no real root/sudo needed.linux.rsunit test pins the lingering policy: a system service never lingers; a user service lingers unless--no-linger.ci.yml): the new install smoke test, the existing-but-previously-unwired uninstall smoke test, andshellcheckoninstall.sh/uninstall.sh/ both test scripts.cargo fmt,cargo clippy -p freenet --bins -- -D warnings, and the service module tests (95) are green;shellcheckclean.Refs #4073
[AI-assisted - Claude]