Updates and rollback

How the desktop app finds, downloads, retries and installs an update, and how rollback works.

The desktop app updates itself from a feed. Updates are for the packaged app only (setupUpdater returns at once when it is not packaged, desktop/main.js). Windows behaviour is from code only; the update code is the same file for both systems except the AppImage steps below, which are Linux.

01settings#

The setting lives in desktop.json in the shell data folder, as {"update": {...}}. Nothing inventories desktop.json as fields, so its update keys are listed here.

key meaning default
update.url the update feed's base url; only http or https is kept, a trailing / is dropped (cleanUpdateSetting) https://smith.boxsie.co.uk/updates (DEFAULT_UPDATE_URL)
update.channel stable or dev; anything else is stable (CHANNELS; line 37) stable
update.skip one version the updater does not fetch; set by a rollback (see below); kept only if it looks like 1.2.3 or 1.2.3-rc.1 unset

The feed a channel reads is <url>/latest for stable and <url>/dev for dev (feedUrl). With no url the updater does not start and logs it (desktop/main.js). The shell data folder is in where smith keeps its files.

02when it checks#

It checks at start and then on a timer chain (scheduleCheck in desktop/main.js), each wait chosen by checkIntervalMs (update-logic.js): 15 min while the state is ready, 4 h otherwise (CHECK_READY_MS, CHECK_IDLE_MS). The chain is re-armed when a download lands, so the 15 min takes effect at once. Every caller shares one check, check() in desktop/update-download.js: the timer, the desk's check_update event, the tray's "check for updates", the settings button and the restart press. A call while a check runs gets the running one. The desk's event is still dropped if one ran within the last minute (CHECK_UPDATE_GAP_MS, update-logic.js; checkFromCore, main.js). The tray and settings button are never throttled (checkNow, main.js). Each check records last_check_at.

A found update is downloaded in the background unless it matches the skip (the update-available handler in main.js). If that version is already downloading, or is already downloaded and its file is still there, nothing starts again. A newer version cancels the running download and starts its own. The updater is set up by configureUpdater (update-download.js): the generic feed, autoDownload off, web installer off, and differential downloads off, so every download is the whole file. The download is checked by electron-updater against the feed's sha512. A downloaded update is also applied when the app quits (autoInstallOnAppQuit).

A download that makes no progress for 60 s is cancelled and tried again, 3 attempts in all, waiting 30 s and then 120 s between them. After the third the state is failed with the error text (no progress for 60s for a stall). retry() starts it again from failed. Progress (percent, transferred, total, bytes_per_second) comes from electron-updater's download-progress event.

03restart now#

"Restart now" never restarts without a press. It waits while any run, chat or sync is mid-work, asking the core's /shell/busy (desktop/main.js). A press first checks the feed (press in update-download.js): if a newer release than the downloaded one exists, it starts that download and returns downloading without installing; if the check fails or finds nothing newer, it goes on as below. The decision is installDecision (update-logic.js): it installs only when no core runs, or the core says mid_work is false with no chats, runs or syncs listed. If asking the core fails, it waits; it never guesses that it is safe. While waiting it looks again every 30 s (UPDATE_RECHECK_MS, main.js), and logs still mid-work; waiting only when the set it waits for changes. When the work clears it asks again, and only a second press installs.

Installing keeps the previous AppImage, takes a snapshot, stops the core and then restarts into the new version (installNow, main.js).

04the header button#

The update state has seven values (updateStep, update-logic.js; held in the flow, update-flow.js). The header button draws only ready, waiting and installing; it draws nothing for none, checking, downloading or failed.

state meaning
none no update is downloaded
checking a check is running (entered from none or failed only)
downloading a download is running; carries percent, transferred, total, bytes_per_second and attempt
failed the download failed after 3 attempts; carries error and attempt
ready an update is downloaded; press to restart now
waiting pressed while work is mid-work; lists the runs and chats being waited for. Cancel goes back to ready (cancelUpdateWait, main.js).
installing the install is under way

If the downloaded file is gone or replaced, the state goes to none or downloading, never ready or waiting (guardPendingInstall, main.js). The state is sent to the page and posted to the core's /shell/update-state so the update card can use it, with the fields above and last_check_at (RFC3339) when set (setUpdateState, postUpdateState, main.js). The core keeps these states and fields; any other state becomes none (internal/desk/update_card.go).

05cards#

The conductor can draft an update card, propose_update, "update smith to v", for a downloaded update. It only drafts the card; only the operator's click in the window restarts the app, waiting for running work first. It is refused when no update is ready. propose_restart proposes restarting smith so start-only settings load; the click is refused while a run is live, since a restart would pause it (; route).

06the tickets snapshot before an update#

Before an update is applied the shell asks the core for a snapshot with POST /shell/before-update (beforeUpdate, desktop/main.js). The core records that it is about to restart for the update, and, if a tickets db snapshot runner exists, takes a snapshot (shellBeforeUpdate; BeforeUpdate). Quitting with an update waiting does the same (the before-quit handler in main.js). A failure is logged and never touches data. Where those snapshots go is in backup and restore.

07smith-previous and rollback#

When an update is downloaded, the running AppImage is copied beside itself as smith-previous.AppImage, once per update (PREVIOUS; keepPlan; keepPrevious, main.js). This is the Linux AppImage path; it needs APPIMAGE set, and nothing is kept when the app is itself the previous copy.

A rollback puts the previous AppImage back in the current one's place and starts again (rollback, main.js; rollbackPlan). It goes through the same busy gate as restart now, and waits every 30 s while work is mid-work. If there is no previous version it throws "there is no previous version to go back to" (main.js).

08skip#

A rollback saves the version it went back from as update.skip, so the updater does not fetch it again (rollbackSetting; doRollback, main.js). The skip matches one version exactly; a newer one is still taken (skipsVersion). At start the skip is cleared once the running app is that version or newer, which means it was installed (clearSkipOnInstall; setupUpdater, main.js).

09version mismatch#

When a core answers, the shell compares its version and commit with the app's (versionMatch; checkCoreVersion, main.js). A dev build has no expected commit and always matches. On a mismatch it logs version mismatch: the core is <version> (<commit>), the app is <version> (<commit>) and keeps the text for settings.

10the changelog tool#

changelog is a read-only desk tool. It answers from the changelog built into the install: the release it runs, or with since (a version like 0.2.12) every release after it. limit caps the change rows: default 50, at most 200 (changelogInput).

Releases newer than the install come from the update channel's feed and carry installed: false. When the feed cannot be reached, latest.error says so and the built-in rows still come back. The answer also has installed (version, commit, channel), releases and truncated. The channel is update.channel in desktop.json, read by UpdateChannel.

11not checked#

The Windows installer side of updates, and how the settings page draws the mismatch text, are not checked.


The release process (building, channels on the server, publishing, pruning and promoting) is in docs/desktop-release.md.