Supervise launchd dev runtime through hot-dev-bg

This commit is contained in:
rcourtman
2026-03-24 15:39:55 +00:00
parent 15d30e4754
commit 1b1900e8bb
6 changed files with 114 additions and 8 deletions
@@ -37,6 +37,9 @@ server-side update execution surfaces.
15. `scripts/dev-check.sh`
16. `scripts/toggle-mock.sh`
17. `scripts/clean-mock-alerts.sh`
18. `scripts/dev-launchd-setup.sh`
19. `scripts/dev-launchd-wrapper.sh`
20. `scripts/com.pulse.hot-dev.plist.template`
## Shared Boundaries
@@ -51,7 +54,7 @@ server-side update execution surfaces.
2. Add or change release-build metadata injection, release artifact assembly, or governed promotion metadata resolution through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, and `scripts/release_control/resolve_release_promotion.py`, plus the container Dockerfile and governed release workflows that consume those same contracts
3. Add or change shell installer, Windows installer, container-agent installer, or auto-update script behavior through `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh`
4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `tests/integration/scripts/managed-dev-runtime.mjs`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, and `scripts/clean-mock-alerts.sh`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `tests/integration/scripts/managed-dev-runtime.mjs`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, and `scripts/com.pulse.hot-dev.plist.template`
## Forbidden Paths
@@ -196,6 +199,12 @@ Makefile targets, `scripts/toggle-mock.sh`, and `scripts/clean-mock-alerts.sh` m
route through the managed runtime control plane when they are operating on the
local dev stack, instead of resurrecting lane-local `hot-dev.sh` or raw Vite
process management through separate shell folklore.
That same rule now extends to the macOS auto-start surface. The launchd helper
may not boot a separate legacy foreground runtime beside the managed dev stack:
`scripts/dev-launchd-wrapper.sh`, `scripts/dev-launchd-setup.sh`, and the
generated `com.pulse.hot-dev` LaunchAgent template must supervise the same
managed `hot-dev-bg` control plane, so login-time auto-start, crash restart,
and takeover diagnostics all operate on one runtime model.
That shared `scripts/install.sh` boundary must also keep one canonical service
argument builder for the runtime flags it persists. Token-bearing install
paths, token-file systemd paths, wrapper-script launches, and later service
@@ -2136,7 +2136,10 @@
"package.json",
"scripts/build-release.sh",
"scripts/clean-mock-alerts.sh",
"scripts/com.pulse.hot-dev.plist.template",
"scripts/dev-check.sh",
"scripts/dev-launchd-setup.sh",
"scripts/dev-launchd-wrapper.sh",
"scripts/hot-dev-bg.sh",
"scripts/hot-dev.sh",
"scripts/install-container-agent.sh",
@@ -2241,7 +2244,10 @@
"Makefile",
"package.json",
"scripts/clean-mock-alerts.sh",
"scripts/com.pulse.hot-dev.plist.template",
"scripts/dev-check.sh",
"scripts/dev-launchd-setup.sh",
"scripts/dev-launchd-wrapper.sh",
"scripts/hot-dev-bg.sh",
"scripts/hot-dev.sh",
"scripts/toggle-mock.sh",
+4 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# dev-launchd-setup.sh - Install or uninstall the Pulse dev Launch Agent
# dev-launchd-setup.sh - Install or uninstall the Pulse managed dev LaunchAgent
#
# Usage:
# ./scripts/dev-launchd-setup.sh # Install and start
@@ -89,15 +89,16 @@ install() {
fi
echo ""
log_info "=== Pulse Dev Launch Agent Installed ==="
log_info "=== Pulse Dev LaunchAgent Installed ==="
log_info ""
log_info "The dev environment will now auto-start on login and auto-restart on crash."
log_info "The managed dev runtime will now auto-start on login and auto-restart on crash."
log_info ""
log_info "Useful commands:"
log_info " Restart: launchctl kickstart -k ${GUI_DOMAIN}/${LABEL}"
log_info " Stop: launchctl kill SIGTERM ${GUI_DOMAIN}/${LABEL}"
log_info " Status: launchctl print ${GUI_DOMAIN}/${LABEL}"
log_info " Logs: tail -f ${LOG_DIR}/hot-dev.stderr.log"
log_info " Runtime: cd ${ROOT_DIR} && ./scripts/hot-dev-bg.sh status"
log_info " Disable: launchctl bootout ${GUI_DOMAIN}/${LABEL}"
log_info " Remove: $0 uninstall"
}
+4 -3
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash
# dev-launchd-wrapper.sh - Wrapper for running hot-dev.sh under launchd
# dev-launchd-wrapper.sh - Wrapper for running hot-dev-bg launchd-session
#
# launchd doesn't source login profiles, so PATH won't include go, npm,
# node, fswatch, etc. Hand off to an interactive login zsh so the user's
# normal shell environment is loaded before exec'ing hot-dev.sh.
# normal shell environment is loaded before exec'ing the managed launchd
# supervisor path.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
export PULSE_DEV_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)"
exec /bin/zsh -ilc 'cd "$PULSE_DEV_ROOT" && exec bash scripts/hot-dev.sh'
exec /bin/zsh -ilc 'cd "$PULSE_DEV_ROOT" && exec bash scripts/hot-dev-bg.sh launchd-session --takeover'
+54 -1
View File
@@ -7,6 +7,7 @@
# ./scripts/hot-dev-bg.sh stop
# ./scripts/hot-dev-bg.sh restart
# ./scripts/hot-dev-bg.sh backend-restart
# ./scripts/hot-dev-bg.sh launchd-session [--takeover]
# ./scripts/hot-dev-bg.sh verify [--takeover]
# ./scripts/hot-dev-bg.sh status
# ./scripts/hot-dev-bg.sh logs
@@ -669,6 +670,56 @@ verify_bg() {
run_verify_proof_command
}
launchd_session_bg() {
local takeover="${1:-false}"
local managed_pid poll_interval
require_python
stop_and_exit() {
log "Launchd supervisor received termination signal; stopping managed runtime..."
stop_bg
exit 0
}
trap stop_and_exit TERM INT HUP
if ! is_running; then
log "Managed runtime is not running. Starting supervised session..."
start_bg "${takeover}"
else
managed_pid="$(managed_session_pid)"
if [[ -n "${managed_pid}" ]] && has_unmanaged_listeners "${managed_pid}"; then
if [[ "${takeover}" != "true" ]]; then
fail "Managed runtime has split ownership. Rerun with: ./scripts/hot-dev-bg.sh launchd-session --takeover"
fi
log "Managed runtime has split ownership. Reclaiming ports before launchd supervision..."
stop_bg
start_bg "${takeover}"
elif ! runtime_healthy; then
log "Managed runtime is unhealthy. Restarting it before launchd supervision..."
stop_bg
start_bg "${takeover}"
fi
fi
managed_pid="$(managed_session_pid)"
[[ -n "${managed_pid}" ]] || fail "Managed runtime did not produce a session pid"
poll_interval="${HOT_DEV_BG_SUPERVISOR_POLL_INTERVAL:-1}"
log "Supervising managed runtime for launchd (pid: ${managed_pid})"
while is_running; do
sleep "${poll_interval}"
done
rm -f "${PID_FILE}"
log "Managed runtime exited unexpectedly under launchd supervision"
touch "${LOG_FILE}"
tail -n 80 "${LOG_FILE}" || true
return 1
}
usage() {
cat <<EOF
Usage: $(basename "$0") <command>
@@ -678,6 +729,7 @@ Commands:
stop
restart
backend-restart
launchd-session [--takeover]
verify [--takeover]
status
logs
@@ -689,7 +741,7 @@ parse_takeover_flag() {
local flag="${2:-}"
case "${command}" in
start|restart|verify)
start|restart|verify|launchd-session)
if [[ -z "${flag}" ]]; then
printf "false\n"
return 0
@@ -731,6 +783,7 @@ main() {
start_bg "${takeover}"
;;
backend-restart) restart_backend_bg ;;
launchd-session) launchd_session_bg "${takeover}" ;;
verify) verify_bg "${takeover}" ;;
status) status_bg ;;
logs) logs_bg ;;
+36
View File
@@ -8,6 +8,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
HOT_DEV_BG="${ROOT_DIR}/scripts/hot-dev-bg.sh"
PACKAGE_JSON="${ROOT_DIR}/package.json"
FRONTEND_PACKAGE_JSON="${ROOT_DIR}/frontend-modern/package.json"
DEV_LAUNCHD_WRAPPER="${ROOT_DIR}/scripts/dev-launchd-wrapper.sh"
if [[ ! -x "${HOT_DEV_BG}" ]]; then
echo "hot-dev-bg.sh not found or not executable at ${HOT_DEV_BG}" >&2
@@ -106,6 +107,7 @@ test_cli_parses_takeover_flag() {
printf "start=%s\n" "$(parse_takeover_flag start --takeover)"
printf "restart=%s\n" "$(parse_takeover_flag restart --takeover)"
printf "verify=%s\n" "$(parse_takeover_flag verify --takeover)"
printf "launchd=%s\n" "$(parse_takeover_flag launchd-session --takeover)"
printf "backend_restart=%s\n" "$(parse_takeover_flag backend-restart)"
printf "plain=%s\n" "$(parse_takeover_flag start)"
if parse_takeover_flag status --takeover >/tmp/hot-dev-bg.invalid 2>&1; then
@@ -119,6 +121,7 @@ test_cli_parses_takeover_flag() {
assert_contains "takeover parsing enables start" "${output}" "start=true"
assert_contains "takeover parsing enables restart" "${output}" "restart=true"
assert_contains "takeover parsing enables verify" "${output}" "verify=true"
assert_contains "takeover parsing enables launchd-session" "${output}" "launchd=true"
assert_contains "backend restart remains flagless" "${output}" "backend_restart=false"
assert_contains "start without flag stays false" "${output}" "plain=false"
assert_contains "unexpected status flag is rejected" "${output}" "invalid=rejected"
@@ -145,6 +148,37 @@ PY"
assert_contains "verify proof defaults password" "${output}" "password=admin"
}
test_launchd_session_supervises_managed_runtime() {
local output
output="$(
HOT_DEV_BG_PATH="${HOT_DEV_BG}" \
bash -lc '
source "${HOT_DEV_BG_PATH}"
set +e
test_dir="$(mktemp -d)"
trap "rm -rf \"$test_dir\"" EXIT
PID_FILE="${test_dir}/hot-dev-bg.pid"
LOG_FILE="${test_dir}/hot-dev-bg.log"
require_python(){ :; }
start_bg(){ printf "%s\n" "999999" > "${PID_FILE}"; }
launchd_session_bg true
status=$?
printf "status=%s\n" "${status}"
'
)"
assert_contains "launchd session starts supervised runtime" "${output}" "Managed runtime is not running. Starting supervised session..."
assert_contains "launchd session announces supervision" "${output}" "Supervising managed runtime for launchd"
assert_contains "launchd session exits nonzero on child crash" "${output}" "status=1"
}
test_launchd_wrapper_uses_managed_supervisor() {
local output
output="$(sed -n '1,80p' "${DEV_LAUNCHD_WRAPPER}")"
assert_contains "launchd wrapper uses managed launchd-session" "${output}" "scripts/hot-dev-bg.sh launchd-session --takeover"
}
test_root_package_exposes_managed_runtime_entrypoints() {
local output
output="$(
@@ -292,6 +326,8 @@ test_detects_unmanaged_listeners() {
main() {
test_cli_parses_takeover_flag
test_verify_command_injects_managed_runtime_env
test_launchd_session_supervises_managed_runtime
test_launchd_wrapper_uses_managed_supervisor
test_root_package_exposes_managed_runtime_entrypoints
test_frontend_package_exposes_managed_runtime_entrypoints
test_backend_restart_requires_managed_runtime