Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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 @@ -27,6 +27,55 @@ 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()
// TTY-scoped workspaces without a fallback poll timer only refresh
// ports on a shell-activity kick, so re-arm one refresh burst now to
// repopulate the display promptly when it is turned back on. The
// host-wide/delta poll modes already refresh themselves through the
// timer restarted above, so skip the burst when that timer is live.
if daemonReady, !isStopping, !remotePortScanTTYNames.isEmpty, remotePortPollTimer == nil {
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.
private func suspendRemotePortScanningLocked() {
remotePortScanGeneration &+= 1
remotePortScanBurstTask?.cancel()
remotePortScanBurstTask = nil
remotePortScanBurstActive = false
remotePortScanActiveReason = nil
remotePortScanPendingReason = nil
cancelRemotePortScanCoalesceLocked()
remoteScannedPortsByPanel.removeAll()
stopRemotePortPollingLocked()
polledRemotePorts = []
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 +109,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 +200,7 @@ extension RemoteSessionCoordinator {
}

func performRemotePortScanLocked() {
guard remotePortScanningEnabled else { return }
let ttyNamesByPanel = remotePortScanTTYNames
guard !ttyNamesByPanel.isEmpty else {
remoteScannedPortsByPanel.removeAll()
Expand Down Expand Up @@ -336,6 +387,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,20 +195,24 @@ struct RemotePlatformProbeScriptTests {
}

private static func runProcess(executablePath: String, arguments: [String]) throws -> ProcessResult {
let process = Process()
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = arguments
process.standardInput = FileHandle.nullDevice
process.standardOutput = stdoutPipe
process.standardError = stderrPipe

try process.run()
process.waitUntilExit()

let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
return ProcessResult(status: process.terminationStatus, stdout: stdout, stderr: stderr)
// Serialize against the other real-subprocess suite; see
// ``remoteSubprocessTestLock``.
try withRemoteSubprocessTestLock {
let process = Process()
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = arguments
process.standardInput = FileHandle.nullDevice
process.standardOutput = stdoutPipe
process.standardError = stderrPipe

try process.run()
process.waitUntilExit()

let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
return ProcessResult(status: process.terminationStatus, stdout: stdout, stderr: stderr)
}
}
}
Loading
Loading