Skip to content
Merged
5 changes: 5 additions & 0 deletions CLI/SSHPTYResizeMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ actor SSHPTYResizeMonitor {
}
}

nonisolated func requestCurrentResize() {
let size = CMUXCLI.currentCLITerminalSize()
eventContinuation.yield((size: size, force: true))
}

nonisolated func cancel() {
source.cancel()
eventContinuation.finish()
Expand Down
7 changes: 7 additions & 0 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11336,6 +11336,13 @@ struct CMUXCLI {
)
defer { resizeMonitor.cancel() }

// Reconcile the remote PTY size once now that the SIGWINCH source is
// armed. The handshake captured the terminal size before the source
// existed; any SIGWINCH that arrived in that window was delivered with
// SIGWINCH's default disposition (ignore) and lost, with no later
// correction.
resizeMonitor.requestCurrentResize()

Task.detached(priority: .userInitiated) { [resizeMonitor, fd] in
var buffer = [UInt8](repeating: 0, count: 8192)
while true {
Expand Down
76 changes: 36 additions & 40 deletions cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3683,10 +3683,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stdout.isEmpty, result.stdout)
XCTAssertTrue(result.stderr.contains("ssh-pty-attach: remote PTY start failed"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_attach_end",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}

func testSSHPTYAttachBridgeEOFWhileSessionRunsExitsWithoutSSHRetryStatus() throws {
Expand Down Expand Up @@ -3727,6 +3724,10 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
"attachment_id": surfaceId,
]
)
case "workspace.remote.pty_resize":
XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
XCTAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
id: id,
Expand Down Expand Up @@ -3791,11 +3792,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
result.stderr
)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_sessions",
"workspace.remote.pty_detach",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_detach"])
}

func testSSHPTYAttachBridgeEOFWhenSessionGoneClearsLocalState() throws {
Expand Down Expand Up @@ -3834,6 +3831,11 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
"attachment_id": surfaceId,
]
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
XCTAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
id: id,
Expand Down Expand Up @@ -3889,11 +3891,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stdout.isEmpty, result.stdout)
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_sessions",
"workspace.remote.pty_attach_end",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}

func testSSHPTYAttachWithoutSurfaceDoesNotSendLocalAttachEnd() throws {
Expand Down Expand Up @@ -3937,6 +3935,10 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
"attachment_id": attachmentID ?? "attachment",
]
)
case "workspace.remote.pty_resize":
XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
XCTAssertNil(params["surface_id"])
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
XCTAssertNil(params["surface_id"])
return self.v2Response(
Expand Down Expand Up @@ -3978,10 +3980,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stdout.isEmpty, result.stdout)
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_sessions",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions"])
}

func testSSHPTYAttachBridgeResetWhenSessionGoneClearsLocalState() throws {
Expand Down Expand Up @@ -4020,6 +4019,11 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
"attachment_id": surfaceId,
]
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
XCTAssertEqual(params["surface_id"] as? String, surfaceId)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(
id: id,
Expand Down Expand Up @@ -4075,11 +4079,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertTrue(result.stdout.isEmpty, result.stdout)
XCTAssertTrue(result.stderr.isEmpty, result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_sessions",
"workspace.remote.pty_attach_end",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_resize", "workspace.remote.pty_sessions", "workspace.remote.pty_attach_end"])
}

func testSSHPTYAttachWaitUsesCurrentTerminalSizeForBridgeHandshake() throws {
Expand Down Expand Up @@ -4151,6 +4151,12 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
"attachment_id": surfaceId,
]
)
case "workspace.remote.pty_resize":
let params = payload["params"] as? [String: Any] ?? [:]
XCTAssertEqual(params["attachment_token"] as? String, "attach-token")
XCTAssertEqual(params["cols"] as? Int, 132)
XCTAssertEqual(params["rows"] as? Int, 43)
return self.v2Response(id: id, ok: true, result: ["resized": true])
case "workspace.remote.pty_sessions":
return self.v2Response(id: id, ok: true, result: ["sessions": []])
case "workspace.remote.pty_attach_end":
Expand Down Expand Up @@ -4395,15 +4401,11 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
}
XCTAssertEqual(bridgeReady.wait(timeout: .now() + 5), .success)

var sawResize = false
for _ in 0..<10 {
Darwin.kill(process.processIdentifier, SIGWINCH)
if resizeRequestReceived.wait(timeout: .now() + 0.2) == .success {
sawResize = true
break
}
}
XCTAssertTrue(sawResize, "Expected ssh-pty-attach to issue a resize RPC after SIGWINCH")
XCTAssertEqual(
resizeRequestReceived.wait(timeout: .now() + 5),
.success,
"Expected ssh-pty-attach to issue its initial resize RPC after bridge ready"
)

closeBridge.signal()
wait(for: [bridgeHandled], timeout: 5)
Expand Down Expand Up @@ -4587,10 +4589,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(result.status, 1, result.stderr)
XCTAssertTrue(result.stderr.contains("ssh-pty-attach: missing session"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_attach_end",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}

func testSSHPTYAttachRequireExistingSessionNotFoundFailsWithoutWaitRetry() throws {
Expand Down Expand Up @@ -4674,10 +4673,7 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase {
XCTAssertEqual(result.status, 1, result.stderr)
XCTAssertTrue(result.stderr.contains("persistent SSH PTY session is no longer running"), result.stderr)
let methods = state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }
XCTAssertEqual(methods, [
"workspace.remote.pty_bridge",
"workspace.remote.pty_attach_end",
])
XCTAssertEqual(methods, ["workspace.remote.pty_bridge", "workspace.remote.pty_attach_end"])
}

func testSSHSessionListAllWorkspacesReportsQueryErrors() throws {
Expand Down
2 changes: 1 addition & 1 deletion cmuxTests/CLINotifyProcessTestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ extension CLINotifyProcessIntegrationRegressionTests {
pending.append(buffer, count: count)
}

let payload: [String: Any] = ["type": "ready"]
let payload: [String: Any] = ["type": "ready", "attachment_token": "attach-token"]
guard var data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return }
data.append(0x0A)
data.withUnsafeBytes { rawBuffer in
Expand Down
Loading