Repository navigation
Recover stale restored SSH PTY sessions #5689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10194,7 +10194,11 @@ struct CMUXCLI { | |
| responseTimeout: waitForReady ? 185 : nil | ||
| ) | ||
| } catch { | ||
| throw CLIError(message: "ssh-pty-attach: \(userFacingRemotePTYErrorMessage(error))") | ||
| let userFacingMessage = userFacingRemotePTYErrorMessage(error) | ||
| throw CLIError( | ||
| message: "ssh-pty-attach: \(userFacingMessage)", | ||
| exitCode: remotePTYErrorIndicatesMissingSession(error) ? 253 : 1 | ||
| ) | ||
| } | ||
| var connectedFD: Int32? | ||
| let controlSocketLock = NSLock() | ||
|
|
@@ -10435,6 +10439,21 @@ struct CMUXCLI { | |
| return userFacingRemotePTYErrorMessage(debugString(value) ?? "unknown error") | ||
| } | ||
|
|
||
| private func remotePTYErrorIndicatesMissingSession(_ value: Any?) -> Bool { | ||
| if let error = value as? Error { | ||
| return remotePTYErrorIndicatesMissingSession(String(describing: error)) | ||
| } | ||
| return remotePTYErrorIndicatesMissingSession(debugString(value) ?? "") | ||
| } | ||
|
|
||
| private func remotePTYErrorIndicatesMissingSession(_ message: String) -> Bool { | ||
| let lowered = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() | ||
| guard !lowered.isEmpty else { return false } | ||
| return lowered.contains("pty_session_not_found") || | ||
| (lowered.contains("persistent ssh pty session") && lowered.contains("not running")) || | ||
| (lowered.contains("persistent pty session") && lowered.contains("not running")) | ||
| } | ||
|
Comment on lines
+10449
to
+10455
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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! |
||
|
|
||
| private func userFacingRemotePTYErrorMessage(_ message: String) -> String { | ||
| let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { return "remote PTY operation failed" } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7062,10 +7062,22 @@ final class WorkspaceRemoteSessionController { | |
| process: process, | ||
| stderrPipe: stderrPipe | ||
| ) { | ||
| let retryDelay = 2.0 | ||
| let retryDelay: TimeInterval | ||
| let didCleanStaleListener: Bool | ||
| if Self.reverseRelayStartupFailureIndicatesStaleRemoteListener( | ||
| startupFailure, | ||
| relayPort: relayPort | ||
| ) { | ||
| didCleanStaleListener = cleanupStaleRemoteRelayListenerLocked(relayPort: relayPort) | ||
| retryDelay = didCleanStaleListener ? 0.25 : 2.0 | ||
| } else { | ||
| didCleanStaleListener = false | ||
| retryDelay = 2.0 | ||
| } | ||
| let retrySeconds = max(1, Int(retryDelay.rounded())) | ||
| debugLog( | ||
| "remote.relay.startFailed relayPort=\(relayPort) " + | ||
| "cleanedStaleListener=\(didCleanStaleListener ? 1 : 0) " + | ||
| "error=\(startupFailure)" | ||
| ) | ||
| if let relayServer { | ||
|
|
@@ -7074,10 +7086,12 @@ final class WorkspaceRemoteSessionController { | |
| cliRelayServer = nil | ||
| } | ||
| } | ||
| publishDaemonStatus( | ||
| .error, | ||
| detail: "Remote SSH relay unavailable: \(startupFailure) (retry in \(retrySeconds)s)" | ||
| ) | ||
| if !didCleanStaleListener { | ||
| publishDaemonStatus( | ||
| .error, | ||
| detail: "Remote SSH relay unavailable: \(startupFailure) (retry in \(retrySeconds)s)" | ||
| ) | ||
| } | ||
| scheduleReverseRelayRestartLocked(remotePath: remotePath, delay: retryDelay) | ||
|
Comment on lines
+7089
to
7095
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The fix is to distinguish the noop case — only suppress status publication (and use the fast 0.25 s delay) when the cleanup actually terminated a process (non-empty stdout / a richer boolean signal from the helper), and fall back to publishing the status normally for the noop case. |
||
| return | ||
| } | ||
|
|
@@ -9253,6 +9267,16 @@ final class WorkspaceRemoteSessionController { | |
| return bestErrorLine(stderr: stderr) ?? "status=\(process.terminationStatus)" | ||
| } | ||
|
|
||
| static func reverseRelayStartupFailureIndicatesStaleRemoteListener( | ||
| _ detail: String, | ||
| relayPort: Int | ||
| ) -> Bool { | ||
| guard relayPort > 0 else { return false } | ||
| let lowered = detail.lowercased() | ||
| return lowered.contains("remote port forwarding failed") | ||
| && lowered.contains("listen port \(relayPort)") | ||
| } | ||
|
|
||
| private static func meaningfulErrorLine(in text: String) -> String? { | ||
| let lines = text | ||
| .split(separator: "\n") | ||
|
|
@@ -13941,7 +13965,10 @@ final class Workspace: Identifiable, ObservableObject { | |
| ) | ||
| return SSHPTYAttachStartupCommandBuilder.command( | ||
| sessionID: sessionID, | ||
| foregroundAuth: foregroundAuth | ||
| foregroundAuth: foregroundAuth, | ||
| remoteCommand: remoteConfiguration.relayPort.map( | ||
| SSHPTYAttachStartupCommandBuilder.restoredRemoteShellCommand(relayPort:) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,7 +152,10 @@ nonisolated enum SSHPTYAttachStartupCommandBuilder { | |
| " --command-b64 \(shellQuote(Data($0.utf8).base64EncodedString()))" | ||
| } ?? "" | ||
| let attachCommand = "\"$cmux_ssh_attach_cli\" --socket \"$CMUX_SOCKET_PATH\" ssh-pty-attach --wait\(requireExistingFlag) --workspace \"$CMUX_WORKSPACE_ID\" --session-id \"$cmux_ssh_attach_session_id\" --attachment-id \"${CMUX_SURFACE_ID:-}\"\(commandB64Flag)" | ||
| lines += retryingAttachLines(command: attachCommand) | ||
| let fallbackCommand = requireExisting && !commandB64Flag.isEmpty | ||
| ? "\"$cmux_ssh_attach_cli\" --socket \"$CMUX_SOCKET_PATH\" ssh-pty-attach --wait --workspace \"$CMUX_WORKSPACE_ID\" --session-id \"$cmux_ssh_attach_session_id\" --attachment-id \"${CMUX_SURFACE_ID:-}\"\(commandB64Flag)" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a restored attach exits 253, the first Useful? React with 👍 / 👎. |
||
| : nil | ||
| lines += retryingAttachLines(command: attachCommand, missingSessionFallbackCommand: fallbackCommand) | ||
| return "/bin/sh -c \(shellQuote(lines.joined(separator: "\n")))" | ||
| } | ||
|
|
||
|
|
@@ -169,8 +172,11 @@ nonisolated enum SSHPTYAttachStartupCommandBuilder { | |
| ) | ||
| } | ||
|
|
||
| private static func retryingAttachLines(command: String) -> [String] { | ||
| [ | ||
| private static func retryingAttachLines( | ||
| command: String, | ||
| missingSessionFallbackCommand: String? = nil | ||
| ) -> [String] { | ||
| var lines = [ | ||
| "cmux_ssh_attach_reconnect_limit=\"${CMUX_SSH_RECONNECT_LIMIT:-20}\"", | ||
| "case \"$cmux_ssh_attach_reconnect_limit\" in ''|*[!0-9]*) cmux_ssh_attach_reconnect_limit=20 ;; esac", | ||
| "cmux_ssh_attach_reconnect_delay=\"${CMUX_SSH_RECONNECT_DELAY_SECONDS:-2}\"", | ||
|
|
@@ -179,13 +185,25 @@ nonisolated enum SSHPTYAttachStartupCommandBuilder { | |
| "while :; do", | ||
| " \(command)", | ||
| " cmux_ssh_attach_status=$?", | ||
| ] | ||
| if let missingSessionFallbackCommand { | ||
| lines += [ | ||
| " if [ \"$cmux_ssh_attach_status\" -eq 253 ]; then", | ||
| " if [ -t 2 ]; then printf '\\n\\033[33m[cmux] persisted SSH PTY session is gone; starting a new remote shell.\\033[0m\\n' >&2 || true; fi", | ||
| " \(missingSessionFallbackCommand)", | ||
| " exit \"$?\"", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fallback attach skips bridge retriesMedium Severity After exit code 253, the stale-session fallback runs a second Reviewed by Cursor Bugbot for commit bdf5944. Configure here. |
||
| " fi", | ||
| ] | ||
| } | ||
| lines += [ | ||
| " case \"$cmux_ssh_attach_status\" in 254|255) ;; *) exit \"$cmux_ssh_attach_status\" ;; esac", | ||
| " if [ \"$cmux_ssh_attach_retry\" -ge \"$cmux_ssh_attach_reconnect_limit\" ]; then exit \"$cmux_ssh_attach_status\"; fi", | ||
| " cmux_ssh_attach_retry=$((cmux_ssh_attach_retry + 1))", | ||
| " if [ -t 2 ]; then printf '\\n\\033[33m[cmux] remote PTY bridge closed; reattaching (attempt %s/%s).\\033[0m\\n' \"$cmux_ssh_attach_retry\" \"$cmux_ssh_attach_reconnect_limit\" >&2 || true; fi", | ||
| " if [ \"$cmux_ssh_attach_reconnect_delay\" -gt 0 ]; then sleep \"$cmux_ssh_attach_reconnect_delay\"; fi", | ||
| "done", | ||
| ] | ||
| return lines | ||
| } | ||
|
|
||
| private static func foregroundAuthLines(_ auth: ForegroundAuth) -> [String] { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.