Summary
install-controller.sh describes itself as "idempotent, single machine", but re-running it against an already-loaded launchd agent fails at launchctl bootstrap and, because the script runs under set -euo pipefail, exits immediately — after having already booted out the running service. The net effect is that re-running the installer stops a working controller and leaves it stopped.
Environment
- Controller:
main @ 52c2b20 (v2.9.10); same code ships in the 2.9.10 desktop bundle
- macOS 27.0 (26A5388g), Apple M5 Pro (arm64)
Affected code
scripts/install-controller.sh:161-162
launchctl bootout "$SERVICE" >/dev/null 2>&1 || true
launchctl bootstrap "gui/$(id -u)" "$PLIST"
launchctl enable "$SERVICE"
launchctl kickstart -k "$SERVICE"
bootout is asynchronous. The || true swallows its status, and nothing waits for the service to actually leave the domain before bootstrap runs. When the domain still holds the old service, bootstrap fails:
Bootstrap failed: 5: Input/output error
Try re-running the command as root for richer errors.
set -e then aborts the script before enable/kickstart, so nothing restarts the service.
Reproduction
- Run
install-controller.sh on a clean machine. Controller installs and is healthy on :8080.
- Run the exact same command again — for example to change
LOCAL_STUDIO_MODELS_DIR.
Observed:
[local-studio] bun: 1.3.14
[local-studio] updating existing checkout at ...
[local-studio] installing controller dependencies…
[local-studio] reusing existing API key from .env
Bootstrap failed: 5: Input/output error
Script exits 5. launchctl list | grep local.studio returns nothing, and curl http://127.0.0.1:8080/health fails — the previously working controller is now down.
Recovering by hand works fine, which is what points at ordering rather than anything deeper:
launchctl bootout gui/$(id -u)/org.local.studio.controller
sleep 2
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.local.studio.controller.plist
launchctl kickstart -k gui/$(id -u)/org.local.studio.controller
Impact
Re-running the installer is the documented path for changing configuration (the header advertises LOCAL_STUDIO_MODELS_DIR, LOCAL_STUDIO_PORT, and friends as env overrides) and it is what the desktop app's "Deploy controller" flow pipes over ssh. In all of those cases the second run takes the service down instead of reconfiguring it. On a remote deploy the failure mode is worse, since the controller is left stopped on a machine you may not be sitting at.
Suggested fix
Wait for bootout to actually settle before bootstrapping:
launchctl bootout "$SERVICE" >/dev/null 2>&1 || true
for _ in $(seq 1 50); do
launchctl print "$SERVICE" >/dev/null 2>&1 || break
sleep 0.1
done
launchctl bootstrap "gui/$(id -u)" "$PLIST" || true
launchctl enable "$SERVICE"
launchctl kickstart -k "$SERVICE"
Tolerating a bootstrap failure and relying on kickstart -k is the belt-and-braces part: if the service is somehow still registered, kickstart -k restarts it with the freshly written plist regardless, so the script converges either way.
Summary
install-controller.shdescribes itself as "idempotent, single machine", but re-running it against an already-loaded launchd agent fails atlaunchctl bootstrapand, because the script runs underset -euo pipefail, exits immediately — after having already booted out the running service. The net effect is that re-running the installer stops a working controller and leaves it stopped.Environment
main@52c2b20(v2.9.10); same code ships in the 2.9.10 desktop bundleAffected code
scripts/install-controller.sh:161-162bootoutis asynchronous. The|| trueswallows its status, and nothing waits for the service to actually leave the domain beforebootstrapruns. When the domain still holds the old service,bootstrapfails:set -ethen aborts the script beforeenable/kickstart, so nothing restarts the service.Reproduction
install-controller.shon a clean machine. Controller installs and is healthy on :8080.LOCAL_STUDIO_MODELS_DIR.Observed:
Script exits 5.
launchctl list | grep local.studioreturns nothing, andcurl http://127.0.0.1:8080/healthfails — the previously working controller is now down.Recovering by hand works fine, which is what points at ordering rather than anything deeper:
Impact
Re-running the installer is the documented path for changing configuration (the header advertises
LOCAL_STUDIO_MODELS_DIR,LOCAL_STUDIO_PORT, and friends as env overrides) and it is what the desktop app's "Deploy controller" flow pipes over ssh. In all of those cases the second run takes the service down instead of reconfiguring it. On a remote deploy the failure mode is worse, since the controller is left stopped on a machine you may not be sitting at.Suggested fix
Wait for
bootoutto actually settle before bootstrapping:Tolerating a
bootstrapfailure and relying onkickstart -kis the belt-and-braces part: if the service is somehow still registered,kickstart -krestarts it with the freshly written plist regardless, so the script converges either way.