Skip to content

fix(updater): actually check for updates on Program Files installs - #198

Open
mrjeeves wants to merge 2 commits into
mainfrom
claude/allmystuff-cecsupport-auto-updates-r6rjxi
Open

fix(updater): actually check for updates on Program Files installs#198
mrjeeves wants to merge 2 commits into
mainfrom
claude/allmystuff-cecsupport-auto-updates-r6rjxi

Conversation

@mrjeeves

Copy link
Copy Markdown
Owner

Why

Auto-update looked like it never ran. The wiring was actually fine — the GUI spawns tick_forever (gui/src-tauri/src/main.rs), the node spawns tick_forever_unattended (node/src/bin/serve.rs) — but on the most common Windows install it never reached the network, and on the installs where it did work it had no way to tell anyone.

Three independent causes, each enough on its own to make the feature look absent.

What was wrong

1. Program Files was treated as a package manager. detect_install_kind_from_path flagged any Windows path containing \program files\ as PackageManager, and check_now returned on that before the fetch. Our own MSI installs exactly there — release.yml runs tauri-action with "targets": "all" and no bundle.windows config, so both the per-user NSIS setup and the per-machine MSI ship. Every machine that took the MSI had self-update silently switched off, the check included.

There was already an internal contradiction: os_bundle_paths_are_left_alone asserts C:\Program Files\AllMyStuff\allmystuff-gui.exe is safe to swap, while the check path refused to even look at it. The Windows branch was also #[cfg(target_os = "windows")], so no test on CI ever exercised it.

2. A managed install checked nothing at all. Returning before the fetch meant such an install never contacted the release feed, so it could not report a new release even though it could perfectly well have done so.

3. Every non-staging outcome was swallowed. run_attended_check matched Ok(_) => {}, so Disabled, PackageManager, NotDue, UpToDate and PolicyBlocked logged nothing at any level. A ticker that was silently disabled and one running fine and finding nothing produced identical output: none. That is the hardest part of "it never checks" to diagnose.

What changed

  • Install-kind detection — Program Files is no longer a package-manager signal; Chocolatey (\chocolatey\lib\) and Scoop (\scoop\apps\) still are, as is Homebrew and /usr/bin. Whether an install can be written is now asked at runtime with a create/delete write probe rather than guessed from the path — the only reliable test on Windows, where ACLs make a permissions read meaningless, and exactly what the swap itself will attempt. This keeps a genuinely unwritable per-machine install from staging downloads it can never apply and retrying forever.
  • New CheckOutcome::ManualUpdateAvailable — a package-managed or unwritable install now runs the check and reports that a release exists. It can't install it; it can say so.
  • Every outcome is logged, via a new log_check_outcome.
  • stamp_check_now() moved after a successful fetch. It previously ran before fetch_release, so one offline moment burned the full 24h cooldown.
  • A notification path. tick_forever held no AppHandle, so it structurally could not talk to the UI — a staged update sat on disk until someone happened to open Settings → Updates. New tick_forever_notify takes a callback; the GUI passes one that emits update://checked, and the store surfaces staged / manual / policy-blocked results as a toast from anywhere in the app.
  • "Check now" is no longer disabled on package-managed installs, since checking works there now. The notice text explains that it still checks but installs through whatever owns the files.

Testing

cargo test -p allmystuff-updater — 22 pass, including two new ones covering the Windows classification (previously untested) and the write probe. cargo clippy -p allmystuff-updater --all-targets -- -D warnings and cargo fmt --check clean. pnpm check clean (155 files, 0 errors). cargo check -p allmystuff-cli passes — no consumer matches exhaustively on CheckOutcome, so the new variant is additive.

The Tauri GUI crate could not be compiled in this environment (needs GTK/WebKit system libraries); that edit follows the app.handle().clone() pattern already used a few lines above it in the same closure, and CI covers it.

Note

gui/src-tauri/Cargo.lock picks up an unrelated 0.2.480.2.49 sync of local crate versions that the release commit missed. It regenerates on any build; no third-party dependency changed.


Generated by Claude Code

claude added 2 commits July 29, 2026 07:14
The background ticker was wired up correctly — the GUI spawns
`tick_forever`, the node spawns `tick_forever_unattended` — but on the
most common Windows install it never reached the network, and when it did
work it had no way to tell anyone.

Three separate causes, all of which made auto-update look absent:

1. `detect_install_kind` flagged ANY path under `C:\Program Files\` as
   package-managed, and `check_now` bailed on that before the fetch. Our
   own MSI installs there (release.yml builds `targets: "all"`, so both
   the NSIS setup and the per-machine MSI ship), so every MSI install had
   self-update silently switched off — the check included. Program Files
   is no longer a package-manager signal; Chocolatey and Scoop still are.
   Whether the install can be written is now asked at runtime with a
   write probe rather than guessed from the path, which is the only
   reliable test on Windows where ACLs make a permissions read
   meaningless.

2. A package-managed or unwritable install now still *checks*, reporting
   the new `ManualUpdateAvailable` outcome instead of returning before
   the fetch. It can't install the update, but it can say one exists.

3. Every non-staging outcome was swallowed by a bare `Ok(_) => {}`, so a
   ticker that was disabled and a ticker that was running fine and
   finding nothing produced identical output: none, at any log level.
   Each outcome is now logged.

Also: `stamp_check_now()` ran before `fetch_release`, so a failed check
burned the full 24h cooldown. It now stamps only after a successful
fetch.

Finally, the ticker held no handle to the UI, so a staged update sat on
disk until someone thought to open Settings -> Updates. `tick_forever_notify`
takes a callback; the GUI passes one that emits `update://checked`, and
the store surfaces it as a toast from anywhere in the app. "Check now" is
no longer disabled on package-managed installs, since checking works there.

Adds tests for the Windows classification (the `#[cfg(target_os =
"windows")]` branch had none) and the write probe.

gui/src-tauri/Cargo.lock picks up an unrelated 0.2.48 -> 0.2.49 sync the
release commit missed; it regenerates on any build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMPNLjvNYQCeR3xGfdDUCg
Every *file* layer in `init_logging` already sets `with_ansi(false)`; the
stdout layer was left on tracing-subscriber's default, which is
unconditionally ON — it does not check for a TTY.

Under systemd that stdout is captured by journald/rsyslog, so each line
reaches /var/log/syslog wrapped in escape sequences:

  ubuntu allmystuff-serve[793]: #33[2m2026-07-29T09:12:59Z#033[0m #33[32m INFO#033[0m …

That is tens of wasted bytes on every line of a daemon that logs
continuously, and it breaks grep against what you can actually read.

Colour is now gated on `std::io::stdout().is_terminal()`, so a terminal
still gets colour and a service gets plain text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMPNLjvNYQCeR3xGfdDUCg
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.

2 participants