Skip to content

fix(services): fall back to ss and netstat when lsof is absent - #10459

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.50from
aron-intframe:fix/port-pid-fallback-probes
Aug 15, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.50from
aron-intframe:fix/port-pid-fallback-probes

Conversation

@aron-intframe

@aron-intframe aron-intframe commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #10431.

resolvePortPid() only shells out to lsof. No lsof means spawn raises ENOENT, on("error") turns it into null, and now the caller can't tell "nothing holds this port" from "I had no way to look" — so a service adopted after a supervisor restart keeps pid: null forever. That's the state the adopt-branch test at ServiceSupervisor.test.ts:246 exists to prevent.

Now it tries lsof, then ss, then netstat, sharing one deadline so the whole lookup still costs at most PID_RESOLVE_TIMEOUT_MS instead of three times it.

The output parsing for each is a pure exported function (parseLsofPid, parseSsPid, parseNetstatPid) so the formats are testable on a box that doesn't have the binary. netstat has no port filter, so its parser matches the local-address column rather than scanning the line — otherwise a foreign address ending in the same number reads as a listener. There's a test for that row.

Validation (Hard Rule #18, TDD)

This box has lsof, so I emulated the reported environment by pointing PATH at a dir holding only ss and netstat. Same script, same host, only portProbe.ts swapped:

before:  real=2027683  resolved=null       <- bug
after:   real=2027730  resolved=2027730

That's now a permanent test (resolvePortPid still resolves a pid on a host without lsof). It skips rather than passing vacuously if neither fallback is installed, and it asserts lsof is genuinely unreachable on the shimmed PATH first so it can't pass for the wrong reason.

node --import tsx/esm --test tests/unit/services/portProbePid.test.ts   →  9 pass, 0 fail
npx eslint src/lib/services/portProbe.ts tests/.../portProbePid.test.ts →  clean

Pre-commit ran clean as well (prettier, eslint with suppressions, check-docs-sync, check:any-budget:t11, check:tracked-artifacts).

One pre-existing failure worth disclosing

tests/unit/services/ServiceSupervisor.test.ts is 6 passed / 1 failed here, on adopted service resolves and records the real pid of the process holding the port ('error' !== 'running'). It fails the same way on a pristine release/v3.8.50 tree with my files removed, so it isn't mine — but it's the neighbouring test, so I'd rather say it than have it look like I didn't run it.

Environment: Node 22.14.0 (engines want >=22.19.0; better-sqlite3 warns EBADENGINE on it). lsof, ss and netstat are all present here, which is why the fallback had to be exercised through a shimmed PATH rather than by uninstalling something.

resolvePortPid shelled out to lsof alone. On a host without it, spawn
raises ENOENT, the error handler turned that into null, and the caller
could not tell 'nothing holds this port' from 'I have no way to look' -
so a service adopted on a supervisor restart kept pid: null forever,
silently, which is the regression the adopt-branch test guards against.

Probes lsof, then ss, then netstat, sharing one deadline so the whole
lookup still costs at most PID_RESOLVE_TIMEOUT_MS. Output parsing for
each is a pure exported function so the formats are unit-testable
without the binary being installed.

netstat cannot filter by port, so its parser matches the local-address
column rather than scanning the line, keeping a foreign address that
ends in the same number from being read as a listener.
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @aron-intframe — the lsof→ss→netstat probe chain fixes the silent pid: null on hosts without lsof, and the parser isolation with shared-deadline budget is clean. The no-lsof PATH-shim test is exactly the regression guard this needed. Note red CI is an inherited base-red, not this change. 🚀

@diegosouzapw
diegosouzapw merged commit 972c459 into cheaper-inference:release/v3.8.50 Aug 15, 2026
4 of 5 checks passed
@aron-intframe

Copy link
Copy Markdown
Contributor Author

Thanks for the read, and sorry for the slow reply.

One thing worth leaving here for whoever touches resolvePortPid next: the chain still returns null when all three probes are unavailable, so "nothing is listening on that port" and "this host cannot tell me" remain the same value. That is the distinction the original pid: null bug turned on, and the PATH shim only proves the lsof-missing case, where ss or netstat still answers. On minimal images — distroless, scratch-based sidecars — none of the three is present, so the branch is reachable in production, not just in the test. If it ever needs to be actionable, the honest shape is a third state at the call site rather than a null the caller has to guess at.

Your confirming the red CI as inherited was the useful half of the review for us, by the way. We had it in the PR body as a claim about someone else's tree; you turned it into a fact.

tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…er-inference#10459)

resolvePortPid shelled out to lsof alone. On a host without it, spawn
raises ENOENT, the error handler turned that into null, and the caller
could not tell 'nothing holds this port' from 'I have no way to look' -
so a service adopted on a supervisor restart kept pid: null forever,
silently, which is the regression the adopt-branch test guards against.

Probes lsof, then ss, then netstat, sharing one deadline so the whole
lookup still costs at most PID_RESOLVE_TIMEOUT_MS. Output parsing for
each is a pure exported function so the formats are unit-testable
without the binary being installed.

netstat cannot filter by port, so its parser matches the local-address
column rather than scanning the line, keeping a foreign address that
ends in the same number from being read as a listener.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…er-inference#10459)

resolvePortPid shelled out to lsof alone. On a host without it, spawn
raises ENOENT, the error handler turned that into null, and the caller
could not tell 'nothing holds this port' from 'I have no way to look' -
so a service adopted on a supervisor restart kept pid: null forever,
silently, which is the regression the adopt-branch test guards against.

Probes lsof, then ss, then netstat, sharing one deadline so the whole
lookup still costs at most PID_RESOLVE_TIMEOUT_MS. Output parsing for
each is a pure exported function so the formats are unit-testable
without the binary being installed.

netstat cannot filter by port, so its parser matches the local-address
column rather than scanning the line, keeping a foreign address that
ends in the same number from being read as a listener.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(services): resolvePortPid() silently returns null when lsof is absent — adopted services lose pid tracking

2 participants