Skip to content

← The log

Whose launchd job is this? A default cannot say.

An installation is its data directory — so a resolver with sensible fallbacks has to be stopped from filling in an identity it does not have.

FixKonstantin Tarkus

Engwire’s launchd job goes in under one fixed label, com.engwire.local. That is Engwire’s choice rather than launchd’s, and it means a user gets one supervised installation: a second one — a second data directory, which ENGWIRE_HOME, XDG_DATA_HOME or HOME each decide — wants the label the first is already using. Before service install replaces a job, or uninstall boots one out, something has to answer a question about the plist that describes it: was this written by me, by another installation, or is there no plist here?

installedPlist now answers exactly that: none, ours, or theirs. Note what it answers about — a file, not a loaded job. Those two come apart, and the last section is about how.

An installation is its data directory

Not its binary, not its config. The queue, the runner lock and the worktrees all key on the data directory, so two environments that resolve to the same one are the same installation however their config roots differ — and a plist Engwire wrote carries the environment that locates it.

The function that fills in blanks is the wrong one to ask

paths() resolves an environment into the directories Engwire uses, and it has fallbacks: without ENGWIRE_HOME, data falls back through XDG_DATA_HOME and config through XDG_CONFIG_HOME, each landing on the home directory. That is correct behaviour for configuration, where a missing value should become a sensible default.

It is exactly wrong for identity. Hand paths() an environment that locates nothing and it resolves, helpfully, to whatever the asking process would use for itself — which for an installation sitting in the default location is its own data directory. The job reads as ours. A defaulting function turns I cannot tell into it is mine, which is the one answer that must never be given about a job you cannot identify, because engwire uninstall boots the launchd job out only when the answer is ours. A wrong ours stops somebody else’s runner, quite possibly mid-review.

So the environment is checked for whether it locates data at all before paths() is allowed near it. No data root, no identity, and the answer is theirs.

Three commands read that answer and use it differently.

  • engwire uninstall gates on it. This is the destructive path and the reason the question exists.
  • engwire service install replaces the job either way, and uses the answer only to say when what it is replacing belonged to another installation. That installation gets its job back by running service install itself — the label is a thing you take, not a thing you lose.
  • engwire service uninstall does not ask at all. It removes the user’s job, whoever set it up.

Comparing two data directories is not string comparison

An installation is its data directory, not its spelling. /tmp is a symlink to /private/tmp on macOS, and service install records whatever ENGWIRE_HOME said at install time while uninstall reads whatever it says now. Two spellings, one directory.

Getting that wrong is expensive in exactly one direction: Engwire refuses to stop its own service, and then deletes the data that service is still running against.

Equal strings answer without touching the disk; only two different spellings pay for a filesystem resolution.

Anything it cannot identify is somebody else’s

The reader recognises the shape Engwire generates and declines everything else. A plist that is present but unreadable is somebody else’s. A plist carrying a label that is not Engwire’s is not a job this could act on either — and it is reported as supervising nothing, rather than being credited with supervising this installation, which would be worse than saying nothing about it at all.

The XML is matched as a shape rather than parsed, and the reason is conservatism rather than laziness. A parser is the more permissive reader here, not the more correct one: <key><![CDATA[Label]]></key> and <key>La&#98;el</key> both mean Label to one, so a reader that normalised them would accept documents Engwire did not write. Ownership is inferred from Engwire’s own emitted shape, and the reader is tuned against the false yes. Both errors cost something, but a false no is one you take back by running service install again; a review stopped mid-run is not.

Matching the head as one pattern also does something searching for keys individually would not: it stops values being assembled from different dictionaries. Only the three keys ownership turns on are read, in the order Engwire writes them, and the rest of the document is checked for shape, and for whether any of the three comes round again. Comments sitting between elements are stripped first, so a commented-out key cannot vote; comment-like text inside a tag is refused rather than cleaned up.

Matching only the head has a second payoff — a newer binary can still identify a plist an older one wrote, because the tail carries tuning that changes between versions and says nothing about ownership.

Re-reading the answer, and the race that stays

Ownership is a snapshot, and bootout names the label rather than the file the snapshot came from. Between reading the answer and acting on it, another installation’s service install can replace both the plist and the job under that one label — and stopping that runner is the thing the whole question exists to prevent.

So the answer is re-read immediately before the removal. That does not close the race, and the code says so rather than implying otherwise: only a lock shared across installations could, and install deliberately does not take one. What it removes is the human-sized part of the window: without it, the answer being acted on is the one from before a listing was printed and a confirmation was typed.

A plist is not a job

Absent is none, and none is not quite “no service”. A job stays loaded after somebody deletes the file describing it, and only launchd can be asked about that. installedPlist reads a file; whether the label is loaded is a different question with a different answer, asked of launchctl print and only where the plist is absent. Conflating them would let uninstall walk past an orphaned job it should have reported.

They come apart in the ordinary case too, not only the broken one: install writes the new plist before booting out the old job, so for that moment the file and the loaded job describe different installations.

Nor may the identification throw. service install asks the question, and service install is the command someone runs to repair a service — an identification that fails loudly is a repair that cannot start.

engwire/engwire#17 →

Engwire reviews the pull requests that ask for you, on your own machine.

MIT · macOS and Linux · no account to make

curl -fsSL https://engwire.com/install.sh | sh

What Engwire does →