Skip to content

[BUG] Re-running install-controller.sh stops the controller — bootstrap fails with "5: Input/output error" #392

Description

@prasta1

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

  1. Run install-controller.sh on a clean machine. Controller installs and is healthy on :8080.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions