Skip to content

Plan: Master Control Socket (IPC) #98

@jpillora

Description

@jpillora

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

  1. Master creates an IPC listener and passes the path to the slave via env var
  2. 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
  3. Slave (and external tools) can connect to the socket to call the API
  4. 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}
  • FallbackConfig.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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions