- _safe_kill: a companion can exit between the manager deciding to
signal it and the kill landing; swallow ProcessLookupError at the three
os.kill sites so the resulting race cannot take the manager down.
- _redirect_output: close the opened log fd after dup2 so a long-lived
companion does not leak a descriptor per start.
- serve_connection: drop a control connection whose line grows past
MAX_LINE_BYTES without a newline, so a client cannot pin unbounded
memory in the manager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A typo'd companion_stop_signal (e.g. "SIGTRM") passed validate_string
but raised ValueError in _signal_number when the manager later tried to
send it -- propagating past handle_line and killing the run loop.
Validate stop_signal at config-build time so a bad value fails loudly
on load and reread. As defense-in-depth, catch unexpected exceptions in
ControlServer.handle_line so no handler bug can escape and kill the
manager; they now return an error envelope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire ControlServer.handle_line to a real CompanionManager.handle_command and
assert the full decode/dispatch/encode round trip: status returns the companion
list, start routes through and reports a message, and unknown command, missing
name, and reread without a loader each return an error envelope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add gunicorn/companion/control.py with ControlServer, the manager's control
endpoint. It owns the Unix socket lifecycle (create unlinks any stale socket,
binds, chmods 0o600, and listens; close cleans up) and the newline-delimited
JSON framing: serve_connection buffers reads and answers each complete line.
decode_command parses a request into a JSON object carrying a string cmd, and
encode_response writes a newline-terminated JSON line; malformed input becomes
a CommandError rendered as an {ok: false, error: ...} reply so a bad client
can't take the manager down. Turning a command into an action is delegated to a
dispatch callable, wired up in the later command tasks.
The socket is 0o600 and owned by the non-root user gunicorn runs as; no group
switching.
Add tests/test_companion_control.py covering decode, encode, handle_line
dispatch and error envelopes, and socket create/close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>