§7d.4 Integration with the server
Wiring in cli/root.go
// Pseudocode — exact order matters for shutdown hooks.
supSvc, err := service.NewSupervisordService(service.SupervisordConfig{
InitialState: defaultProcessState(selfPath, socketPath),
})
if err != nil { return err }
if err := supSvc.Start(ctx); err != nil {
return fmt.Errorf("supervisord: %w", err)
}
go supSvc.RunHealthLoop(ctx, 5*time.Second)
// ScriptService receives supSvc for status + shutdown ordering
scriptSvc := service.NewScriptService(..., supSvc)
// … HTTP server, hub, etc.
// Shutdown (after SIGINT/SIGTERM):
scriptSvc.Shutdown(ctx) // RPC drain, run.stop
supSvc.Stop(ctx) // supervisorctl shutdown
srv.Shutdown(shutdownCtx)
SupervisordService is constructed before services that depend on
managed processes. Shutdown reverses the order.
Replacing executor/supervisor.go
The hand-rolled supervisor planned in
04-repository-layout.md (pkgs/bigfred/server/executor/supervisor.go)
is superseded by this component:
| Before (§7 #12) | After (§7d) |
|---|---|
exec.Command("loco", "scripts-executor", …) in Go |
[program:scripts-executor] in rendered config |
| custom exponential backoff | supervisord autorestart=true + daemon respawn loop |
executor.healthy flag in Go |
SupervisordService.AllStatus + RPC dial |
| manual SIGKILL after 5 s | stopwaitsecs + ScriptService.Shutdown RPC |
Keep pkgs/bigfred/server/executor/ for the RPC client/server (client.go,
server.go, messages.go, codec.go). Remove supervisor.go from the
plan — its responsibilities move to SupervisordService.
Cross-cutting doc §7 #12 should be updated after implementation to reference §7d instead of the hand-rolled spawn loop.
Managed Redis
loco-server boots its own redis-server under supervisord by
default (group infra, program redis). Rationale:
- BigFred needs Redis for cross-process pub/sub and the
loco:statecache. Asking operators to run a sibling daemon by hand reintroduces the very class of "is everything running?" foot-gun that supervisord exists to eliminate. - The bundled instance binds to
127.0.0.1only, persists with RDB only (--appendonly no, defaultsave 60 100) to limit disk writes while keeping Redis state across restarts, and listens on port 6379 by default to avoid colliding with a pre-existing system Redis on:6379. Pass--redis-no-persistor--redis-rdb-save=""for ephemeral mode. - Operators with their own Redis (e.g. shared between multiple
BigFred installations, or running on another host) pass
--redis-external --redis-addr host:portto skip the managed daemon.loco-server.WaitReadystill blocks onPINGbefore serving HTTP so a misconfigured external Redis fails fast.
CLI surface (all on loco-server):
| Flag | Default | Meaning |
|---|---|---|
--redis-bin |
redis-server |
PATH-relative binary used by the supervised process |
--redis-bind |
127.0.0.1 |
interface the managed daemon binds on |
--redis-port |
6379 |
TCP port the managed daemon listens on |
--redis-data-dir |
supervisord config dir | working dir for dump.rdb |
--redis-addr |
<bind>:<port> |
dial address used by loco-server and dcc-bus |
--redis-external |
false |
skip managed daemon, dial external Redis instead |
--redis-rdb-save |
60:100 |
RDB snapshot rules (seconds:changes, repeatable) |
--redis-no-persist |
false |
disable RDB snapshots (ephemeral mode) |
Default process catalogue (M7 scripts milestone)
| Group | Program | Autostart | Autorestart | Notes |
|---|---|---|---|---|
loco |
scripts-executor |
true | true | same flags as today (--executor-socket) |
Rows added by M4.5 (§7e DCC bus daemon):
| Group | Program | Autostart | Autorestart | Notes |
|---|---|---|---|---|
dcc-bus |
dcc-bus-<layoutId>-<commandStationId> |
true | true | one daemon per attached (layout, commandStation) pair synced by DccBusService; spawned lazily via EnsureRunning or bulk SyncProgramsForLayouts. Command-line includes --layout-id, --command-station-id, --port, --station-name, --station-kind, --station-uri, --speed-steps, --jwt-secret, --redis-addr (§7e.2). |
Future rows (post-M4.5):
| Group | Program | Autostart | Autorestart | Notes |
|---|---|---|---|---|
loco |
mcp-stdio-bridge |
false | false | on-demand, admin-triggered |
Adding a row is one UpsertProgram call — no supervisord knowledge in
the caller beyond ProgramSpec.
system.status WebSocket event
Extend the existing status payload:
{
"type": "system.status",
"payload": {
"scriptsExecutor": "healthy",
"supervisord": {
"daemon": "running",
"groups": {
"loco": {
"scripts-executor": {
"status": "RUNNING",
"pid": 12345,
"uptimeSec": 3600
}
}
}
}
}
}
Mapping rules:
scriptsExecutor: "healthy"when programRUNNINGand executor RPC socket accepts a connection (preserves current semantics).scriptsExecutor: "failed"whenFATALor daemon respawn policy gives up (same banner as §7 #12).supervisord.daemon: "degraded"when daemon respawn backoff exhausted.
Only diffs are broadcast on the health ticker to avoid WS noise.
Who calls Apply
| Caller | When |
|---|---|
cli/root.go |
initial Start (via InitialState) |
ScriptService |
never — executor flags are fixed at boot for M7 |
CommandStationService (future) |
CS added/removed/connection string changes |
| Admin API (future) | manual restart policies |
M7 does not expose HTTP for process management. Dynamic CS workers are a later milestone.
Repository layout additions
Update 04-repository-layout.md when
implementing:
pkgs/bigfred/server/supervisord/
templates/supervisord.conf.tmpl
config.go
render.go
ctl.go
daemon.go
render_test.go # golden-file template tests
pkgs/bigfred/server/service/supervisord.go
pkgs/bigfred/server/service/supervisord_test.go # fake supervisorctl in tests
Remove from the planned tree:
pkgs/bigfred/server/executor/supervisor.go # deleted — replaced by §7d
Makefile / dev environment
Add to 12-makefile.md when implementing:
# Verify supervisord is available in dev/CI
check-supervisor:
@command -v supervisord >/dev/null || (echo "install: pip install supervisor" && exit 1)
Document in 02-tech-stack.md:
| Component | Choice | Role |
|---|---|---|
| Process supervisor | supervisord (Python supervisor package) |
Manages sibling processes non-root; configured via Go templates |
Acceptance criteria (draft)
To be copied into 14-acceptance-criteria/ when the milestone is scheduled:
loco serveras an unprivileged user creates/datapaths, renders config, and starts supervisord without root.supervisorctl -c /data/etc/supervisord/supervisord.conf statusshowsscripts-executorinRUNNINGwithin 10 s of boot.- Changing desired state via
Apply(add/remove program, toggle autostart) rewrites config and applies viareread+updatewithout changing the supervisord daemon PID (hot reload). - Changing a program's
commandrestarts only that program; siblings stayRUNNING. kill -9on a managed program withautorestart=truereturns it toRUNNINGwithout restartingloco server.SIGTERMtoloco serverstops supervisord cleanly; no orphanedscripts-executorremains (pscheck).- Template unit tests cover at least: empty groups, single group / single
program, multiple groups,
autostart=false,autorestart=false,shellQuoteescaping for/bin/bash -c.