4.8.4 Listener Lifecycle
listener.ss is an internal transport-lifecycle module for the network owner.
It uses only high-level :std/io socket operations. It does not authorize peers,
perform TLS or application handshakes, implement pending limits, or publish
connections. Its types and helper exports must not be re-exported by the public
network facade.
4.8.4.1 Construction
make-listener(owner, address) calls stream-listen, which resolves DNS addresses
and dispatches internet and Unix endpoints. It retains the actual socket address
in Listener.address.
This includes the assigned port for TCP port-zero listeners. The existing std/io
backlog and socket-option defaults are used without private socket setup.
The listener owns its server socket, Unix sidecar lock when applicable, and one
accept thread. It borrows the ListenerMonitor. The owner must track listener
construction as network-owned work
until it either registers the result or closes it. Setup completes before the
accept worker starts, but callbacks may run before make-listener returns.
Network.listen! returns the listener’s captured address after
registration and checks concurrent network closure before reporting success.
Unix listeners require a nonempty, NUL-free filesystem pathname. Construction
creates missing parent directories with create-directory* before opening the
sidecar lock. Existing directories are left unchanged; errors propagate normally.
Directories are not removed during cleanup.
Before binding sock, construction opens sock.lock without truncation or symlink
following and tries an exclusive advisory lock. It uses open-file-writer/lock with a zero
timeout: contention fails immediately with Timeout, without touching sock.
The lock file is created with mode 0600 when absent.
Once locked, an existing socket pathname is removed so an abandoned socket from a previous run does not prevent startup. A non-following file-type check refuses to delete regular files, directories, or symlinks. No socket inode tracking is needed: cooperating listeners use the lock to establish ownership.
The lock stays held throughout the listener’s lifetime. On construction failure
after the bind attempt starts, any socket pathname is cleaned up before the lock
is released. Shutdown closes the server socket, removes its pathname, releases
the lock, and notifies the owner. Cleanup still releases the lock if unlink fails.
The .lock file is never removed: it must remain a stable lock inode across runs.
The OS releases the lock when its last open handle closes, including process exit.
This is cooperative exclusion, not protection from processes bypassing the lock. The socket and sidecar paths must stay under listener ownership; callers must not rename or replace them externally. Filesystem errors propagate normally, and non-socket paths are never treated as stale sockets.
4.8.4.2 Listener Monitor
ListenerMonitor has two methods, called without the listener mutex:
-
accept!(listener, socket)receives a newly accepted rawStreamSocketand returns a boolean. True transfers ownership; false makes the listener close that socket and continue accepting. The owner reserves pending capacity and registers its worker before returning true. If it raises, the untransferred socket is closed and the listener aborts. It must return promptly, not run a handshake inline and stall acceptance. -
closed!(listener)runs once after server-socket/Unix-path cleanup and lock release, before the accept worker exits. It lets the owner retire its listener registration. It runs even when acceptance or cleanup fails, and must return promptly.
TCP acceptance transfers a raw socket. The owner must use mutual TLS before passing it to the network handshake driver. Both TLS and application handshake must use the pending attempt’s fixed deadline. An owner at capacity or already closed returns false rather than creating unbounded pending work.
4.8.4.3 Shutdown And Errors
listener-close! atomically marks closed on the first call, closes the server
socket to interrupt accept, and joins the worker outside the mutex. Close ignores
socket-close and join errors and returns void. An already closed listener is an
immediate no-op, including when its worker is still completing cleanup.
Calling close from an owner callback skips self-join; cleanup completes when that
callback returns. An owner requiring completion must explicitly join the worker,
even if close was already requested. Previously transferred sockets belong to the
owner and are not closed by listener shutdown.
Only an accept failure during requested shutdown is treated as normal. Exceptions
from owner callbacks and cleanup remain available through an explicit
network-thread-join!, even if the callback closed the listener first. The internal
worker result wrapper retains the exact exception, including #f; a raw native
join alone does not rethrow it. Unexpected worker failures are logged
through the one /ensemble/network logger with address context and
exception: (exception->string e). No exception messages or irritants
are inspected to infer a recoverable cause. Cleanup still attempts socket close,
pathname cleanup, lock release, and owner notification if an earlier action raises.
4.8.4.4 Tests
./build.sh test std/ensemble/network/listener-test.ss exercises real DNS/TCP and
Unix sockets, byte handoff, refusal, close/join, transferred socket ownership,
self-close, notification ordering, lock contention before and after binding,
sidecar persistence, stale-socket recovery, non-socket preservation, and lock
release after failure. Stale recovery is tested by closing a raw socket and its
lock handle without unlinking the pathname, not by killing a process. The suite
does not simulate peer election or claim a complete network constructor.