kickstart does not reload your plist

Christian Drexler

I lost an evening to this one and the lesson fits in two lines, so here are the two lines first:

launchctl bootout gui/$(id -u)/local.my-service
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.my-service.plist

Run those after any edit to a plist. Everything below is why.

What kickstart does

launchctl kickstart -k gui/<uid>/<label> is the command everyone learns for "restart this service". It kills the running process and starts it again. What it does not do is read the plist file. launchd keeps the job definition it loaded earlier, in memory, and kickstart restarts from that copy. The file on disk could say anything; launchd has not looked at it since the last load.

This is reasonable behaviour for the command's actual purpose, which is to bounce a process whose definition has not changed. It becomes a trap the moment you edit the plist, because the edit and the restart feel like one action and the restart reports success.

How it looks when it bites

I had moved a project to a new folder and updated the path in several plists. Each one got a kickstart. Each came back up and then stopped, and the log said the old path did not exist. Which was true, I had just moved it. The obvious reading was that my edit was wrong, so I opened the plist, checked the path, saw the correct new one, and kickstarted again. Same error. I did that three times before suspecting the restart rather than the edit.

The tell, once you know it: the error mentions a value that is no longer in the file. An old path, an old port, an old environment variable. If the running process is complaining about something you already changed, launchd is running a copy of the plist from before your change.

A second tell: launchctl print gui/$(id -u)/<label> shows the loaded definition. Compare its program arguments and environment with the file. When they differ, there is your problem.

The fix

Unload by label, then load the file. bootout removes the job from launchd entirely, running process included. bootstrap reads the plist fresh and loads it. bootout has to come first, because bootstrapping a label that is already loaded fails with "service already loaded" and nothing changes. On older systems the same pair is launchctl unload and launchctl load; they still work, but Apple calls them legacy, and the bootout and bootstrap pair gives clearer errors.

Then kickstart is fine again, for the case it was made for: the plist is unchanged and only the process needs a nudge.

The habit

I now treat a plist edit as incomplete until the bootout and bootstrap have run, the same way a config change in a server program is incomplete until the reload. A small shell function that takes a label and does both saves typing it. What it mostly saves is the three rounds of staring at a correct file wondering why it is wrong.