open
https://gitlab.synchro.net/main/sbbs/-/work_items/1296
## Summary
A JS `ListeningSocket` can never be closed. Neither `.close()` nor garbage collection releases the underlying sockets, so every script that creates one and is then re-run in the same process (`jsexec -l`, a Services recycle, the IRCd's `/RESTART`) leaks a full set of bound, listening sockets.
## Cause
`js_listening_socket_constructor()` (`src/sbbs3/js_socket.cpp`, around line 3615) stores the real sockets in `p->set` (an `xpms_set`) and sets `p->sock = INVALID_SOCKET`, `p->external = true`.
`do_js_close()` (same file, line 223) removes the event handlers for each `p->set` socket, then:
```c
if (p->sock == INVALID_SOCKET) {
p->is_connected = false;
return;
}
```
For a `ListeningSocket` that condition is always true, so the function returns before anything is closed. `js_finalize_socket()` calls `do_js_close()` and then `if (!p->external) free(p->set);`, which is also skipped because `p->external` is true. The only `xpms_destroy()` calls in the file are on constructor failure paths. The listening sockets stay open for the life of the process.
## Observed today (2026-10-23)
The IRCd running standalone as `jsexec -l ircd.js` on Windows, `REUSEADDR = 1` in `sockopts.ini`. After an oper's `/RESTART`, `terminate_everything()` closed the client and server-link sockets and exited; `jsexec -l` re-ran `ircd.js`, and `Open_PLines()` bound the ports again. `netstat` showed two `LISTENING` sockets per address on ports 6667 and 5050 in the one PID. Windows delivered new connections to the first run's orphaned listeners: clients connected but never received a greeting, and the linked peer server's connections piled up in `CLOSE_WAIT`. The event loop was running normally the whole time. See #1293 for the `SO_REUSEADDR` side of this; on Unix the second `bind()` fails with `EADDRINUSE` instead and the restarted daemon has no listener.
The IRCd has used `ListeningSocket` under JSexec since e1b902b718 (2019). Before that it used a plain `Socket` with `bind()`/`listen()`, whose `p->sock` is valid, so GC closed it and `/RESTART` under `jsexec -l` worked.
## Proposed fix
In `do_js_close()`, before the `INVALID_SOCKET` early return:
```c
if (p->set) {
xpms_destroy(p->set, sock_close_cb, NULL);
p->set = NULL;
}
```
and drop the now-redundant `free(p->set)` from `js_finalize_socket()` (or keep it guarded; `xpms_destroy()` frees the set). `js_accept()` and the event-registration code already treat `p->set == NULL` as "not a listener", so a closed `ListeningSocket` then behaves like a closed `Socket`.
Secondary, in `exec/load/ircd/core.js`: `Open_PLines()` discards the sockets it creates. Keep them and close them in `terminate_everything()` so a `/RESTART` releases the ports explicitly rather than depending on GC timing.
-- *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.38-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)