Skip to content

refactor(uninstall): shared elevation gate + privilege-UX fixes#546

Merged
githubrobbi merged 7 commits into
mainfrom
refactor/uninstall-shared-elevation-gate
Jul 9, 2026
Merged

refactor(uninstall): shared elevation gate + privilege-UX fixes#546
githubrobbi merged 7 commits into
mainfrom
refactor/uninstall-shared-elevation-gate

Conversation

@githubrobbi

Copy link
Copy Markdown
Collaborator

Migrates uffs --uninstall onto the shared elevation gate and fixes a batch of privilege-UX rough edges surfaced during Windows testing. All commits Windows-validated by the maintainer.

Commits

  • refactor(uninstall): adopt the shared elevation gateRemovalPlan implements ElevatablePlan; uninstall drops its duplicated ElevationChoice/gate/platform_elevation_choice and delegates to commands::elevation. The shared gate gains offer_inflow_elevation (uninstall keeps its Windows 3-way one-UAC path; update stays binary).
  • fix(uninstall): deep-sweep daemon reload stops cooperatively — reload now stops the daemon via the cooperative IPC shutdown (works on an elevated daemon from a non-elevated shell) instead of the policy-gated daemon kill, so the reload reaches the elevated start and delivers the promised UAC.
  • fix(broker): --uninstall no-ops when the service is absent — treats ERROR_SERVICE_DOES_NOT_EXIST (1060) as success, and checks existence (a non-elevated query) before the Administrator gate, so removing a non-existent broker is a clean no-op on either terminal.
  • feat(uninstall): spinner during the deep-sweep daemon reload — a cold-cache daemon start (~90 s) now animates a spinner instead of raw connect-retry chatter; the spinner is extracted to a shared commands::spinner (winget's private copy removed).
  • fix(spinner): erase the full drawn line — clears label + 11 columns so a long label no longer leaves a stale tail.
  • fix(uninstall): report legacy-GUI exclusions in the -v sweep diagnostic — the sweep count now reads found N → M legacy-GUI excluded → K stray, so the drop from the name-filter count to the final list is visible.

Validation

Windows-tested end-to-end across multiple rounds (elevation gate 3-way, deep-sweep reload + UAC, broker no-op both terminals, winget/daemon update corners). native + x86_64-pc-windows-msvc clippy + rustdoc clean; 147 uffs-cli tests pass.

githubrobbi and others added 7 commits July 8, 2026 12:55
Migrate `uffs --uninstall` onto the shared `commands::elevation` gate that
`uffs --update` already uses, so both flows share one implementation of
"surface admin-only work up front, decide once". Behavior-preserving.

- Extend the shared gate with `offer_inflow_elevation`: uninstall passes
  `true` (it has a one-shot elevated helper → Windows 3-way elevate/continue/
  abort, `action = "removal"`); update passes `false` (no in-flow UAC → binary
  continue/abort). `GateWording` regains `action`; `ElevationChoice` regains
  `ElevateLater` (was uninstall's `ElevateAtRemoval`).
- `RemovalPlan` now implements `ElevatablePlan` (its inherent
  `requires_elevation` / `drop_elevation_required` moved into the trait impl to
  satisfy `same_name_method`; `render_elevation_needed` delegates to
  `render::print_elevation_gate`). Call sites import the trait.
- Delete uninstall's local `ElevationChoice` + `elevation_gate` +
  `platform_elevation_choice` (both cfg); the thin wrapper delegates to the
  shared gate. uninstall/mod.rs drops ~90 lines.

147 uffs-cli tests green (incl. the 12 uninstall plan tests on the now-trait
methods); native + windows-msvc clippy + rustdoc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed kill

The deep-sweep (`d`) reload shelled the policy-gated `daemon kill` to stop the
running daemon before restarting it with full drive coverage. A non-elevated
shell can't use that on an elevated daemon, so it failed loudly ("requires an
elevated shell"), bailed before the elevated start, and the UAC prompt the `d`
prompt promised never appeared.

Stop the daemon COOPERATIVELY first — an IPC shutdown the daemon honors
regardless of its own elevation (same-user pipe), the same stop
`winget::quiesce` uses — and fall back to the gated kill only if the daemon
ignores it. Then the reload reaches the elevated start and delivers the
promised UAC.

- New `stop_running_daemon` (cooperative shutdown -> poll down -> gated-kill
  fallback -> re-check) + `daemon_is_down`; `wait_until_daemon_down` shares the
  down-check.
- Narration "kill + start" -> "stop + start".

xwin (compiles this #[cfg(windows)] path) + native clippy + rustdoc clean;
147 uffs-cli tests pass.
`uffs-broker --uninstall` bailed on any `sc delete` failure, so removing a
broker that was never installed screamed "[SC] OpenService FAILED 1060: The
specified service does not exist" — even though "not installed" is exactly the
requested end state.

Treat ERROR_SERVICE_DOES_NOT_EXIST (1060) as success: the request is satisfied
by doing nothing. New pure `absent_service_signal` classifier (exit code
primary, sc text fallback) + Windows unit tests.

native + windows-msvc clippy + rustdoc clean.
…he spinner

The deep-sweep (`d`) reload starts the daemon, which on a cold cache can take
~90s. In loud mode that showed only raw "connect attempt N/20" chatter, which
reads as a hang. Animate a spinner over the (quiet) start instead so the wait
reads as "working".

Extract the spinner into a shared `commands::spinner` (Windows-only for now,
since both callers are `#[cfg(windows)]`) and point both the winget
orchestration and the new coverage reload at it, removing winget's private
copy (behavior-identical output).

native + windows-msvc clippy + rustdoc clean; 147 uffs-cli tests pass.
Follow-up to the 1060 no-op: the existence check now runs BEFORE the
Administrator gate. A non-elevated `uffs-broker --uninstall` with no broker
installed prints "Broker service is not installed — nothing to remove." and
exits 0, instead of demanding an elevated terminal for work that would not
happen. Elevation is still required only when there is actually a service to
delete. Existence is a non-elevated SCM query (`uffs_winsvc::is_installed`).

native + windows-msvc clippy clean.
A label longer than 60 chars (the deep-sweep "starting the index daemon (a
cold cache can take up to ~90 s)") left a stale tail ("90 s)…") on screen after
the spinner finished, because the erase wiped a fixed 60 columns. Clear
`label.chars().count() + 11` columns — exactly the "  <glyph> <label>…      "
line drawn each frame.

native + windows-msvc clippy + rustdoc clean; 147 uffs-cli tests pass.
The per-pattern "kept=N" line prints after the name/known-dir filter but BEFORE
the legacy-GUI exclusion (which happens in version_strays), so the count
overstated what finally showed — e.g. kept=21 uffs.exe but only 3 in EXTRA,
because 18 were the predecessor C++ GUI product. Count those exclusions and
emit a -v line (mirroring the timeout counter), so the math reads
found N candidate -> M legacy-GUI excluded -> versioned K stray.

native + windows-msvc clippy clean; 147 uffs-cli tests pass.
@githubrobbi githubrobbi enabled auto-merge July 9, 2026 01:18
@githubrobbi githubrobbi added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit 0b2bd84 Jul 9, 2026
21 checks passed
@githubrobbi githubrobbi deleted the refactor/uninstall-shared-elevation-gate branch July 9, 2026 01:33
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