ADR 0050 — Registered dashboard server and supervised restart
- Status: Amended
- Date: 2026-10-24
- Amends:
- Amended by: ADR 0084 — the server’s launchers wait while setup changes the runtime, and setup restarts the server
- Restructured: 2026-10-25
Context
Section titled “Context”A Windows installer update from v0.20.0 to v0.21.0, started from the dashboard, ended with the dialog reporting that the updater had stopped and offering Try again. Try again reported “Already on v0.21.0”. Closing and reopening Imprint showed the new version. The logs in the install’s data/ directory show why:
imprint update(windows-setup) calleduiStopbefore starting the installer.uiStoponly knew a server recorded indata/imprint_ui.json, which onlyimprint ui startwrote. The desktop app starts its own service (imprint.desktop_server), and ADR 0025 decided it does not write that file. Nothing was stopped.- The installer’s Restart Manager found one application using its files (“Imprint memory dashboard”) and shut it down. That was the portable executable’s stub in
bin\, which waits for the Electron app it extracted to a temporary directory. The app itself is not an installed file and kept running. So did its service (pid 40992 inimprint_ui.log), until the user quit the app about a minute after the installer finished. - The installer’s relaunch started a second app instance, which yields to the running one through the single-instance lock and only brings its window forward.
/api/versionresolves the version once per server process, so the old server kept reporting v0.20.0. The update dialog treated the updater’s exit as the end of the update. The windows-setup updater exits as soon as the installer starts, so the dialog said it had stopped.
The flaw is general. Two launchers (the desktop app and imprint ui) start the same server, and only one registered it. The archive and macOS package methods restarted only a server started by imprint ui start. The desktop shell also survives an update in practice with every desktop package: the Windows portable executable and the Linux AppImage run from a temporary copy, and macOS Installer does not quit running apps. So an update must replace the server underneath a running shell.
Decision
Section titled “Decision”The server registers itself. imprint.api.DashboardServer serves the dashboard for every launcher (python -m imprint.api and imprint.desktop_server). Once it is listening it writes data/imprint_ui.json with its pid and port. On shutdown it removes the file if the file still names it. The CLI no longer writes the file. imprint ui stop, status and restart, the already-running check in imprint ui start, CLI commands that enqueue through the API, and imprint update read it. So they act on the running server whoever started it.
A restart is the server exiting for its owner to start it again. POST /api/restart makes the server finish in-flight requests, shut down and exit with status 75 (EX_TEMPFAIL). Whoever started the server starts it again when it exits with that status:
- The desktop app’s
DesktopServicespawns the service again. Any other unexpected exit is still reported on the startup screen. imprint ui(foreground) supervises its server the same way.imprint ui startnow leavesimprint ui serverunning detached, which supervises the server and writes to the UI log.
Ownership does not change: a desktop-owned service stays desktop-owned and Quit still stops it, and a daemon stays a daemon. A server started without one of these launchers (for example uvicorn imprint.api:app, or the container’s python -m imprint.api) just stops; uvicorn imprint.api:app answers 409 because it has no DashboardServer.
imprint ui restart restarts the registered server and waits up to 60 seconds for the replacement to register and answer. When no server runs, it starts nothing and exits successfully. Before this change it ran stop then start.
Every update method restarts the server after installing. Archive and macOS package updates run the installed binary’s imprint ui restart. The Windows installer’s provisioning script, imprint-setup.ps1, ends with imprint ui restart. That covers silent updates, interactive upgrades over a running Imprint and Repair. A failed restart is logged and does not fail setup. The windows-setup updater no longer stops the server before starting the installer, and /RELAUNCH=1 still reopens or focuses Imprint.
The dashboard reloads into the new version. /api/update reports failed when the updater it started exited with an error. Only then does the dialog say the update failed and offer Try again. A successful exit hands over to the installer or the server restart. The dialog polls until /api/version reports the target release, then reloads the page.
Windows background programs get a console without a window. imprint ui start’s supervisor and the dashboard’s updater are started with CREATE_NO_WINDOW instead of DETACHED_PROCESS. Console programs they start (Python under imprint ui serve; setup and imprint ui restart under the updater) inherit that console instead of each opening a window.
Rejected alternatives
Section titled “Rejected alternatives”- The desktop app writes the registration for its child. That makes two writers for one fact and still leaves the app’s service without a restart path. The server is the one thing every launcher shares.
- Stop the registered server and start a daemon in its place (the old
imprint ui restart). If the desktop app also restarted its service, both would race to bind the port, and which one served afterwards would depend on timing. If it did not, the server would change owner after every update, and Quit would leave a daemon behind. - The desktop app restarts its service after any unexpected exit.
imprint ui stopcould then never stop it, and a restart would still race a CLI-started replacement. - The server re-executes itself.
execvkeeps the pid and the lifetime pipe on POSIX, but Windows has noexec. The parent sees an exit there anyway, and a replacement would need hand-built handle and console inheritance. - Close the desktop app for the Windows installer (ask it to quit, or install Electron unpacked so Restart Manager closes it). This would fix only one method: archive overlays and macOS packages also leave the shell running. It changes the release packaging, and it could be adopted later to refresh the shell itself.
- Stop the server before the Windows installer runs. The desktop app would show a startup error for the length of the installation, and starting the service afterwards would need a separate path.
- Reload every open page when the server’s version changes. That would need a version watcher in every page; this change keeps it to the update dialog.
Consequences
Section titled “Consequences”- An update by any method replaces the running dashboard server with the installed code without changing who owns it. The page that started the update reloads itself into the new version. The false “stopped” state and Try again after a Windows installer handover are gone.
- Stopping or restarting waits at most 5 seconds for in-flight requests (
GRACEFUL_SHUTDOWN_SECONDS), then closes the rest. Unbounded, a restart on Windows hung about once in 40 attempts after the v0.22.0 release: when a client resets its socket, CPython 3.12’s proactor transport raises in_call_connection_lostbefore notifying uvicorn, so uvicorn waits for a connection that is already gone. - The desktop shell (Electron’s main process and preload) keeps running the code it started with until the app is quit and reopened. The dashboard pages come from the server and are current.
imprint ui stopalso stops a desktop-owned service; the app then shows “Local service stopped” with Try again.imprint ui statusreports it.- A daemon started by
imprint ui startis now two processes: the Go supervisor and the Python server.imprint ui stopstops the server, and the supervisor exits with it. - When the queue lock is busy, CLI commands that enqueue through the API now also reach a desktop-owned server.
- Releases before this one neither register their server nor implement
/api/restart, and their updater does not run the new restart. After the first update to this release, reopen Imprint (or restart animprint ui startserver) once, as ADR 0023’s transition required. No fallback to the old stop-and-start is kept. - Provisioning on Windows runs while the old server runs, as it already did in practice. If a release changes dependencies, files loaded by running Imprint processes (the dashboard and MCP servers) could still block their replacement. This change does not address that.
- Other open dashboard pages (another browser tab, the Echo companion) reconnect to the restarted server but keep their old page code until they are reloaded.
Invalidation conditions
Section titled “Invalidation conditions”- If a launcher starts the server without supervising it, a restart through
/api/restartstops the server: that launcher must supervise it, or this design must change. - If desktop packaging lets installers replace the running shell, the deferred unpacked Electron install, the premise that the shell survives updates changes.
Validation
Section titled “Validation”Executed on Linux/WSL2:
- Python: the real desktop service registers its pid and port, removes the registration when its pipe closes, and exits with 75 after
POST /api/restart. An app without aDashboardServerrefuses with 409. The updater status distinguishes a handover from a failure, and the launcher configuration test coversDashboardServer. The full suite passes (335 tests). - Go: the supervisor restarts on 75 only and ends on a signal.
restartRegisteredUIwaits for the replacement, starts nothing without a running server, and reports a server that cannot restart or does not come back. The full suite passes. - Electron:
DesktopServicerespawns on 75, quit ends the replacement, 75 during quit does not respawn, and a failed respawn is reported. 22 tests pass. - Browser: the update dialog waits through the installer handover without offering Try again, then reloads when the new version serves, and offers Try again after a failure. The full Playwright suite passes (59 tests).
- End to end, with the real CLI and Python server:
imprint ui start,restart,statusandstopkept the same supervisor while the server pid changed, and left no process or registration behind. The realDesktopServicefromui-electron/service.jswas driven with the real CLI:imprint ui statussaw its service,imprint ui restartreplaced it without a reported failure, andimprint ui stopreached the app as “Local service stopped”.
Executed natively on Windows through WSL interop:
scripts/test-windows-setup.ps1passes under Windows PowerShell 5.1. Setup ends withui restart, and a failing restart is logged without failing setup. Run against the previousimprint-setup.ps1, the test fails.- The cross-compiled Go tests pass on Windows: supervisor restart, restart waiting and refusal, and a console child of a
detachAttrsprocess has no visible window. The DETACHED_PROCESS counterpart, which would open a window on the desktop, was not run.
Not executed: a real Inno Setup update from one release to the next, an interactive installer upgrade, the macOS package handover, and the packaged Electron app on Windows, macOS or Linux, including the startup screen after imprint ui stop. Record native results in platform validation.