Skip to content

Gate remote SSH port scanning on the sidebar ports settings (#6123) - #6136

Merged
austinywang merged 13 commits into
mainfrom
issue-6123-remote-port-poll-broken-pipe
Jun 15, 2026
Merged

austinywang merged 13 commits into
mainfrom
issue-6123-remote-port-poll-broken-pipe

Conversation

@austinywang

@austinywang austinywang commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6123.

The bug

With remote SSH workspaces configured, cmux enters a bad control-plane state: cmux ping fails with Failed to write to socket (Broken pipe, errno 32), remote reconnect stops working until a full restart, cmux repeatedly spawns short-lived /usr/bin/ssh children, quit becomes slow, and (with the 1Password SSH agent) 1Password CPU spikes.

Crucially, the reporter set the documented sidebar visibility settings and it did not stop the backend scanning:

"sidebar": { "hideAllDetails": true, "showSSH": false, "showPorts": false }

Root cause (confirmed by inspection + the reporter's sample stacks)

RemoteSessionCoordinator drives remote listening-port discovery by spawning /usr/bin/ssh synchronously on the coordinator's serial queue from three places:

  • the DispatchSource poll timer (startRemotePortPollingLocked → pollRemotePortsLocked),
  • shell-activity scan bursts (kickRemotePortScanLocked → performRemotePortScanLocked → scanRemotePortsByPanelLocked), and
  • the bootstrap remote-TTY resolver (requestBootstrapRemoteTTYIfNeededLocked, used only to TTY-scope the scans).

The polling/scan decisions consulted only configuration.terminalStartupCommand and the tracked TTYs — never sidebar.showPorts / sidebar.hideAllDetails. So those settings only hid the display; the ssh scan loop kept running. With several remote hosts (the reporter reproduced with three) the serial queue is pinned on 8s ssh execs, which starves the control-socket path (the reporter's samples show TerminalController.spawnClientHandler alongside the …PortScan… → RemoteSessionProcessRunner.run → OS_dispatch_semaphore.wait stack) and makes quit wait on in-flight scans.

The fix

Gate every ssh-spawning port-discovery path on a queue-confined remotePortScanningEnabled flag, derived app-side from the ports-visibility settings — showPorts && !hideAllDetails, mirroring SidebarWorkspaceAuxiliaryDetailVisibility.resolved (the same precedence the sidebar uses). When ports are not displayed there is nothing for the scans to populate, so:

  • remotePortPollingModeLocked() returns nil when disabled → the poll timer never starts / is stopped.
  • kickRemotePortScanLocked and performRemotePortScanLocked short-circuit → no burst, no ssh.
  • requestBootstrapRemoteTTYIfNeededLocked / scheduleBootstrapRemoteTTYRetryLocked short-circuit → the bootstrap TTY resolver spawns no ssh either.
  • Disabling tears down the poll timer, in-flight burst, coalesce task, and the bootstrap-TTY retry, and clears detected ports. Enabling resumes polling, re-requests the bootstrap TTY, and re-arms a refresh burst as appropriate.

In the reporter's exact config (showPorts:false, hideAllDetails:true) the whole pathology is disabled: no ssh loop → no broken pipe, no 1Password churn, prompt quit, reconnect unblocked. Connection-establishing ssh (daemon bootstrap, reverse relay, proxy) is intentionally not gated — only the port-discovery loop is.

Wiring

  • Workspace.remotePortScanningEnabledFromSettings() reads the settings; configureRemoteConnection pushes the value before start(), so a fresh coordinator (and every reconnect) honors it immediately.
  • TabManager.sidebarMetadataSettingsDidChange() fans the value out to live remote sessions when settings change (gated to actual transitions).

Tests

New RemotePortScanGatingTests drives the queue-confined *Locked methods through a spy process runner (fully deterministic — no wall-clock waits): disabling stops the poll timer and spawns no ssh; a scan/kick is dropped; the bootstrap-TTY resolver spawns no ssh; toggling off tears down active polling and clears detected ports; re-enabling restarts polling; plus enabled-path sanity cases.

Two-commit red/green: commit 1 adds the suite + an inert setter (tests fail), commit 2 implements the gating (tests pass). The bootstrap-TTY gating + tests were added in a follow-up after autoreview flagged that path.

I also serialized the two real-subprocess suites (RemoteSessionProcessRunnerTests, RemotePlatformProbeScriptTests) against each other via a shared remoteSubprocessTestLock: they share the process-global fd table, and the new suite's parallel load exposed the documented cross-suite fd-recycling window. Verified green across repeated full-suite runs of swift test --package-path Packages/CmuxRemoteSession (31 tests).

Scope notes

  • Settings gating is the root-cause fix the issue calls for ("disabling those should stop the backend polling … or a separate setting"). I gated on the existing display settings rather than adding a new setting, so it works for the reporter's existing cmux.json with no new surface.
  • I deliberately did not refactor the coordinator's "blocking ssh execs run on the serial queue" design (explicitly by-design per the type's isolation docs, and a typing/concurrency-sensitive area). The gating eliminates the loop for the reporter's scenario; hardening the default-on case (so a busy scan can never starve the control socket) is a larger, riskier change better done separately.

Localization audit

No user-facing strings are added or changed. The fix gates existing backend behavior on existing settings; the settings.app.showPorts.subtitle help text remains accurate. No Resources/Localizable.xcstrings or web/messages/*.json keys changed, and web/data/cmux.schema.json is unchanged from base (an earlier description tweak was reverted to avoid adding unlocalized docs text).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Remote SSH port discovery is now fully gated by your sidebar “show ports” vs “hide details” setting. When hidden, scanning is suspended and any related port state is cleared (including bootstrap probing); when shown again, scanning restarts automatically.
  • Tests

    • Added/extended automated tests to verify enable/disable behavior (polling lifecycle, SSH spawn suppression, and port cleanup) and improved reliability by serializing subprocess-based test execution.

austinywang and others added 2 commits June 14, 2026 16:29
Remote SSH workspaces keep spawning short-lived `/usr/bin/ssh` port-scan
children even when the user disables the sidebar port/SSH detail rows
(`sidebar.showPorts=false`, `sidebar.hideAllDetails=true`). The scans run
synchronously on the coordinator's serial queue, which starves the cmux
control socket (`cmux ping` → Broken pipe) and slows quit.

This is the red half of the two-commit regression structure. It adds the
`remotePortScanningEnabled` flag plus an inert `updateRemotePortScanningEnabled`
setter (assignment only — the gating itself lands in the follow-up commit), and
the `RemotePortScanGatingTests` suite that drives the queue-confined port-scan
`*Locked` methods through a spy process runner. The four gating cases fail
because nothing consults the flag yet; the two enabled-path sanity cases pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remote-workspace port discovery ran unconditionally: `RemoteSessionCoordinator`
scheduled a `DispatchSource` poll timer and shell-activity scan bursts that each
spawned `/usr/bin/ssh` synchronously on the coordinator's serial queue,
regardless of the sidebar `showPorts`/`hideAllDetails` settings. The reporter
set `sidebar.showPorts=false` and `sidebar.hideAllDetails=true` (which only hid
the display) and still saw a tight `/usr/bin/ssh` respawn loop that starved the
cmux control socket (`cmux ping` → Broken pipe), wedged remote reconnect,
slowed quit (termination waited on in-flight 8s scans), and — with the
1Password SSH agent — spiked 1Password CPU.

Gate the whole ssh-spawning path on a `remotePortScanningEnabled` flag the app
derives from the ports-visibility settings (`showPorts && !hideAllDetails`,
mirroring `SidebarWorkspaceAuxiliaryDetailVisibility.resolved`): when ports are
not displayed there is nothing for the scans to populate, so polling and bursts
are suspended and no ssh is spawned. Disabling tears down the poll timer,
in-flight burst, and detected ports; enabling resumes polling (and re-arms one
TTY-scoped refresh when no fallback timer covers it).

Wiring:
- `Workspace.remotePortScanningEnabledFromSettings()` reads the settings;
  `configureRemoteConnection` pushes the value before `start()` so a fresh
  coordinator (and reconnects) honor it immediately.
- `TabManager.sidebarMetadataSettingsDidChange()` fans the value out to live
  remote sessions on settings changes, gated to actual transitions.
- Document the new backend effect on the `sidebar.showPorts`/`hideAllDetails`
  schema descriptions (the reporter configures these via cmux.json).

Tests: the `RemotePortScanGatingTests` gating cases now pass — disabling stops
the poll timer and spawns no ssh, a scan/kick is dropped, toggling off tears
down active polling, and re-enabling restarts it. Also serialize the two
real-subprocess test suites against each other via `remoteSubprocessTestLock`:
they share the process-global fd table, and the added gating suite's parallel
load exposed the documented cross-suite fd-recycling window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 15, 2026 3:32am
cmux-staging Building Building Preview, Comment Jun 15, 2026 3:32am

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a remotePortScanningEnabled boolean flag to RemoteSessionCoordinator and a public updateRemotePortScanningEnabled(_:) API that fully tears down or re-arms port scanning. All SSH-spawning scan, poll, burst, kick, and bootstrap TTY paths early-return when the flag is false. Enablement is derived from sidebar settings in Workspace and propagated through TabManager on every settings change. A global test subprocess lock is introduced and applied across existing process-runner test suites, and a new RemotePortScanGatingTests suite validates all gating behaviors.

Changes

Remote port scan gating by sidebar settings

Layer / File(s) Summary
remotePortScanningEnabled flag, public API, and scan/poll guards
Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift, RemoteSessionCoordinator+PortScan.swift, RemoteSessionCoordinator+BootstrapTTY.swift
Adds the remotePortScanningEnabled queue-confined flag and updateRemotePortScanningEnabled(_:) toggle API. On disable: cancels burst/coalesce tasks, clears scanned ports, stops polling, publishes a snapshot. On enable: re-arms polling and schedules a refresh burst. Guards kickRemotePortScanLocked, performRemotePortScanLocked, remotePortPollingModeLocked, and both bootstrap TTY methods with early-return checks.
Workspace and TabManager settings wiring
Sources/Workspace.swift, Sources/TabManager.swift
Adds Workspace.remotePortScanningEnabledFromSettings(defaults:) reading sidebar.showPorts/hideAllDetails, and Workspace.applyRemotePortScanningEnabled(_:) forwarding to remoteSessionController. configureRemoteConnection applies the derived flag immediately. TabManager.refreshRemotePortScanningEnablement() caches the last-propagated value and fans out changes to all live remote workspace tabs on every sidebar settings change.
Subprocess test serialization lock and existing test updates
Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSubprocessTestLock.swift, RemoteSessionProcessRunnerTests.swift, RemotePlatformProbeScriptTests.swift, RemoteReconnectPolicyTests.swift
Introduces a global NSLock (remoteSubprocessTestLock) for serializing real-subprocess tests across multiple suites to prevent fd-recycling cross-wiring. Applies lock/defer-unlock to all five process-runner test cases, wraps the runProcess helper in platform-probe tests, marks reachability-probe tests as serialized, and wraps endpoint-resolution tests with explicit lock/defer-unlock.
RemotePortScanGatingTests suite, harness, and stubs
Packages/CmuxRemoteSession/Package.swift, Tests/CmuxRemoteSessionTests/RemotePortScanGatingTests.swift
Expands test target dependencies to CmuxCore, CmuxRemoteDaemon, CmuxRemoteWorkspace. Adds RemotePortScanGatingTests covering: poll timer teardown/restart, SSH suppression/allowance, burst-kick scheduling, port clearing, and bootstrap TTY resolution gating. Includes makeCoordinator harness and stubs: SpyProcessRunner, NoopRemoteSessionHost, UnusedRemoteProxyBroker, NoopReachabilityProbe, PassthroughRelayCommandRewriter, StubBuildInfo.

Sequence Diagram(s)

sequenceDiagram
    participant Settings as UserDefaults
    participant TabManager
    participant Workspace
    participant RemoteSessionCoordinator

    rect rgba(100, 149, 237, 0.5)
        Note over TabManager: sidebarMetadataSettingsDidChange()
        TabManager->>Settings: read showPorts, hideAllDetails
        Settings-->>TabManager: values
        TabManager->>TabManager: refreshRemotePortScanningEnablement()
        alt value unchanged
            TabManager-->>TabManager: early return
        else value changed
            TabManager->>Workspace: applyRemotePortScanningEnabled(enabled)
            Workspace->>RemoteSessionCoordinator: updateRemotePortScanningEnabled(enabled)
        end
    end

    rect rgba(255, 165, 0, 0.5)
        Note over RemoteSessionCoordinator: updateRemotePortScanningEnabled(false)
        RemoteSessionCoordinator->>RemoteSessionCoordinator: cancel burst/coalesce tasks
        RemoteSessionCoordinator->>RemoteSessionCoordinator: cancel bootstrap TTY retry
        RemoteSessionCoordinator->>RemoteSessionCoordinator: clear scanned ports, stop polling
        RemoteSessionCoordinator->>RemoteSessionCoordinator: publish empty ports snapshot
    end

    rect rgba(60, 179, 113, 0.5)
        Note over RemoteSessionCoordinator: updateRemotePortScanningEnabled(true)
        RemoteSessionCoordinator->>RemoteSessionCoordinator: re-arm polling state
        RemoteSessionCoordinator->>RemoteSessionCoordinator: request bootstrap TTY or schedule refresh burst
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#5767: Adds "suspended" connection UI state and reconnect affordance handling in Workspace.swift and ContentView.swift, complementing this PR's gating of remote port scanning during inactive remote sessions.

Poem

🐇 When ports are hidden, no SSH shall roam,
The flag goes false and scanning stays home.
With guards in place on each kick and poll,
The bootstrap TTY takes a quiet stroll.
A spy runner counts every call made right—
Now ports only scan when they're in sight! 🔍

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: gating remote SSH port scanning based on sidebar ports visibility settings, directly addressing issue #6123.
Description check ✅ Passed The description covers summary (what changed and why), testing approach with deterministic test suite, and implementation details. However, it lacks a demo video section and the testing section could be more explicit about manual verification steps.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #6123: gates port scanning on sidebar settings via remotePortScanningEnabled flag, stops SSH spawning when disabled, prevents control-plane starving, implements settings integration at Workspace and TabManager levels, and adds deterministic test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to gating port scanning on sidebar settings and test serialization. No unrelated refactoring or functionality drift. Test lock serialization addresses fd-recycling issues directly caused by new parallel test suite load.
Cmux Swift Actor Isolation ✅ Passed All production Swift changes maintain proper actor isolation: @MainActor methods in Workspace and TabManager inherit isolation from their classes; RemoteSessionCoordinator's queue-confined flag and...
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking/timing-based synchronization in production Swift. Gating logic uses queue-confined flag with guard early-returns. NSLock usage limited to test-only scaffolding for fd-...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous operations (RestorableAgentSessionIndex.load(), sysctl, or disk I/O) added to main actor or interactive paths. New methods only perform lightweight UserDefaults reads and c...
Cmux Cache Substitution Correctness ✅ Passed lastRemotePortScanningEnabled is an event-driven deduplication cache that always reads fresh from UserDefaults; never persists, returns cached-only values, or appears in persistence/undo/snapshot p...
Cmux No Hacky Sleeps ✅ Passed The rule explicitly excludes Swift; scope is TypeScript, JavaScript, shell, and non-Swift build/runtime scripts. This PR contains only .swift files, so the rule does not apply.
Cmux Algorithmic Complexity ✅ Passed All production code changes comply with algorithmic complexity rules. The PR's main loop (TabManager.refreshRemotePortScanningEnablement) is a single-pass O(n) over workspaces with O(1) per-item wo...
Cmux Swift Concurrency ✅ Passed The PR uses only established queue-confined patterns (queue.async with _Locked methods) matching existing APIs like start() and updateRemoteRelayIDAliases(). No new Dispatch queues, Combine state,...
Cmux Swift @Concurrent ✅ Passed All new methods are synchronous, properly dispatcher-based, and follow correct isolation patterns: static settings reader (pure sync helper), @MainActor dispatchers calling queue-confined coordinat...
Cmux Swift File And Package Boundaries ✅ Passed PR respects Swift file and package boundaries: only 21 lines added to oversized Workspace.swift (under 250-line threshold), test code properly excluded, CmuxRemoteSession package logic separated by...
Cmux Swift Logging ✅ Passed All production code changes have no logging calls; existing NSLog statements in Workspace.swift/TabManager.swift are properly guarded with #if DEBUG.
Cmux User-Facing Error Privacy ✅ Passed PR introduces "remote port scan failed" error strings only in debug-only logging (debugLog wrapped in #if DEBUG), never shown to users. No localization keys added, no UI alerts, no user-facing chan...
Cmux Full Internationalization ✅ Passed PR adds no user-facing text, string catalog entries, or web UI changes; gates backend port-scanning on existing sidebar visibility settings without adding localization requirements.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI state layout violations. Changes to TabManager and Workspace add only private state (lastRemotePortScanningEnabled) and simple wiring methods called from notification callbac...
Cmux Architecture Rethink ✅ Passed PR is a correctness fix addressing issue #6123 root cause with clear single source of truth (UserDefaults), proper derivation pattern, no timing repairs, and properly scoped test synchronization.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR makes no changes to NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup. All changes are backend SSH port scanning behavior gating: a new remotePortScanningEnabled flag in Remo...
Cmux Source Artifacts ✅ Passed All changed paths (11 files) are intentional Swift source code, test suites, or package manifests. No source-control artifacts (logs, build output, caches, DerivedData, screenshots, temp files, or...

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6123-remote-port-poll-broken-pipe

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

austinywang and others added 2 commits June 14, 2026 16:47
The port-scan gating + app wiring grew four files past their recorded
budget (RemoteSessionCoordinator+PortScan, Workspace, TabManager,
RemoteSessionCoordinator). Refresh the budget to accept the small,
necessary growth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/data/cmux.schema.json`:
- Around line 656-660: Replace the hard-coded English description strings in the
hideAllDetails and showPorts properties with descriptionKey routing keys (follow
the existing pattern used by other properties in the file). Then add the
corresponding translated message entries for each descriptionKey in all 21
locale files in web/messages/ (en, ja, ar, bs, da, de, es, fr, it, km, ko, no,
pl, pt-BR, ru, th, tr, uk, zh-CN, zh-TW) to enable proper i18n localization.
Ensure the descriptionKey names are consistent between the schema and the locale
files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a9d23bb3-6681-45eb-8b23-ebd9bd869fcc

📥 Commits

Reviewing files that changed from the base of the PR and between d6fb91d and d94002e.

📒 Files selected for processing (10)
  • Packages/CmuxRemoteSession/Package.swift
  • Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
  • Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
  • Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePlatformProbeScriptTests.swift
  • Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePortScanGatingTests.swift
  • Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift
  • Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSubprocessTestLock.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • web/data/cmux.schema.json

Comment thread web/data/cmux.schema.json
Autoreview caught an ssh-spawning path the first cut missed: the bootstrap
remote-TTY resolver (`requestBootstrapRemoteTTYIfNeededLocked` and its bounded
retry) reads `~/.cmux/relay/<port>.tty` over ssh and runs from
`beginConnectionAttemptLocked` and the proxy-ready path regardless of the
port-scan flag. Since that TTY is resolved *only* to TTY-scope the port scans
(`applyBootstrapRemoteTTY` → `syncRemotePortScanTTYs` + `kickRemotePortScan`),
it must respect the same gate — otherwise a remote workspace created while
`sidebar.showPorts` is false (or `hideAllDetails` is true), or one with a
pending retry when toggled off, still spawns `/usr/bin/ssh`.

Gate the resolver and its retry on `remotePortScanningEnabled`, cancel any
in-flight retry when scanning is suspended, and re-request the bootstrap TTY
when scanning is re-enabled (covering the no-TTY-yet case, alongside the
existing refresh-burst for the TTY-known case). Adds two coordinator tests:
disabling spawns no resolver ssh; the enabled path spawns one (sanity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a control-plane pathology (broken pipe, 1Password CPU spikes, slow quit) caused by RemoteSessionCoordinator unconditionally spawning /usr/bin/ssh for port discovery even when the user had configured sidebar.showPorts: false or sidebar.hideAllDetails: true. The sidebar settings previously suppressed only the display, not the underlying scan loop.

  • Adds a remotePortScanningEnabled flag (queue-confined) whose disabling cascades through suspendRemotePortScanningLocked() to tear down the poll timer, cancel any in-flight burst/coalesce chain, cancel the bootstrap-TTY retry, and clear published port state; re-enabling resumes polling and re-arms the TTY resolver.
  • Wires the flag into all three ssh-spawning paths: kickRemotePortScanLocked, performRemotePortScanLocked, remotePortPollingModeLocked, and requestBootstrapRemoteTTYIfNeededLocked / scheduleBootstrapRemoteTTYRetryLocked.
  • Pushes the value at coordinator creation (before start()) via configureRemoteConnection, and fans it out to live sessions via TabManager.sidebarMetadataSettingsDidChange on settings transitions.

Confidence Score: 5/5

Safe to merge — the gating is applied consistently across all three ssh-spawning paths and the serial-queue ordering guarantees the flag is set before start() executes.

The flag is derived from the same precedence rule the sidebar already uses (hideAllDetails wins), tear-down on disable is comprehensive (burst, coalesce chain, bootstrap-TTY retry, poll timer, published ports), and re-enable correctly resumes polling and re-requests the bootstrap TTY. Stale burst steps are dropped by the existing generation guard, and the UserDefaults.didChangeNotification firehose is debounced to actual transitions. The deterministic test suite exercises every guarded code path with no wall-clock waits.

No files require special attention.

Important Files Changed

Filename Overview
Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift Core gating logic: adds updateRemotePortScanningEnabled, updateRemotePortScanningEnabledLocked, and suspendRemotePortScanningLocked; guards kickRemotePortScanLocked, performRemotePortScanLocked, and remotePortPollingModeLocked on the flag. State teardown on disable mirrors the proxy-error teardown path and is comprehensive.
Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+BootstrapTTY.swift Adds guard remotePortScanningEnabled at the top of both requestBootstrapRemoteTTYIfNeededLocked and scheduleBootstrapRemoteTTYRetryLocked, closing the third ssh-spawning path.
Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift Adds the remotePortScanningEnabled queue-confined var (default true), documented with the correct derivation rule.
Sources/Workspace.swift Adds remotePortScanningEnabledFromSettings() with correct hideAllDetails-wins precedence and applyRemotePortScanningEnabled; pushes the value before controller.start() in configureRemoteConnection.
Sources/TabManager.swift Fans the current ports-visibility value to all live remote sessions on settings changes; lastRemotePortScanningEnabled debounces the UserDefaults.didChangeNotification firehose to actual transitions.
Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePortScanGatingTests.swift New deterministic test suite driving *Locked methods via queue.sync with a SpyProcessRunner; covers disable/enable/toggle/bootstrap-TTY/kick paths with no wall-clock waits.
Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSubprocessTestLock.swift Introduces a process-global NSLock to serialize cross-suite fd-table access; used uniformly with lock()/defer unlock() in all three real-subprocess suites.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Settings change"] --> B["TabManager.sidebarMetadataSettingsDidChange()"]
    B --> C["refreshRemotePortScanningEnablement()"]
    C --> D{"Value changed?"}
    D -- No --> E["No-op"]
    D -- Yes --> F["applyRemotePortScanningEnabled(enabled)"]
    F --> G["updateRemotePortScanningEnabled (queue.async)"]
    G --> H{"enabled?"}
    H -- false --> K["suspendRemotePortScanningLocked()"]
    H -- true --> L["updateRemotePortPollingStateLocked()"]
    L --> N{"TTYs known?"}
    N -- No --> O["requestBootstrapRemoteTTYIfNeededLocked()"]
    N -- Yes --> P["scheduleRemotePortScanCoalesceLocked()"]
Loading

Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines 21 to +25
@Test("Capture survives the pipe read handles being torn down mid-run")
func captureSurvivesPipeReadHandleTeardown() throws {
// Serialize against the platform-probe suite; see ``remoteSubprocessTestLock``.
remoteSubprocessTestLock.lock()
defer { remoteSubprocessTestLock.unlock() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 RemoteSessionProcessRunnerTests uses direct lock()/unlock() while RemotePlatformProbeScriptTests uses the withRemoteSubprocessTestLock helper introduced in the same PR. Both patterns are safe (the defer ensures unlock), but using the helper everywhere would make the serialization intent uniform and reduce the chance of a future test accidentally omitting the defer.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — unified both suites on the direct lock() / defer unlock() idiom and removed the helper (e1b8672), so the serialization intent is consistent.

— Claude Code

austinywang and others added 5 commits June 14, 2026 17:13
Autoreview flagged that extending the raw English `description` for
sidebar.showPorts/hideAllDetails renders untranslated on non-English
/docs/configuration pages (the page falls back to property.description when
there is no descriptionKey). The schema note was a non-essential nicety; revert
it so the PR adds no unlocalized user-facing text. The settings' behavior is
documented in the PR/commit messages, and a properly localized docs note can
be a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview flagged that the enabled bootstrap-TTY sanity test could race its
own retry: the spy returned empty stdout, so requestBootstrapRemoteTTYIfNeeded
treated the TTY as unresolved and scheduled the 0.5s bootstrap retry. The test
asserted runCount == 1 before cancelling it, so a >0.5s thread deschedule under
CI load could let the retry fire and bump the count to 2.

Return a valid TTY ("ttys005") so resolution succeeds on the first pass and
schedules no retry, and assert the resolved flag — the exact run count can no
longer race a delayed retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Greptile flagged that RemotePlatformProbeScriptTests used the
withRemoteSubprocessTestLock helper while RemoteSessionProcessRunnerTests used
direct lock()/defer unlock(). Unify on the direct lock()/defer idiom in both
suites and drop the helper, so the serialization intent is uniform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6123)

Autoreview noted the fd-table serialization missed a third real-subprocess
suite: RemoteHostReachabilityProbeTests resolveEndpoint cases shell out to
/usr/bin/ssh -G with Process/Pipe in the same target, so they can still race
captureSurvivesPipeReadHandleTeardown's mid-run fd teardown. Mark the suite
.serialized (matching the other two real-IO suites) and take
remoteSubprocessTestLock around the two synchronous ssh -G tests. The async
probeTCP cases use sockets (a recycled socket fd only delays, never corrupts,
the pipe-capture reader) and cannot hold the NSLock across their await, so they
rely on the suite-level .serialized ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview noted that suspendRemotePortScanningLocked() cancelled tasks and
cleared published ports but left two pieces of hidden scanner bookkeeping
intact: remotePortPollBaselinePorts and bootstrapRemoteTTYRetryCount. After a
disable/re-enable, host-wide-delta polling could subtract against a pre-disable
baseline (surfacing ports that appeared while ports were hidden), and an
exhausted bootstrap TTY retry budget would not restart because the schedule
guard still saw the old count. Reset both on suspend so re-enabling behaves like
a fresh scanner start (matching the updateRemotePortPollingStateLocked teardown
and stopAllLocked resets). Adds a toggle-off regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t-poll-broken-pipe

# Conflicts:
#	.github/swift-file-length-budget.tsv
@austinywang
austinywang merged commit e8c137e into main Jun 15, 2026
33 of 34 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — d1a04de5 Deployed Jun 15, 2026 by vercel[bot]
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.

Remote SSH port polling causes Broken pipe on cmux socket across stable and nightly

1 participant