Self-Update (flow update)
flow update pulls the latest release from GitHub, verifies it, and replaces the
running flow.exe and flowd.exe in place. It is a single command with one
optional flag:
| Command | Action |
|---|---|
flow update | Download, verify, stage, and swap in the latest release |
flow update --check | Print whether an update is available; exit non-zero if so. Safe while the daemon runs. |
The install path is self-located: flow updates whichever copy of itself is
running, via std::env::current_exe().parent(). No registry key, config file,
or PATH entry is consulted. This matches the existing
find_daemon_exe convention — flowd.exe is assumed to
live next to flow.exe, and both are replaced together.
The Windows File-Lock Problem
The central design constraint is that Windows refuses to open a running .exe
for write. While flow.exe is executing, its image file is locked by the
kernel; the same is true for flowd.exe while the daemon is running. A naive
updater that downloads a new binary and overwrites the old one in place fails
with ERROR_SHARING_VIOLATION on the very file it is trying to replace.
There is a narrow escape: Windows does allow a running executable to be renamed (moved within the same volume) but not overwritten or deleted. This is the primitive the updater exploits. The flow is:
- Download the new archive into memory.
- Extract the two binaries to a
.stage/directory next to the install dir. - Spawn a detached helper process (the shim) that outlives
flow.exe. flow.exeexits.- The shim waits for
flow.exe’s PID to disappear, then for each binary:- renames the running file to
<name>.old(rename is allowed), - renames the staged file into place,
- deletes
<name>.old.
- renames the running file to
- The shim removes
.stage/and deletes itself.
By the time the shim renames anything, flow.exe has exited and flowd.exe is
guaranteed absent (see Refuse-if-running below), so the
rename window is contention-free.
sequenceDiagram
participant User
participant flow as flow.exe
participant Stage as .stage/
participant Shim as powershell shim
participant FS as filesystem
User->>flow: flow update
flow->>FS: extract flow.exe + flowd.exe into .stage/
flow->>Shim: spawn detached, pass flow PID
flow->>User: "updated to vX.Y.Z — run flow start"
flow->>flow: process exits
Shim->>FS: wait until flow.exe PID gone
Shim->>FS: rename flow.exe -> flow.exe.old
Shim->>FS: rename .stage\flow.exe -> flow.exe
Shim->>FS: delete flow.exe.old
Shim->>FS: (repeat for flowd.exe)
Shim->>FS: rmdir .stage, delete self
Refuse-if-running
The updater refuses to proceed if transport::is_daemon_running() reports a live
daemon, telling the user to run flow stop first. This is a deliberate UX
choice rather than a technical limitation:
- It avoids a surprise window shuffle —
flow stopruns graceful-shutdown window rescue, which the user should see happen explicitly, not as a side effect of an update. - It simplifies the shim: with
flowd.exeguaranteed absent, the shim only needs to wait for one PID (flow.exeitself) before swapping.
flow update --check is exempt from this guard because it only queries the
GitHub releases API and never touches local files — it is safe to run any time,
including while the daemon is actively tiling.
Start-Time Notification
flow start can optionally check GitHub for a newer release on each launch and
print a one-line notification prompting flow update when one exists. It is
on by default and controlled by the top-level check_for_updates field in
flow.toml (see Config Reference):
check_for_updates = true # set to false to silence the start-time notification
The notification is deliberately non-intrusive:
- Non-blocking — the check runs after the daemon reports ready, so a slow
network can never delay tiling.
flow startprintsflow: daemon startedfirst, then the notification if one applies. - Bounded — the network call is capped by a short timeout, so a dead or
unresponsive network resolves in seconds rather than hanging the command. The
same bound also applies to the metadata fetch in
flow update; only the multi-MB archive download is left unbounded. - Best-effort — every error (offline, parse failure, GitHub outage) is silenced. The daemon always starts regardless of the outcome.
- Print-only — it only echoes
flow: update available (<tag>) — run "flow update" to install; it never downloads or installs anything.
This automatic check reuses check_for_update — the
same call behind flow update --check. That explicit command is unaffected
by the check_for_updates flag: a user who disables the automatic notification
can still ask for a version check on demand.
Version Comparison
Versions come from two sources:
- The running binary’s version:
env!("CARGO_PKG_VERSION")(e.g.0.1.0). - The latest release’s version: the
tag_namefield of the GitHub releases API response (e.g.v0.1.1).
Both are parsed into a (u32, u32, u32) tuple and compared with tuple ordering.
The leading v on the tag is stripped before parsing. This deliberately avoids
pulling in a semver crate — FlowWM uses plain MAJOR.MINOR.PATCH integers with
no pre-release suffixes, so a three-tuple is the exact model. --check returns
“up to date” when the tuples are equal; the install path returns early (a
no-op success) when not strictly newer, so re-running flow update after a
successful update is idempotent.
Integrity Verification
Every release ships a SHA-256 sidecar (<zipname>.sha256) alongside the zip,
produced by Get-FileHash in the release workflow (see
.github/workflows/release.yml). The
updater:
- Downloads the sidecar first and parses the 64-hex-character hash out of its
GNU-coreutils-style
<hash> <filename>\nformat. - Downloads the zip into memory and computes
SHA256over the bytes via thesha2crate. - Compares the computed lowercase-hex digest against the sidecar hash
(case-insensitively —
Get-FileHashemits uppercase, the Rust digest is lowercase). - Aborts with
UpdateError::ShaMismatchif they disagree.
The zip is never extracted or written to disk before the hash check passes.
Edge-Case Guards
Two environment checks run before any download:
- Running-from-zip: if
current_exe()resolves to a path containing\TempN_(Windows’ temporary folder for opened-in-place zip extraction), the updater aborts withUpdateError::RunningFromZip. Updating a binary inside a transient extraction folder would “succeed” but be discarded the moment the user closes the zip viewer. - Read-only install directory: a writability probe (create and delete a
.flow-write-probetemp file) catchesProgram Filesinstalls that need elevation. The updater reportsUpdateError::ReadOnlyDirrather than failing mysteriously halfway through the swap.
Code Path
| Step | Location |
|---|---|
| Public entry points | check_for_update, perform_update in src/updater/mod.rs |
| CLI wiring | cmd_update in src/bin/flow.rs |
| Daemon-running guard | transport::is_daemon_running in src/ipc/transport.rs |
| Shim script builder + spawner | src/updater/shim.rs |
The updater module is a sibling of the other top-level modules under src/
(registered in src/lib.rs). It depends only on
flow_wm::ipc::transport for the daemon-running guard — it does not touch the
layout engine, window registry, or config system.
Cross-References
- IPC – the named-pipe transport used for the
daemon-running guard and for
flow stop - Architecture – subsystem overview
- Roadmap – future work (e.g. delta updates, package-manager integration)