Plan: Master Control Socket (IPC)
Add a control socket from master to slave using unix domain sockets (POSIX)
and named pipes (Windows). Serve an HTTP API on it. Signals remain the
backbone — IPC is best-effort and additive.
Goals
- Master creates an IPC listener and passes the path to the slave via env var
- Master serves an HTTP API on the IPC listener:
POST /overseer/trigger-update — trigger a fetch cycle
GET /overseer/status — return master status as JSON
- Fallback to user-provided
Config.Handler for unmatched routes
- Slave (and external tools) can connect to the socket to call the API
- If IPC is unavailable, everything falls back to signals (existing behavior)
Platform Strategy
Follow the existing build-tag pattern (sys_posix.go, sys_windows.go,
sys_unsupported.go). The IPC transport is abstracted behind two functions
that each platform file implements:
// listenIPC creates a platform-appropriate IPC listener.
func listenIPC(path string) (net.Listener, error)
// dialIPC connects to an IPC listener.
func dialIPC(path string) (net.Conn, error)
// ipcPath generates a unique IPC address for this overseer instance.
func ipcPath(token string) string
| Platform |
Listener |
Path format |
Dependency |
| linux, darwin, freebsd |
net.Listen("unix", path) |
/tmp/overseer-<token>.sock |
stdlib |
| windows |
winio.ListenPipe(path, nil) |
\\.\pipe\overseer-<token> |
github.com/Microsoft/go-winio |
| unsupported |
returns error |
n/a |
none |
Both listenIPC and dialIPC produce stdlib net.Listener / net.Conn,
so the HTTP server and client code in api.go is fully platform-agnostic.
Config Changes (overseer.go)
Add to Config:
// Handler is an optional fallback HTTP handler served on the overseer
// control socket/pipe. Requests not matching overseer's built-in routes
// (/overseer/*) are forwarded here. Runs in the master process.
Handler http.Handler
New env constant:
envIPCPath = "OVERSEER_IPC_PATH"
State Changes (proc_slave.go)
Add to State:
// IPCPath is the address of the master's control socket (unix socket
// path on POSIX, named pipe path on Windows). Empty if IPC is unavailable.
IPCPath string
Populated from os.Getenv(envIPCPath) in slave.run().
Master Changes (proc_master.go)
New fields on master:
ipcPath string
ipcListener net.Listener
In master.run(), after setupSignalling() and before forkLoop():
mp.ipcPath = ipcPath(token())
if ln, err := listenIPC(mp.ipcPath); err != nil {
mp.warnf("ipc listen failed (%s). control socket disabled.", err)
} else {
mp.ipcListener = ln
go http.Serve(ln, mp.buildAPIMux())
}
In master.fork(), add to slave env:
e = append(e, envIPCPath+"="+mp.ipcPath)
Cleanup: defer close listener + remove socket file (POSIX only; pipes auto-clean).
API (api.go — new file)
Platform-agnostic. Builds the HTTP mux and handlers.
func (mp *master) buildAPIMux() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/overseer/trigger-update", mp.handleTriggerUpdate)
mux.HandleFunc("/overseer/status", mp.handleStatus)
if mp.Config.Handler != nil {
mux.Handle("/", mp.Config.Handler)
}
return mux
}
POST /overseer/trigger-update — calls go mp.fetch(), returns {"ok":true}
GET /overseer/status — returns JSON: {pid, binHash, slaveID, restarting, restartedAt}
- Fallback —
Config.Handler if set, otherwise 404
Package-Level Function (overseer.go)
func TriggerUpdate() error
- From master: calls
mp.fetch() directly
- From slave: HTTP POST to
IPCPath, falls back to triggerRestart() (signal) on error
func TriggerUpdate() error {
if currentProcess == nil {
return errors.New("overseer not running")
}
if sp, ok := currentProcess.(*slave); ok {
if sp.state.IPCPath != "" {
if err := ipcTriggerUpdate(sp.state.IPCPath); err == nil {
return nil
}
// fall through to signal
}
sp.triggerRestart()
return nil
}
if mp, ok := currentProcess.(*master); ok {
go mp.fetch()
}
return nil
}
Signal Fallback
IPC is best-effort. Signals are never removed.
- IPC setup failure in master is a warning, not fatal (matches
Fetcher.Init() pattern)
State.IPCPath is empty when IPC is unavailable — slave checks before dialing
TriggerUpdate() from slave tries IPC first, falls back to RestartSignal
Restart() remains purely signal-based, unchanged
New Files
| File |
Build tags |
Contents |
api.go |
(none) |
buildAPIMux, handlers, ipcTriggerUpdate helper |
ipc_posix.go |
linux,darwin,freebsd |
listenIPC, dialIPC, ipcPath via unix socket |
ipc_windows.go |
windows |
listenIPC, dialIPC, ipcPath via named pipe (go-winio) |
ipc_unsupported.go |
!linux,!darwin,!windows,!freebsd |
stubs returning errors |
Modified Files
| File |
Changes |
overseer.go |
Add Handler to Config, envIPCPath const, TriggerUpdate() |
proc_master.go |
Add IPC fields, start HTTP server, pass env to slave, cleanup |
proc_slave.go |
Populate State.IPCPath from env |
example/main.go |
Demonstrate Config.Handler usage |
go.mod |
Add github.com/Microsoft/go-winio (Windows only) |
Dependency Impact
- POSIX: No new dependencies
- Windows: Adds
github.com/Microsoft/go-winio (MIT, widely used, only compiled on Windows)
Plan: Master Control Socket (IPC)
Add a control socket from master to slave using unix domain sockets (POSIX)
and named pipes (Windows). Serve an HTTP API on it. Signals remain the
backbone — IPC is best-effort and additive.
Goals
POST /overseer/trigger-update— trigger a fetch cycleGET /overseer/status— return master status as JSONConfig.Handlerfor unmatched routesPlatform Strategy
Follow the existing build-tag pattern (
sys_posix.go,sys_windows.go,sys_unsupported.go). The IPC transport is abstracted behind two functionsthat each platform file implements:
net.Listen("unix", path)/tmp/overseer-<token>.sockwinio.ListenPipe(path, nil)\\.\pipe\overseer-<token>github.com/Microsoft/go-winioBoth
listenIPCanddialIPCproduce stdlibnet.Listener/net.Conn,so the HTTP server and client code in
api.gois fully platform-agnostic.Config Changes (
overseer.go)Add to
Config:New env constant:
State Changes (
proc_slave.go)Add to
State:Populated from
os.Getenv(envIPCPath)inslave.run().Master Changes (
proc_master.go)New fields on
master:In
master.run(), aftersetupSignalling()and beforeforkLoop():In
master.fork(), add to slave env:Cleanup: defer close listener + remove socket file (POSIX only; pipes auto-clean).
API (
api.go— new file)Platform-agnostic. Builds the HTTP mux and handlers.
POST /overseer/trigger-update— callsgo mp.fetch(), returns{"ok":true}GET /overseer/status— returns JSON:{pid, binHash, slaveID, restarting, restartedAt}Config.Handlerif set, otherwise 404Package-Level Function (
overseer.go)mp.fetch()directlyIPCPath, falls back totriggerRestart()(signal) on errorSignal Fallback
IPC is best-effort. Signals are never removed.
Fetcher.Init()pattern)State.IPCPathis empty when IPC is unavailable — slave checks before dialingTriggerUpdate()from slave tries IPC first, falls back toRestartSignalRestart()remains purely signal-based, unchangedNew Files
api.gobuildAPIMux, handlers,ipcTriggerUpdatehelperipc_posix.golinux,darwin,freebsdlistenIPC,dialIPC,ipcPathvia unix socketipc_windows.gowindowslistenIPC,dialIPC,ipcPathvia named pipe (go-winio)ipc_unsupported.go!linux,!darwin,!windows,!freebsdModified Files
overseer.goHandlerto Config,envIPCPathconst,TriggerUpdate()proc_master.goproc_slave.goState.IPCPathfrom envexample/main.goConfig.Handlerusagego.modgithub.com/Microsoft/go-winio(Windows only)Dependency Impact
github.com/Microsoft/go-winio(MIT, widely used, only compiled on Windows)