Skip to content

Seamless upgrades: hand the running core over instead of stopping it; pin core v0.82.0 - #368

Merged
fylorn merged 4 commits into
devfrom
feat/handover-ui
Oct 24, 2026
Merged

fylorn merged 4 commits into
devfrom
feat/handover-ui

Conversation

@fylorn

@fylorn fylorn commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Updates no longer cut the requests in flight. Instead of waiting for them and stopping core, the app asks the running core to wait for its successor and restarts. The new core takes over the same listening sockets, and the old one finishes its requests and exits (core 0.82.0, ThinkWatchProject/ThinkWatch-Core#370).

Changes

  • In-app update (DMG, Windows installer, portable, AppImage): download, replace, POST /handover/arm, restart.
    • If the core refuses or is older than protocol 52, the previous flow runs: wait for requests, stop core, restart.
    • The exit hook does not stop an armed core.
  • Windows installer: the running twcore.exe is renamed before the installer runs. Leftovers are removed at the next start.
  • Linux AppImage: core runs from a per-version copy in the data directory. The AppImage mount goes away when the old app exits, and the old core keeps draining after that.
  • Restart Gateway: hands over to a fresh core of the same version instead of stopping the running one.
  • Homebrew upgrades:
    • A quit Apple Event sent by osascript on behalf of brew upgrade or brew reinstall is recognised and handled like an in-app update. Any other quit stops core as before.
    • When the bundle is replaced without quitting the app (--no-quit, SSH, automation permission denied), the app notices the new version on disk and restarts itself the same way.
  • Window after an update: comes back as it was before the update. An app that lived only in the menu bar does not open its window. .last-exit records whether the exit was armed.
  • Requests in flight: the overview and the menu bar count the requests the previous core is still finishing (Status.draining).
  • Safe-mode page: when the new version cannot read the configuration, the page says the gateway keeps forwarding with the previous configuration and switches over once the file is fixed.
  • Translations and pin: Chinese translations for the new control.handover.* and gw.oauth.held codes; pinned to core v0.82.0. The screenshot oracle is regenerated.

Testing

  • Rust: cargo fmt --check, cargo clippy --all-targets -D warnings and cargo test --workspace pass.
  • Frontend: tsc passes, and vitest passes (1301 tests).
  • Unit tests cover the quit-source decision table, the window-restore rule, the update fallback and the Windows rename.
  • Not yet run on real machines: a full brew upgrade (including --no-quit and a failed upgrade), the Windows installer after the rename, and an AppImage update.

🤖 Generated with Claude Code

fylorn and others added 4 commits October 24, 2026 13:06
The desktop side of the core handover (control API 52). After an update
is in place, the app asks its core to wait for a successor
(POST /handover/arm) and restarts at once; the new version's core takes
over the listeners and the old one finishes its requests. When the core
cannot be armed (older core, login in progress, safe mode), the update
falls back to the previous flow: wait for requests, stop, restart.

- Updates: arm after replacing on macOS, Linux AppImage and Windows
  (installer and portable); the exit hook leaves an armed core running.
  The Windows installer path renames the running twcore.exe to
  twcore.old-<pid>.exe before the installer runs and sweeps leftovers on
  start; a failed install renames it back and adopts the armed core.
- Linux AppImage runs core from a per-version copy in the data
  directory (current and previous kept), so it outlives the mount.
- Restart gateway hands over to a same-version core; the supervisor lets
  go of the armed child, and readiness now checks that the answering
  core is the one it started (a takeover in progress counts as starting).
- Homebrew: a quit Apple Event sent by osascript whose parent is this
  Homebrew's Ruby running brew.rb upgrade/reinstall arms instead of
  stopping core; logout/shutdown and every other sender keep today's
  behaviour. A replaced Info.plist (brew --no-quit etc.) triggers an
  armed restart.
- .last-exit records whether the exit was armed; an armed exit restores
  the window state within five minutes even when the version is
  unchanged.
- In-progress counts on the overview and in the menu bar include the
  draining core's requests; the safe-mode page says the gateway keeps
  the previous configuration while a takeover waits for a fixed config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ig, translate the handover codes

When the updated core cannot read the configuration it now takes over only the
control plane and finishes the takeover by itself once the file is fixed. The
safe-mode page says the gateway keeps forwarding on the previous configuration
instead of claiming it is not running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a flush on a read-only handle)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 1c0d9bb into dev Oct 24, 2026
6 checks passed
@fylorn
fylorn deleted the feat/handover-ui branch October 24, 2026 08:18
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant