- A daemon keeps running the copy it was started from, so after an update the old one ran until the next login and nothing said so; clients compared only the major protocol version. Every build now carries a UUID, the daemon reports it as ServerInfo.build_id under protocol 1.3, and a daemon too old to report one counts as outdated. - The window shows a notice above every page when the daemon it reached is another build, with both versions. Update now registers the copy that was opened as the service and restarts the daemon from it; Not now puts the notice away. Nothing is restarted unless the user asks, so two copies open at once cannot replace each other's daemon in turn. A newer daemon is offered as Use this version, and one the service did not start, or one reached with --socket, gets no button. - service install --start now stops a running daemon before registering and starting, so the daemon started is the program that was asked. Under systemd and Task Scheduler it used to rewrite the registration and leave the old daemon running, since starting a running service does nothing. - service status says when the daemon is another build than the program asked, and --json carries same_build. - The App Store app stops a daemon another build of the app left running and starts its own, instead of attaching to it. - Packaging sets MIDI_HARBOR_BUILD_ID once for everything a run builds, because the App Store app, its helper and each architecture are compiled separately and must agree. Packages of one release share an identifier, so neither replaces the other's daemon.
4.5 KiB
Research: Daemon Updates
The investigation behind this spec, under its number in the project-wide research log.
R-108: A daemon left running by another build
Status: DONE (2026-10-02). Built as T253.
What happened before. A client called GetServerInfo and refused only another major
protocol version. Nothing compared builds, so after an update the old daemon ran on:
| Install | Updating while the daemon runs |
|---|---|
| macOS disk image, launchd | The old daemon kept running until the next login |
| Mac App Store | Quitting the app stops its daemon, so the updated app started the new one; a daemon left by a window that crashed was attached to and kept |
.deb, .rpm |
Installing starts nothing, so the old daemon kept running |
| AppImage | A new file moved over the old one was used at the daemon's next start |
| Windows archive | No installer; the old daemon kept running |
service install --start did not help under systemd or Task Scheduler either: it rewrote the
registration and started the service, and starting a service that is running does nothing.
The identifier. midi_harbor_core::BUILD_ID, a UUID set by crates/core/build.rs. It takes
MIDI_HARBOR_BUILD_ID from the environment when that is set, and makes one otherwise. A package
can hold binaries built separately that must agree: the App Store app and its headless helper,
and each architecture of a universal binary. So packaging/macos/build.sh and the release build
each choose one identifier for everything they build in a run. A build outside packaging gets a
new identifier when the core crate or VERSION changes, which is as often as cargo runs the
script again without forcing every build to relink. The daemon reports it as
ServerInfo.build_id (7), protocol 1.3; an older daemon leaves it empty, which matches nothing.
One release, several packages. The .deb, .rpm and AppImage of a release hold the same
binary and so the same identifier. A user who installs two of them has one build twice, and no
daemon to update, so neither window replaces the other's daemon. Comparing the registered path as
well was considered and left out: it would restart the daemon every time the other copy was
opened, to run the same code.
Why reinstall rather than restart. Restarting runs whatever the service is registered to run, which after a second install is the other copy. Registering this copy and then starting it is what makes the daemon the one the window came with, wherever it is.
Asking instead of acting. The first version replaced the daemon as soon as the window connected. That needed guards against a loop: two windows of different builds each replacing the other's daemon when they reconnected, and a replacement that failed being tried again on every reconnect. The owner asked for a notice with an Update now button instead. Nothing is then restarted unless someone asks, which removes the loop rather than guarding it, and leaves the few seconds without MIDI to be timed by the person at the machine.
What the notice says. "The daemon is outdated" for an older version or one too old to say,
"a different build" for the same version, and "newer than this window" for a newer one, where the
button reads Use this version: an older program in a newer one's place may not read the
configuration it wrote, which is refused with SchemaTooNew. The button is offered only when the
service manager says it is running the daemon and the window is on the service's own socket.
Otherwise there is no registration that says how the daemon was started, and the notice says to
restart it from this copy.
While the daemon is being replaced the old one still answers for a moment. The window does not reconnect until the replacement has finished, or it would put the notice straight back.
Replacing under each service manager. ServiceManager::replace stops the daemon, installs
the registration and starts it. Stopping comes first so the service manager stops the process it
started. launchd is the exception: installing boots the old job out, which stops its daemon, and
stopping it beforehand only has launchd start the old one again in between.
The App Store build has no service. The app starts its helper, or attaches to one already answering. It now asks an attached daemon for its build and, when it is another, asks it to stop and starts its own.
Checked live on Arch Linux under systemd, with two builds of the tree given the identifiers
aaaaaaaa-… and bbbbbbbb-…. See T253 for what was seen.