A fork-per-message child inherits the server's listening socket. A child
that outlived the server (a long-running handler such as an engine loop)
kept the port bound, so the restarted server failed with EADDRINUSE until
that child died.
- Track every forked child (Server\ChildProcesses). On hard and soft
shutdown, send SIGTERM to the ones still running, SIGKILL them after
websockets.shutdown.child_grace_seconds (default 2) and reap them. A PID is
signalled only while waitpid() confirms it is still an unreaped child, with
SIGCHLD blocked, so a recycled PID is never hit. Opt out with
websockets.shutdown.terminate_children = false.
- Right after fork the child closes its copy of the listening socket, so even
a stray child cannot hold the port. Only the fd is closed: the React server
is not closed, because removing it from an ext-ev/ext-event loop would edit
the epoll set the child shares with the parent.
- The child restores default SIGTERM/SIGINT/SIGCHLD handling. Under ext-ev
the inherited libev handlers swallowed SIGTERM in the child.
- A child that dies of a fatal error (out of memory, E_ERROR) now sends
<event>:error from a shutdown function (Server\ChildFatalErrorGuard,
#1462). A child that dies without any reply (killed by a signal, or the
shutdown function never ran) gets <event>:error from the parent, decided
once both its stream and its exit status are in
(Websocket\ChildMessageOutcome).
- Tests: real forks, signals and sockets for the shutdown, the port release
and the fatal reply; end to end through Handler::forkWithSocketPair(). The
test TestCase now stops a leftover broadcast socket, which made
ws_available() true for later tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- AGENTS.md: canonical agent/contributor reference — #[Websocket] attribute vs native
Websocket\Controller, the :progress/:response/:error wire protocol,
progress/success/error/broadcast/whisper, ws_broadcast/ws_whisper/wsSession globals,
the fork-per-message execution model, auth()->user() over the bridge, defer() vs Jobs,
resolver cache/restart, and a footgun checklist with a source map.
- README + docs/advanced-usage/helpers-and-testing: replace the fictional
wsSession('channel', [...]) "auth payload" example — wsSession() takes no arguments and
returns a per-connection store — and fix WebsocketService::getAuth($socketId) signature.
- docs/getting-started/installation: beyondcode -> blax-software composer require.