Skip to main content
  • Version 0.1.0 · Purity server-only (shells out via node:child_process)
  • Purpose Answer “which process is listening on this port?” on POSIX and Windows, safely enough that the answer can be handed to process.kill.
  • Workspace deps none
  • External deps none (Node built-ins only)
Server-only, and deliberately so: it spawns lsof (POSIX) or netstat (Windows). It exists because two callers that cannot import each other need the same answer — the clawboo CLI’s stop / restart and the dashboard server’s managed-Gateway control (stopGateway, POST /api/system/gateway). Each carried its own copy until the copies started to matter.
Both callers feed the result to process.kill, so a wrong answer force-kills an innocent process. Treat the three hardening details below as load-bearing, not incidental, and keep the parsers’ test coverage if you change them.

Public API

All exports come from the single . barrel (src/index.ts). No subpath exports.

Functions

Types

The three details that matter

  1. -sTCP:LISTEN on POSIX. The command is lsof -nP -iTCP:<port> -sTCP:LISTEN -t. Plain lsof -i :PORT also matches connected sockets, so a browser tab open on the dashboard appears in that output and taking the first line can return the browser’s PID. -n / -P additionally skip DNS and /etc/services lookups, because a stalled resolver would block this synchronous call.
  2. A raised maxBuffer for netstat -ano. Node’s 1 MB default throws ENOBUFS on a host with thousands of connections, and the surrounding catch would silently turn that into “no process found”.
  3. A locale-independent listener fallback. netstat.exe translates its state column (ABHÖREN on German Windows, À L'ÉCOUTE on French), so an English-only LISTENING match finds nothing there. When no LISTENING row matches, a row whose local address is the port and whose foreign address is the all-zero placeholder (0.0.0.0:0 / [::]:0) is accepted — the structural signature of a listening socket, which no locale translates. The PID is read as the last column, which survives a localized state string containing a space.

Why a package rather than a copy in each app

apps/cli and apps/web/server are separate build targets and neither may import the other, so shared logic belongs in a @clawboo/* package. That matters more here than for most shared code: a fix applied to one copy and not the other is a bug that only manifests on one surface, and the failure mode is force-killing the wrong process.

Testing

src/__tests__/processLookup.test.ts covers the parsers against realistic netstat -ano and lsof -t fixtures, including a localized (ABHÖREN) row, a space-bearing French state string, IPv6 local addresses, CRLF output, UDP rows on the same port, prefix collisions (:187890 must not match :18789), and a synthetic LISTENING row whose foreign address is the target port, which is the only shape that isolates the local-address column check. findListenerPid is unit-tested too, through the exported LookupDeps seam (an injected run plus platform): the POSIX lsof -nP -iTCP:<port> -sTCP:LISTEN -t argv, the Windows netstat -ano path, and the tool-absent / non-zero-exit case, all without spawning anything. The CLI’s stopDashboard tests (apps/cli/src/__tests__/lifecycle.test.ts) stub the lookup out and cover the SIGTERM / poll / SIGKILL escalation instead.

See also

Last modified on August 8, 2026