Read the port scan's process table from the kernel instead of ps - #15353
Conversation
The batched port scan ran `lsof -nP -a -p <pids> -iTCP -sTCP:LISTEN -Fpn` every two seconds. Before lsof answers it calls close() on every descriptor number up to `kern.maxfilesperproc`, which is 138,240 on current macOS, and it forks a child that repeats the sweep. That is far more work than the lookup itself, and it repeats for the life of the app. `ListeningPortLookup` asks the kernel directly through libproc (PROC_PIDLISTFDS + PROC_PIDFDSOCKETINFO) and keeps only TCP sockets in TSI_S_LISTEN. Measured against lsof over 483 PIDs the results are identical, and one scan drops from 200ms to under 7ms. Completeness semantics are unchanged: a PID we may not read is a miss only when its identity is also unreadable while it is still present, so a panel behind the root `login` process can still retire its ports.
Remote port scanning already follows `sidebar.showPorts` (issue #6123), but the local scanner kept running its two-second sweep no matter what. Nothing displays or reports the result while the detail is hidden, so the work is pure cost. `SidebarWorkspaceDetailDefaults.portScanningEnabled` now holds the one rule both paths read, and `TabManager` pushes changes to the local scanner alongside the remote sessions it already updates. Note this also blanks `listeningPorts` for local workspaces in the control socket and CLI summaries while ports are hidden, which is the same trade remote workspaces already make.
Resolves conflicts with the cancellable burst lifecycle (#11109) and the lsof PID chunking that came with it: the kernel lookup has no argv, so the chunking and its test go away, and PortLsofScanResult keeps its new name PortListenerScanResult at the call sites main added. The end-to-end retirement tests keep main's compressed burst schedule and its late-burst stop, now triggered from the fifth listener lookup instead of the fifth lsof call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hiding the ports detail now also invalidates a burst that is already running, the same way unregistering the last panel does, so its remaining timers never scan. Showing it again rescans every registered panel once, since ports that opened or closed while hidden were never seen and an idle panel would otherwise keep a stale list until its next command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Hiding the ports detail now publishes no ports for every tracked panel and agent workspace, so the socket, CLI, custom sidebars and the command palette (which read published ports without the sidebar's visibility gate) don't keep a list frozen at the moment scanning stopped. A fresh agent request ID fences off any scan still in flight. - A queued follow-up panel or agent scan no longer runs after scanning is turned off; a dropped agent request clears its in-flight mark. - PortScannerPublicationTests fed ports through a fake lsof, which the scanner no longer calls. Both runners now supply ports through listeningPortsProvider, and the churn test counts per-PID lookups instead of lsof argv lists. - Test for the cleared list; normalize the pbxproj; drop stale lsof wording from two doc comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes the CI compile error in the previous commit: remove(keys:) takes a Set, not an Array. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 5 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (14)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Cross-model review (Codex gpt-5.6-sol)
|
|
Review: correctness pass by a review subagent on a0090bc (this PR's own commit; the #10277 commits underneath were reviewed there). No correctness or compile blockers found. Checked: Fixed (491e0ab):
Left:
|
|
Review: The follow-up review checked process-list truncation semantics and scan concurrency documentation. Fixed: DarwinProcessListing.isTruncated is read-only and initialized explicitly, and the KernelPortProcessTable concurrency note now accurately describes up to four panel enumerations and overlap with an agent scan. Left: No known review issues. |
PortScanner still spawned /bin/ps for every panel scan (ps -t) and up to twice per agent scan (ps -ax), which repeats every two seconds while an agent runs. Each of those execs is a fork that endpoint security tools log. The scanner now asks the kernel directly and spawns no subprocess. KernelPortProcessTable answers both questions with one DarwinProcessEnumerator capture per call: terminal processes by matching e_tdev against a stat of each requested /dev entry, and parents from pbi_ppid. A terminal that no longer exists is dropped as authoritative emptiness, replacing the ps vanished-TTY retry loop; one that exists but cannot be inspected makes the answer incomplete. PortScanner takes the reader through a PortProcessTableReading seam instead of CommandRunning. DarwinProcessListing gains isTruncated so callers can tell a truncated PID list from a listed PID that exited before it was read. Only the first leaves a port scan incomplete. The ps output parsing tests are replaced by KernelPortProcessTable tests through its seams, and the PortScanner fixtures return process mappings directly. Co-authored-by: Trevor Burnham <trevorburnham@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DarwinProcessListing.isTruncated is now a let set through an explicit internal initializer that defaults it, so hand-built listings still compile and consumers can't flip it. The KernelPortProcessTable doc comment now says a panel scan makes up to four enumerations and can overlap an agent scan, instead of claiming scans never overlap. Co-authored-by: Trevor Burnham <trevorburnham@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
e9bf799 to
d55a345
Compare
|
/catch-up |
|
Caught Resolved:
This push used the Actions token, so CI will not start on its own. Push any commit (or close and reopen) to get checks on the new head. Catch-up run · RFC #14631 |
|
Heads up, I ran The head is now Second, do not read the current green as a verified head. The rollup says SUCCESS but there are only 7 contexts on Catch-up resolved Not taking this PR over, it is clearly yours and moving. I only unblocked the conflict. — Raindrop g2 🫧 / Run: run_worker_20260930_3fc64ba6 |
The automated catch-up merge did not emit the normal pull-request check suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI failure attributionCI stopped on
Not re-run automatically: Written by |
Dogfood tours of
|
Review: independent correctness pass on
|
Resolve the port scanner and project conflicts while retaining the kernel process-table implementation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reconcile cmuxTests target membership introduced by the main catch-up merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
|
Keep main project entries while restoring the kernel port scanner sources and tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Catch up the ready PR to the current main tip without changing the kernel scanner implementation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Merge receipt for
Labeled |
#15353 rewrote the late-burst test's fixture again, now reading the process table from the kernel too, and kept the fifth-lookup trigger and so its race. cmuxTests/PortScannerTests.swift resolves to main's version with this branch's change ported onto it: the per-lookup hook perform(atListenerLookup:stopsListening:_:), a kick from inside the second port lookup, and the lookup count in the failure message.
Summary
The local port scanner still spawned
/bin/ps:ps -t <ttys>for each panel scan (twice per scan) andps -axup to twice per agent scan. Agent scans repeat every 2 s while an agent runs and panel scans burst six times over 10 s after every shell command, so cmux forkedpsdozens of times a minute, and endpoint security tools log every one of those execs. After this change the scanner spawns no subprocess at all. Together with #10277 (which replacedlsof), an idle agent goes from about 3 execs every 2 s to none.KernelPortProcessTableanswers both questions in process with the existingDarwinProcessEnumerator(the sameproc_listallpids+proc_pidinfo/sysctlreader the Task Manager uses), so there is no new enumerator:stateach requested/dev/<tty>and matchproc_bsdinfo.e_tdev. A terminal that no longer exists is authoritative emptiness (this replaces thepsvanished-TTY retry loop); one that exists but can't be inspected makes the answer incomplete.pbi_ppidfor every process, for agent process-tree expansion.Completeness follows the PID list:
DarwinProcessListinggainsisTruncated, so a PID that exited between listing and reading (which can hold no port and parent nothing) no longer counts as missing evidence, while a truncated or unreadable list still does. PortScanner takes the reader through aPortProcessTableReadingseam instead ofCommandRunning.This re-lands the
pshalf of #3801 by @TrevorBurnham on the current layout, reusing the codebase's process enumerator instead of adding a newsysctl(KERN_PROC_ALL)copy. Commits carry hisCo-authored-by.Stacked on #10277; until that merges this diff also shows its commits.
Testing
python3 scripts/verify-local.pypasses (Swift syntax, pbxproj, test wiring, package groups). Nothing was compiled locally; CI compiles and runs the tests.KernelPortProcessTableTestscover the seams: a vanished TTY does not discard surviving panels, every TTY vanished is complete emptiness, an unreadable device is incomplete,/dev/ttysNNNnames map back to the canonical name, truncated or empty listings are incomplete, a PID missing between list and read stays complete, NODEV processes are ignored, plus live checks against this test process and/dev/null.psoutput now return process mappings directly; thepsoutput-parsing tests (malformed rows, stderr diagnostics, retry budget, timeout) are removed with the code they tested.Changelog
Changed: The sidebar port scan no longer spawns
psorlsof; it reads processes and listening ports directly from the kernelChecklist
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Replaces the port scanner's
psandlsofcalls with direct kernel reads, so a scan spawns no subprocesses. Panel scans previously forkedps -ttwice each, agent scans forkedps -axup to twice every two seconds, and the listening-port lookup forkedlsof; endpoint security tools log every exec. An idle agent goes from several forks every two seconds to none.KernelPortProcessTableanswers process questions through the existingDarwinProcessEnumerator: terminal processes by matchingproc_bsdinfo.e_tdevagainst astatof each requested/dev/<tty>, parents frompbi_ppid. A vanished terminal is authoritative emptiness (replacing the oldpsretry loop); one that exists but can't be inspected makes the scan incomplete.ListeningPortLookupreads each PID's listening TCP sockets from the kernel through libproc, cutting a scan from ~200ms to under 7ms.PortScannernow takes both readers through seams instead ofCommandRunning.DarwinProcessListinggainsisTruncated, so a PID that exits between listing and reading no longer counts as missing evidence; only a truncated or unreadable PID list does.Written for commit 65f437c. Summary will update on new commits.