Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
16698 Sources/ContentView.swift
14785 Sources/TerminalController.swift
13358 Sources/Panels/BrowserPanel.swift
12292 Sources/Workspace.swift
12313 Sources/Workspace.swift
12088 Sources/GhosttyTerminalView.swift
12046 cmuxTests/AppDelegateShortcutRoutingTests.swift
9331 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
Expand All @@ -16,7 +16,7 @@
6363 cmuxTests/GhosttyConfigTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6153 CLI/cmux_open.swift
6096 Sources/TabManager.swift
6116 Sources/TabManager.swift
6074 Sources/TextBoxInput.swift
5925 cmuxTests/TerminalAndGhosttyTests.swift
5526 cmuxTests/BrowserConfigTests.swift
Expand Down Expand Up @@ -139,10 +139,10 @@
680 Sources/FileExplorerSearchController.swift
677 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
668 cmuxTests/FeedCoordinatorTests.swift
655 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
654 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
650 Sources/Panels/MarkdownRemoteImageLoader.swift
649 Sources/CmuxTopSnapshot.swift
648 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
641 cmuxTests/CommandPaletteNucleoFFITests.swift
630 Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
621 cmuxTests/FinderFileDropRegressionTests.swift
Expand All @@ -159,6 +159,7 @@
594 Sources/SessionIndexModels.swift
594 cmuxTests/PortalTabDragRoutingTests.swift
588 cmuxTests/CommandPaletteShortcutCustomizationTests.swift
586 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
586 Sources/JSONCParser.swift
585 Sources/Cloud/VMClient.swift
580 Packages/CmuxExtensionKit/Tests/CmuxExtensionKitTests/CmuxExtensionKitTests.swift
Expand Down Expand Up @@ -189,7 +190,6 @@
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
524 CLI/CMUXCLI+AutoNaming.swift
523 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
522 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
Expand Down
7 changes: 6 additions & 1 deletion Packages/CmuxRemoteSession/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ let package = Package(
),
.testTarget(
name: "CmuxRemoteSessionTests",
dependencies: ["CmuxRemoteSession"]
dependencies: [
"CmuxRemoteSession",
.product(name: "CmuxCore", package: "CmuxCore"),
.product(name: "CmuxRemoteDaemon", package: "CmuxRemoteDaemon"),
.product(name: "CmuxRemoteWorkspace", package: "CmuxRemoteWorkspace"),
]
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ extension RemoteSessionCoordinator {
static let bootstrapRemoteTTYRetryLimit = 8

func requestBootstrapRemoteTTYIfNeededLocked() {
// The bootstrap TTY is resolved only to TTY-scope the port scans, so
// when port scanning is disabled there is no reason to spawn ssh for
// it (issue #6123). Re-enabling re-requests it.
guard remotePortScanningEnabled else { return }
guard !bootstrapRemoteTTYResolved else { return }
guard let relayPort = configuration.relayPort, relayPort > 0 else { return }
if !remotePortScanTTYNames.isEmpty {
Expand Down Expand Up @@ -49,6 +53,7 @@ extension RemoteSessionCoordinator {

func scheduleBootstrapRemoteTTYRetryLocked() {
guard !isStopping else { return }
guard remotePortScanningEnabled else { return }
guard daemonReady else { return }
guard !bootstrapRemoteTTYResolved else { return }
guard remotePortScanTTYNames.isEmpty else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,66 @@ extension RemoteSessionCoordinator {
}
}

/// Enables or disables remote listening-port discovery on the coordinator
/// queue. The app derives the flag from the sidebar ports-visibility
/// settings (`sidebar.showPorts` and `sidebar.hideAllDetails`): disabling
/// tears down any active poll timer and in-flight scan burst and stops
/// every ssh-spawning scan; enabling resumes polling when the daemon is
/// ready and re-arms a TTY-scoped refresh so ports repopulate promptly.
public func updateRemotePortScanningEnabled(_ enabled: Bool) {
queue.async { [weak self] in
self?.updateRemotePortScanningEnabledLocked(enabled)
}
}

func updateRemotePortScanningEnabledLocked(_ enabled: Bool) {
guard remotePortScanningEnabled != enabled else { return }
remotePortScanningEnabled = enabled
guard enabled else {
suspendRemotePortScanningLocked()
return
}
updateRemotePortPollingStateLocked()
guard daemonReady, !isStopping else { return }
if remotePortScanTTYNames.isEmpty {
// Resume bootstrap TTY resolution (its ssh is gated on the flag
// too) so TTY-scoped scanning can start once the remote TTY is
// known again.
requestBootstrapRemoteTTYIfNeededLocked()
} else if remotePortPollTimer == nil {
// TTYs are known but no fallback poll timer covers them, so re-arm
// one refresh burst to repopulate the display promptly. The
// host-wide/delta poll modes already refresh themselves through the
// timer restarted above.
remotePortScanPendingReason = remotePortScanPendingReason?.merged(with: .refresh) ?? .refresh
scheduleRemotePortScanCoalesceLocked()
}
}

/// Tears down every ssh-spawning port-scan activity and clears detected
/// ports. Mirrors the scan teardown on the proxy-error path so a disabled
/// scanner leaves no poll timer, burst, or stale ports behind, and resets
/// the hidden poll/bootstrap bookkeeping (delta baseline, retry budget) so
/// re-enabling resumes like a fresh scanner start rather than against
/// pre-disable state.
private func suspendRemotePortScanningLocked() {
remotePortScanGeneration &+= 1
remotePortScanBurstTask?.cancel()
remotePortScanBurstTask = nil
remotePortScanBurstActive = false
remotePortScanActiveReason = nil
remotePortScanPendingReason = nil
cancelRemotePortScanCoalesceLocked()
cancelBootstrapRemoteTTYRetryLocked()
bootstrapRemoteTTYRetryCount = 0
remoteScannedPortsByPanel.removeAll()
stopRemotePortPollingLocked()
polledRemotePorts = []
remotePortPollBaselinePorts = nil
keepPolledRemotePortsUntilTTYScan = false
publishPortsSnapshotLocked()
}

func updateRemotePortScanTTYsLocked(_ ttyNames: [UUID: String]) {
let previousTTYNames = remotePortScanTTYNames
let nextTTYNames = ttyNames.reduce(into: [UUID: String]()) { result, entry in
Expand Down Expand Up @@ -60,6 +120,7 @@ extension RemoteSessionCoordinator {

func kickRemotePortScanLocked(panelId: UUID, reason: PortScanKickReason) {
guard !isStopping else { return }
guard remotePortScanningEnabled else { return }
guard daemonReady else { return }
guard remotePortScanTTYNames[panelId] != nil else { return }
if remotePortScanBurstActive, remotePortScanActiveReason == .command, reason == .refresh {
Expand Down Expand Up @@ -150,6 +211,7 @@ extension RemoteSessionCoordinator {
}

func performRemotePortScanLocked() {
guard remotePortScanningEnabled else { return }
let ttyNamesByPanel = remotePortScanTTYNames
guard !ttyNamesByPanel.isEmpty else {
remoteScannedPortsByPanel.removeAll()
Expand Down Expand Up @@ -336,6 +398,7 @@ extension RemoteSessionCoordinator {
}

private func remotePortPollingModeLocked() -> RemotePortPollingMode? {
guard remotePortScanningEnabled else { return nil }
if !remotePortScanTTYNames.isEmpty {
return shouldUseTTYFallbackRemotePortPollingLocked() ? .ttyScoped : nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ public final class RemoteSessionCoordinator: @unchecked Sendable {
var polledRemotePorts: [Int] = []
var remotePortPollBaselinePorts: Set<Int>?
var keepPolledRemotePortsUntilTTYScan = false
/// Whether remote listening-port discovery (TTY-scoped scan bursts and the
/// host-wide/delta poll fallback) may spawn ssh. The app derives this from
/// the sidebar ports-visibility settings (`sidebar.showPorts` and
/// `sidebar.hideAllDetails`) via ``updateRemotePortScanningEnabled(_:)``;
/// when ports are not displayed there is nothing for the scans to populate,
/// so the whole ssh-spawning path is suspended.
var remotePortScanningEnabled = true
var bootstrapRemoteTTYResolved = false
var bootstrapRemoteTTYRetryTask: Task<Void, Never>?
var bootstrapRemoteTTYRetryToken: UUID?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ struct RemotePlatformProbeScriptTests {
}

private static func runProcess(executablePath: String, arguments: [String]) throws -> ProcessResult {
// Serialize against the other real-subprocess suite; see
// ``remoteSubprocessTestLock``.
remoteSubprocessTestLock.lock()
defer { remoteSubprocessTestLock.unlock() }
let process = Process()
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
Expand Down
Loading
Loading