Skip to content

fix(deploy): keep manage after uninstall so manage purge stays reachable - #479

Open
dormouse-bot wants to merge 2 commits into
mainfrom
fix/issue-473
Open

fix(deploy): keep manage after uninstall so manage purge stays reachable#479
dormouse-bot wants to merge 2 commits into
mainfrom
fix/issue-473

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

manage uninstall printed Use "manage purge" separately to delete those irreversibly. and then deleted $ROOT/bin — where manage itself lives — so the command it had just pointed at was gone by the time the banner was on screen. uninstall now removes bin/run-server instead of the whole bin directory, and purge closes by printing the one command that clears what is left — the install root and the log directory, which sits outside it on Linux and macOS. Verified by running the generated manage scripts against fake install roots (below); the bug reproduces on main and does not on this branch.

#473 left the fix as an operator-experience call and deliberately proposed none. You asked for the PR in #472, so this makes the call — preserve manage across uninstall, the second of the issue's three options — and is a single revertible commit if you want a different one.

Why this option

The other two do not actually fix the reported thing. Reordering the message to "purge first, then uninstall" leaves anyone who already ran uninstall — the common path — with no entry point, and inverts the natural order for everyone else. Advertising purge in the closing install banner is real but orthogonal: it improves discovery before teardown and changes nothing about reachability after it. Left out here so this stays one concern; happy to add it separately.

Preserving manage gives the install root a statable invariant: uninstall removes the service and the code; what is left is your data plus the one tool that can delete it. config/ and state/ were already preserved by design, so bin/manage joins them rather than being a new exception.

purge does not delete itself. Self-deletion is the same class of problem this PR is fixing — on Windows in particular, cmd.exe may hold manage.cmd open for the duration, which I can't test from a Linux runner — so it prints the rm -rf / Remove-Item line instead. That line appears only when bin/run-server is already gone, so a purge on a live install is unchanged.

Side effect worth naming: the triage comment on #473 flagged that Windows' Remove-Tree $bin may already be failing partially against that same cmd.exe handle, with Invoke-Uninstall reporting success regardless. That delete is gone now, so the hazard goes with it — but it was never confirmed on a Windows box, and this PR does not confirm it either.

Verification

No automated coverage exists for this path — DORMOUSE_INSTALL_TEST=1 stops before systemd/launchd/Serve and never runs manage uninstall, which is why the bug survived. So I extracted the manage scripts the installers generate and ran them against throwaway roots.

The fixture has to put the logs at their real location. Both manage scripts carry [ -d "$ROOT/logs" ] && LOG_DIR="$ROOT/logs", so a DORMOUSE_INSTALL_ROOT-shaped root puts the logs inside it and makes a root-only teardown command look complete when it isn't — that is how the second bug fixed here got past the first pass. The transcripts below are from a fixture with HOME pointed at a throwaway tree and no $ROOT/logs.

Linux and macOS — full uninstall → purge, plus the baseline on main

The generated Linux manage, run with ROOT=$HOME/.local/share/dormouse-server holding bin/{manage,run-server}, config/, state/, releases/, run/, current, and the logs at $XDG_STATE_HOME/dormouse-server/logs — outside the root, as on a real install:

$ manage uninstall          # answered y on a pty
This script is left in place so "purge" can still delete them
irreversibly afterwards:

  ".../dormouse-server/bin/manage" purge
...
uninstalled. config and state remain at:
  .../dormouse-server/config
  .../dormouse-server/state

delete them irreversibly with:

  ".../dormouse-server/bin/manage" purge

The command it names then works, and the command that prints finishes the job:

$ manage purge              # typed the confirmation phrase
purged.

the service and code were already uninstalled; what remains is
this script and the logs:

  rm -rf ".../.local/share/dormouse-server" ".../.local/state/dormouse-server"

$ rm -rf ".../.local/share/dormouse-server" ".../.local/state/dormouse-server"
$ find "$HOME" -mindepth 1
~/.local  ~/.local/share  ~/.local/state      # XDG dirs only; nothing of dormouse's

The macOS manage produces the LaunchAgent-worded equivalent with ~/Library/Logs/Dormouse Server as the second path — the space is quoted correctly and the command runs as printed, leaving ~/Library/Logs and nothing of dormouse's.

Baselines, same fixture. The original bug, using the manage generated by origin/main at 01d1328c:

root after uninstall:  config logs state
does the advertised next command exist?
  bin/manage: GONE -> 'manage purge' is unreachable

And the logs bug, using the manage generated by this branch at 3e132752:

  rm -rf ".../.local/share/dormouse-server"       # the whole printed command

survivors: ~/.local/state/dormouse-server/logs/server.{out,err}.log

Negative cases, both platforms: purge with bin/run-server still present (a live install) prints purged. and stops there; and on a DORMOUSE_INSTALL_ROOT-shaped fixture, where the logs are inside the root, the two printed paths collapse and rm -rf handles the duplicate.

Windows — parse check plus a unit test of the changed branch

The generated manage.ps1 needs WindowsIdentity at load, so it cannot run end to end on a Linux runner. What I could do:

  • [Parser]::ParseFile on install-windows.ps1 and on the generated manage.ps1 body — both clean.
  • Extracted Invoke-Purge and ran it under pwsh with Remove-Tree / Read-Host stubs, with the one bin\run-server.ps1 separator normalized for the Linux filesystem. Both branches behave: present → purged. and nothing more; absent → the teardown line with the root interpolated.

Not verified on Windows: that Invoke-Uninstall's [IO.File]::Delete($runServer) succeeds while the task is stopped, the cmd.exe handle question above, and that the & exit guard in manage.cmd is still doing nothing harmful now that its stated cause is gone (see "Not included"). deploy-lint remains the only automated signal for this file, as docs/specs/server.md says.

Lints: spec-lint (24 specs), deploy-lint (27 checks) and its selftest, loopback-lint, xterm-lint all pass; bash -n clean on both shell installers and on both generated manage scripts.

Not included

Removing the trailing & exit from manage.cmd. Its rem block justified it with "manage uninstall deletes bin -- this very file", which this PR made false; the comment is rewritten to say so, but the & exit stays. Nothing in manage removes that file anymore, so the guard is unreachable — but the failure it prevents is manage uninstall exiting 1 and looking broken, and I can't run a batch file from a Linux runner to confirm the removal is safe. That trade wants a Windows box, not an argument; happy to drop it if you'd rather.

A deploy-lint rule pinning the banner to the deletion would make this regression-proof, and #473 raises it. Left out because that lint is scoped to the FAIL IF clauses in SECURITY.md ("Credentials at rest", "Network posture") and its header says so; a teardown-ordering rule would widen its charter. Worth doing if you want it there anyway — say the word.

Specs

docs/specs/server.md gained the teardown ordering as an invariant next to the manage surface listing, since the reason uninstall spares bin is not visible from the code — including which paths the printed teardown command has to name on each platform, and why. SELF_HOST.md checkpoint 5 states the order explicitly, as #473 asked.

manage uninstall printed "use manage purge to delete those irreversibly"
and then deleted $ROOT/bin, which is where manage itself lives. The command
the banner pointed at no longer existed, so config/ and state/ could only be
removed by hand or by reinstalling purely to get bin/manage back.

Uninstall now removes bin/run-server rather than the whole bin directory, and
says where manage still is. Purge, when bin/run-server is already gone, closes
by printing the one command that clears what remains -- it cannot delete
itself out from under the shell running it.

Closes #473
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: ed8a59e
Status: ✅  Deploy successful!
Preview URL: https://6dd515e9.mouseterm.pages.dev
Branch Preview URL: https://fix-issue-473.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two findings, one of them the same class of bug this PR fixes.

purge's closing banner names a command that doesn't finish the job on Linux or macOS. It prints "this script is all that remains" and offers rm -rf "$ROOT", but on a real install the logs live outside $ROOT: the generated manage sets LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dormouse-server/logs" on Linux and LOG_DIR="$HOME/Library/Logs/Dormouse Server" on macOS, while ROOT is ~/.local/share/dormouse-server / the install root. server.out.log and server.err.log survive the command the operator was told would clear everything — which is the shape of #473 itself, one directory over. Windows is unaffected: $LogDir = Join-Path $Root 'logs' there.

The verification in the PR body couldn't have caught it. Both manage scripts carry [ -d "$ROOT/logs" ] && LOG_DIR="$ROOT/logs" so a DORMOUSE_INSTALL_ROOT fixture — which is what the transcripts show — puts logs inside the root and makes the banner true. Worth re-running the fixture with the logs directory placed at the real path before calling this verified.

Inline suggestions below on both. If you'd rather keep the printed command to a single path, the alternative is to have uninstall (or purge) delete $LOG_DIR outright — logs aren't in the "preserved data" set the way config/ and state/ are, so there's no reason they have to outlive purge at all. Either way the prose needs the same edit: SELF_HOST.md says the command "clears whatever is left in the install root" and docs/specs/server.md says it "removes what is left", and both are only true today by the qualifier the second one omits.

The manage.cmd header comment in install-windows.ps1 is now false. In the $manageCmd here-string, the rem block justifying the trailing & exit says "manage uninstall" deletes bin -- this very file -- so that read fails. After this change uninstall deletes only bin\run-server.ps1, so nothing in manage ever removes manage.cmd and the documented failure mode is unreachable. That comment is outside the diff, so no inline suggestion — but it's the one place in the tree still asserting the behavior this PR removed, and it's load-bearing prose: it's the reason & exit is there at all, with a stated tradeoff (calling the .cmd from another batch script ends that script too) that now buys nothing.

Smaller: the new docs/specs/server.md paragraph lists what uninstall removes as "the releases, the pointers and bin/run-server" — run/ goes too, on all three platforms.

Everything else checks out. bin/ holds only run-server and manage (plus manage.cmd on Windows), so narrowing the delete strands nothing; cmd_rollback's [ -L "$ROOT/previous" ] guard and env_value's || return 1 mean the commands newly reachable on a torn-down install fail cleanly rather than under set -e; and the bin/run-server-absent gate keeps purge on a live install unchanged.

Comment thread deploy/local/install-linux.sh Outdated
Comment thread deploy/local/install-macos.sh Outdated
Comment thread docs/specs/server.md Outdated
purge's closing banner offered `rm -rf "$ROOT"` and called it everything
that remains, but on a real install the logs sit outside $ROOT:
$XDG_STATE_HOME/dormouse-server on Linux, ~/Library/Logs/Dormouse Server
on macOS. server.out.log and server.err.log survived the command the
operator was told would clear the install -- the same shape as the bug
this branch fixes, one directory over. Both banners now name the
dormouse-owned log directory alongside the root. Windows is unaffected:
$LogDir is inside $Root there.

The earlier verification could not have caught it. Both manage scripts
carry `[ -d "$ROOT/logs" ] && LOG_DIR="$ROOT/logs"`, so a
DORMOUSE_INSTALL_ROOT-shaped fixture puts the logs inside the root and
makes the old banner true. Re-ran the fixture with the logs at their real
paths: at the previous head the two log files survive the printed
command; here nothing dormouse-owned does, on either platform.

Linux names LOG_ROOT (the dormouse-server directory) rather than its
logs/ child, so the delete leaves no empty directory behind. On macOS
LOG_DIR is already that level.

Also: the manage.cmd header comment justified its trailing `& exit` with
"manage uninstall deletes bin -- this very file", which this branch made
false. Rewritten to say what is true now -- nothing in manage removes the
file, and the guard is kept so the exit code does not depend on that
staying so. And the spec's list of what uninstall removes omitted run/.
@dormouse-bot

Copy link
Copy Markdown
Collaborator Author

Both findings were real; fixed in ed8a59e, along with the run/ omission. Replies on the three inline threads.

The logs finding is the more serious one, and the review is right that my verification was structurally incapable of catching it. [ -d "$ROOT/logs" ] && LOG_DIR="$ROOT/logs" means any DORMOUSE_INSTALL_ROOT-shaped fixture puts the logs inside the root, which is exactly what makes the old one-path banner true — so the transcripts in the PR body proved a property the real install doesn't have. Re-ran with the logs at their real paths ($HOME pointed at a throwaway tree, no $ROOT/logs): at the previous head server.out.log and server.err.log survive the printed command; at this head nothing dormouse-owned does.

One deviation from the suggestion: on Linux the printed path is now LOG_ROOT — the dormouse-server directory — not its logs/ child, because deleting only the child leaves an empty directory standing and puts the banner back in the same position one level down. That directory is dormouse's alone (the only XDG_STATE_HOME use in the file). macOS needed no equivalent since ~/Library/Logs/Dormouse Server is already that level.

On & exit: the comment was false and is rewritten, but I kept the & exit itself. Nothing in manage removes manage.cmd anymore, so the guard is provably unreachable today — the argument for deleting it is sound. What stopped me is that I can't run a batch file from this runner, and the failure it prevents is manage uninstall exiting 1 and looking broken, which is a worse outward regression than the tradeoff of keeping it (calling the .cmd from another batch script ends that script). Say the word and I'll drop it in a one-line follow-up; it's the kind of change that wants a Windows box more than an argument.

Verification — real-path logs, both platforms, plus the negatives

Method: extract the generated manage from each installer, build a fixture with HOME pointed at a throwaway tree, ROOT=$HOME/.local/share/dormouse-server, and the logs at the platform's real location (not $ROOT/logs, which is what the earlier transcripts used). Run uninstall on a pty, then purge, then execute the command purge printed, verbatim, and list what survives.

Linux, this head:

  rm -rf ".../home/.local/share/dormouse-server" ".../home/.local/state/dormouse-server"

survivors under $HOME (files, symlinks and dirs):
  ~/.local  ~/.local/share  ~/.local/state

Only the XDG directories, which must survive. macOS, same fixture:

  rm -rf ".../home/.local/share/dormouse-server" ".../home/Library/Logs/Dormouse Server"

survivors: ~/.local  ~/.local/share  ~/Library  ~/Library/Logs

The space in the log directory name is quoted correctly and the command runs as printed.

Baseline at 3e13275 (the previous head), identical fixture:

  rm -rf ".../home/.local/share/dormouse-server"

survivors:
  ~/.local/state/dormouse-server/logs/server.err.log
  ~/.local/state/dormouse-server/logs/server.out.log

Negatives, both platforms:

  • Nested-logs fixture ($ROOT/logs present, i.e. the DORMOUSE_INSTALL_ROOT shape): the two paths collapse, rm -rf handles the duplicate, root gone.
  • purge on a live install (bin/run-server present): prints purged. and stops — unchanged.

Windows: $LogDir = Join-Path $Root 'logs' (install-windows.ps1#L1009), so Remove-Item -Recurse -Force $Root already clears them — no change to Invoke-Purge. [Parser]::ParseFile clean on install-windows.ps1, on the generated manage.ps1 body, and the $manageCmd here-string renders with the rewritten rem block intact.

Lints: spec-lint (24 specs), deploy-lint (27 checks) and its selftest, loopback-lint, xterm-lint all pass; bash -n clean on both shell installers and both generated manage scripts.

Docs

docs/specs/server.md gained run/ to the uninstall list and the log-directory qualifier on "removes what is left" — that sentence was true only under the omission the review named. SELF_HOST.md checkpoint 5 said the command "clears whatever is left in the install root"; it now names the log directory too.

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three findings are fixed, and I re-derived each from the code rather than taking the reply at its word. On Linux LOG_ROOT is $XDG_STATE_HOME/dormouse-server, and that directory holds nothing but logs/ — the installer's only mkdir under XDG_STATE_HOME — so naming the parent is right and leaves ~/.local/state standing. On macOS $LOG_DIR is ~/Library/Logs/Dormouse Server, quoted correctly for the space, leaving ~/Library/Logs. Windows really is unaffected: $LogDir = Join-Path $Root 'logs'. run/ is in the spec's uninstall list and matches all three implementations. Threads resolved.

One observation, follow-up material rather than a blocker: nothing executes uninstall or purge. The Linux installer job in .github/workflows/ci.yml installs twice and then stops at bash -n "$root/bin/manage" — the generated script is parsed, never run — and deploy-lint.mjs is textual by design. So both the bug this PR fixes and the banner bug the last review caught sit in the one part of the tree with no executable coverage, which is why two rounds of hand-built fixtures were the only signal available.

Worth naming what a CI step would and wouldn't buy, because the obvious version is weaker than it looks. Running uninstall then purge against the existing test root would catch #473's shape directly — purge unreachable because bin is gone. It would not have caught the log-path bug: DORMOUSE_INSTALL_ROOT forces LOG_DIR="$INSTALL_ROOT/logs", so the generated manage takes the LOG_ROOT="$ROOT" branch and the banner's two paths collapse into one — the same blind spot the original transcripts had. Covering that half needs a fixture with HOME redirected and no $ROOT/logs. And cmd_uninstall refuses when stdin is not a terminal, so the step needs a pty (script -q -c ...), not a printf 'y\n' | pipe.

The change itself is correct as it stands.

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