Repository navigation
Conversation
…ary (nxf 6j6v.c92y) `nxs self-update` swapped the file the service's alias resolves to, but launchd kept executing the process it had started from the old file until a restart or reboot. Fixes stayed inactive in the service and nothing said so. After the swap, self-update now restarts the ambient instance when its plist is installed and its alias resolves to the binary just swapped. The restart goes through nxs_service::launchd::restart_with, which is install_with (the bootout + bootstrap + read-back `nxs sync daemon install` runs) without re-pointing the alias. No launchctl verb is spelled in self-update; the real handle is only ever obtained through RealCtl::for_login_session. - An alias pointing at another build: no restart, the existing note stays. - No installed service (no alias, or an alias beside a removed plist): nothing. - A failed or refused restart is reported with `nxs sync daemon install` and never fails the update. - --json carries a `service_restart` object (would-restart / restarted / failed), absent when there is nothing to restart; --check reports would-restart. - `nxs sync daemon status` notes when the running service's version differs from the binary its alias names, when this binary is that file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184D5PqXGRbwMKDkMkRggdZ
…nxf 6j6v.c92y) - Keep the installed PATH: restart_with reads PATH from the installed plist and falls back to the caller's only when the plist has none (Integrity #2). - Say what a failed restart left: untouched / still loaded / STOPPED / unknown, read back from launchd after the failure (Integrity #1). - Skip the restart, and print the command, when this update runs inside a process the service started, found by walking the ancestors to the live heartbeat pid (Code #2, Integrity #3). - Restarted carries launchd's state; the line says "launchd holds the new job (state: ...)" and points at `nxs sync daemon status` (Integrity #4). - Restart after a swap whose later step failed, detected by the inode changing (Integrity #5). - Name sibling instances that run the same binary (Code #3); name a stale service on the up-to-date path, in --check too (decision 3, Integrity #6). - ServiceAt is the one seam for the service glue; check_output, updated_output and partial_swap_output build the Output that run prints, so the wiring and stream routing are tested (Test #1, #2, #3, #5). Line enum instead of (bool, String) (Code #4). The restart tests are cfg(unix) (Code #5). - stale_process_note moves to nxs_service::program, takes ServiceState and says "different". status builds its note through program_note, tested for order and liveness (Code #1, Test #6). Exact verb sequence with the gui/<uid> domain (Test #4). - Changelog names the old-template limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184D5PqXGRbwMKDkMkRggdZ
|
Review mitigated in a59b63c (17 fixed, 1 declined). Every new or changed test is red under a targeted mutation (applied by script, filtered tests run, file restored): nofix, noplist, anyalias, stoppedasuntouched, noguard, nowalk, nopost, routing, checkrestarts, nostate, nosiblings, nopartial, nostaleuptodate (selfupdate.rs); updaterpath (launchd.rs, red in both nxs-service and cli tests); unconfirmedspeaks, nostale (program.rs); noteorder (daemon.rs). Declined: Integrity #7 (informational; the canonicalize fallback is safe-unequal, and the TOCTOU is covered by the bootstrap retry). New behaviour: PATH taken from the installed plist; failed restart reports untouched/still-loaded/stopped/unknown; restart skipped when the updater descends from the live service pid; siblings named; stale service named on the up-to-date path; restart after a partial swap; changelog names the old-template limit. 🤖 Generated with Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
…swered escalations release, store-free lanes, next filters and paging, per-type ranking, self-update restarts the service (nxf 6j6v.9g8j, 1wep, ys54, t1ym, 15ed, z9jk, c92y) (#38) Folds the reviewed and mitigated PRs #38 (9g8j), #39 (1wep), #40 (ys54), #41 (c92y), #42 (t1ym), #43 (15ed) and #44 (z9jk). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
|
Included in #38, which folded this reviewed and mitigated PR together with the rest of the v0.207 batch and is merged. |
Summary
nxf 6j6v.c92y (bug, P1).
nxs self-updateswapped the binary the background service's alias resolves to, but launchd kept running the process it had started from the old file. Measured on 2026-10-09: the service had been running since 10-07 through two updates, so the fixes of r91p and br25 were inactive. Nothing reported this.Owner decision (2026-10-09): self-update restarts the service itself, through the same path as
nxs sync daemon install, and only for a service whose alias resolves to the binary it just swapped.What changed
nxs_service::launchd::restart_with(new, about 10 lines): callsinstall_withfor a home's instance with its own alias, plist and log directory. That runs the same bootout + bootstrap + retry + read-back that install runs. One difference: it does not re-point the alias. The caller has already checked that the alias names the binary it wants, and re-linking it tocurrent_exe()would be a second decision about which build the service runs (7gz6).self-update, after the swap (crates/cli/src/selfupdate.rs): it restarts the ambient instance only when both of these hold:<install dir>/nxs.The real launchd handle comes only from
RealCtl::for_login_session(MustBeTheLoginSessions), and only when there is something to restart. self-update spells nolaunchctlverb.service_alias_notewarning is unchanged.uninstallremoved the plist". Restarting in that case would re-install a service the user removed.nxs sync daemon install.--json/--check: aservice_restartobject reports one of these:{"instance","status":"would-restart"}with--checkwhen an update is available{"status":"restarted","confirmed":bool}{"status":"failed","error","command":"nxs sync daemon install"}The object is absent when there is nothing to restart, the same as
service_alias, so the existing receipts stay byte-identical on machines without a service. Human mode prints one matching line.nxs sync daemon status(crates/nxs/src/sync/daemon.rs): newstale_process_note. When the service is running, its heartbeat version differs from this binary's version, and this binary is the alias's target,statusadds a note with both versions and the command. It fills the existingprogram.notefield, which is disjoint fromalias_note(that one speaks only when the paths differ), so the JSON shape gets no new key.nxs doctoris unchanged; the status note covers the item, so no follow-up item was created.Changelog fragment
changes/self-update-restarts-the-service.md(fixed, en + de). Nofacade:line, because no consumed surface is touched.Tests
All of these run against a
SpyCtlbehind theLaunchCtlseam, with a service home, alias and plist in aTempDir. No test constructsRealCtl.an_update_whose_service_runs_the_swapped_binary_restarts_it_once_through_the_install_pathbootstrapof this plist, preceded bybootoutof the label, nokickstart, alias unchanged,Restarted { confirmed: true }(read back from the spy)an_update_whose_service_runs_another_build_restarts_nothing_and_keeps_the_notetarget/debugbuild:None, launchd handle never asked for, zero launchctl calls, alias note still names the commanda_machine_with_no_installed_service_gets_no_restarta_failed_restart_names_the_command_and_does_not_fail_the_updateFailed; receipt stillstatus: updatedwithservice_restart.command = "nxs sync daemon install"; human line goes to stderr and contains the command; a refused handle (home check) gives the same resultcheck_reports_a_restart_it_would_do_and_receipts_without_one_are_unchanged--checkreportswould-restart; without a restart the receipt gets no new keya_running_service_older_than_the_binary_its_alias_names_is_reported/a_stale_process_is_claimed_only_where_it_can_be_knownRed without the fix, shown by mutation. Each mutation was applied by a script, the filtered tests were run, and the file was restored:
nofix:restart_service_after_swapalways returnsNone(the pre-fix behaviour)…restarts_it_once_through_the_install_path,a_failed_restart_…noplist: drop theplist.exists()guarda_machine_with_no_installed_service_gets_no_restartanyalias: restart whatever the alias points atan_update_whose_service_runs_another_build_…swallow: map a failed restart toRestarteda_failed_restart_names_the_command_and_does_not_fail_the_updatenostale:stale_process_notealways returnsNonea_running_service_older_than_the_binary_its_alias_names_is_reportedRun locally:
cargo test -p nexus-flow-cli --lib selfupdate(60 passed),cargo test -p nxs --lib -- stale_process a_running_service_older,cargo check -p nxs-service -p nexus-flow-cli -p nxs --tests(no warnings),cargo fmt,cargo xtask changelog check origin/main. No clippy and no full suite; CI is the gate.Spec check
None found.
release-management.md§7.2 / TB-4 rule out background auto-update of the binary. This change restarts the service only as part of aself-updatethe user explicitly ran, and it changes no binary.E4-continuous-sync-client.mddescribes the install path being reused here, and nothing in it contradicts this.Open decisions for the owner
nxs sync daemon installaddresses, so the failure command is exactly right for it. A sibling instance (for examplenexus-flow-dev) whose alias also points at the installed binary is not restarted. Since cvpy, dev instances are installed from builds, so that case is a leftover from before cvpy. I found no correct one-line command to give for a sibling whose restart fails. Extending the restart to siblings is easy (ServiceHome::siblings()+restart_with) if wanted.render_plist. The program path is the alias, so the service does come back on the new binary. But if a release changes the plist template (keys,PATH), that change only arrives with the next manualnxs sync daemon install. The alternative is to spawn the new<dir>/nxs sync daemon installas a child process. That would not be provable through theLaunchCtlseam, which the brief requires.self-update. It will be restarted by the next update that swaps anything.nxs sync daemon statusnow names the stale process and the command in the meantime.🤖 Generated with Claude Code
https://claude.ai/code/session_0184D5PqXGRbwMKDkMkRggdZ