Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

CommandAction
flow updateDownload, verify, stage, and swap in the latest release
flow update --checkPrint 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:

  1. Download the new archive into memory.
  2. Extract the two binaries to a .stage/ directory next to the install dir.
  3. Spawn a detached helper process (the shim) that outlives flow.exe.
  4. flow.exe exits.
  5. 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.
  6. 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 stop runs 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.exe guaranteed absent, the shim only needs to wait for one PID (flow.exe itself) 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 start prints flow: daemon started first, 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_name field 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:

  1. Downloads the sidecar first and parses the 64-hex-character hash out of its GNU-coreutils-style <hash> <filename>\n format.
  2. Downloads the zip into memory and computes SHA256 over the bytes via the sha2 crate.
  3. Compares the computed lowercase-hex digest against the sidecar hash (case-insensitively — Get-FileHash emits uppercase, the Rust digest is lowercase).
  4. Aborts with UpdateError::ShaMismatch if 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 with UpdateError::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-probe temp file) catches Program Files installs that need elevation. The updater reports UpdateError::ReadOnlyDir rather than failing mysteriously halfway through the swap.

Code Path

StepLocation
Public entry pointscheck_for_update, perform_update in src/updater/mod.rs
CLI wiringcmd_update in src/bin/flow.rs
Daemon-running guardtransport::is_daemon_running in src/ipc/transport.rs
Shim script builder + spawnersrc/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)