Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c6b4c45
test: reproduce remote stdin broken pipe abort
lawrencecchen Jul 28, 2026
2a63ff6
fix: ignore remote stdin broken pipes
lawrencecchen Jul 28, 2026
6e2567c
Merge remote-tracking branch 'origin/main' into issue-5750-remote-std…
lawrencecchen Jul 28, 2026
ef6c786
test: reject unexpected remote stdin errors
lawrencecchen Jul 29, 2026
462e80b
fix: propagate unexpected remote stdin errors
lawrencecchen Jul 29, 2026
5b2cd96
refactor: remove remote stdin test injection
lawrencecchen Jul 29, 2026
8ff8d2e
refactor: inject remote process stdin writer
lawrencecchen Jul 29, 2026
ab8bc39
refactor: split remote stdin writer types
lawrencecchen Jul 29, 2026
0e668b4
test: bound unread remote stdin
lawrencecchen Jul 29, 2026
3c8f6ec
fix: bound remote stdin delivery
lawrencecchen Jul 29, 2026
aa1992e
test: retain late remote stdin failures
lawrencecchen Jul 29, 2026
e0b644a
fix: retain late remote stdin failures
lawrencecchen Jul 29, 2026
ca1105e
test: bound inherited remote stdin
lawrencecchen Jul 29, 2026
0e0f159
fix: make remote stdin delivery deadline-aware
lawrencecchen Jul 29, 2026
40bd24e
test: prove failed stdin writers reap children
lawrencecchen Jul 29, 2026
fdaab08
test: bound inherited remote output pipes
lawrencecchen Jul 29, 2026
3bd4881
fix: bound inherited remote output capture
lawrencecchen Jul 29, 2026
134bd4f
fix: wake remote process capture on exit
lawrencecchen Jul 29, 2026
c9bb20b
fix: make remote process completion event driven
lawrencecchen Jul 29, 2026
80b9a08
fix: make remote completion lock free
lawrencecchen Jul 29, 2026
ab34ffa
test: preserve stdin failure through forced kill
lawrencecchen Jul 29, 2026
5c12025
fix: preserve stdin failure through forced kill
lawrencecchen Jul 29, 2026
fe9f1b2
refactor: give stop-aware pipe reads an owner
lawrencecchen Jul 29, 2026
155a251
test: reject non-positive pipe read chunks
lawrencecchen Jul 29, 2026
e65b0c9
fix: normalize pipe read chunk sizes
lawrencecchen Jul 29, 2026
4388ec6
test: make remote process races event driven
lawrencecchen Jul 29, 2026
47d67cd
test: split remote process fixtures
lawrencecchen Jul 29, 2026
c54a980
Merge remote-tracking branch 'origin/main' into issue-5750-remote-std…
lawrencecchen Jul 29, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ internal import CMUXDebugLog
/// synchronized by the capture `DispatchGroup` before any read-back, exactly
/// like the legacy local-variable captures.
public struct RemoteSessionProcessRunner: RemoteSessionProcessRunning {
enum StdinWriter {
static func write(
_ data: Data,
to handle: FileHandle,
executableName: String
) throws {
do {
try handle.writeProcessPipeInput(data)
} catch let error as POSIXError where error.code == .EPIPE {
// A child may exit before consuming optional stdin. The POSIX
// helper turns that expected race into EPIPE instead of SIGPIPE.
} catch {
throw NSError(domain: "cmux.remote.process", code: 3, userInfo: [
NSLocalizedDescriptionKey: "Failed to write stdin for \(executableName): \(error.localizedDescription)",
NSUnderlyingErrorKey: error,
])
}
}
}

/// Test observation seam (package tests only): invoked right after the
/// stdout/stderr capture readers are installed, with the pipe read
/// handles. Return `true` when the hook closes both handles, so the
Expand All @@ -33,7 +53,9 @@ public struct RemoteSessionProcessRunner: RemoteSessionProcessRunning {
self.readHandlesDidInstall = nil
}

init(readHandlesDidInstall: (@Sendable (FileHandle, FileHandle) -> Bool)?) {
init(
readHandlesDidInstall: (@Sendable (FileHandle, FileHandle) -> Bool)? = nil
) {
self.readHandlesDidInstall = readHandlesDidInstall
}

Expand Down Expand Up @@ -187,11 +209,6 @@ public struct RemoteSessionProcessRunner: RemoteSessionProcessRunning {
}
defer { operation?.clearCancellationHandler() }

if let stdin, let pipe = process.standardInput as? Pipe {
pipe.fileHandleForWriting.write(stdin)
try? pipe.fileHandleForWriting.close()
}

func terminateProcessAndWait() {
process.terminate()
let terminatedGracefully = exitSemaphore.wait(timeout: .now() + 2.0) == .success
Expand All @@ -201,6 +218,29 @@ public struct RemoteSessionProcessRunner: RemoteSessionProcessRunning {
}
}

if let stdin, let pipe = process.standardInput as? Pipe {
let inputHandle = pipe.fileHandleForWriting
do {
try StdinWriter.write(
stdin,
to: inputHandle,
executableName: URL(fileURLWithPath: executable).lastPathComponent
)
} catch {
try? inputHandle.close()
if process.isRunning {
terminateProcessAndWait()
}
finishCaptureAndCloseReadHandles()
debugLog(
"remote.proc.stdinWriteFailed exec=\(URL(fileURLWithPath: executable).lastPathComponent) " +
"error=\(error.localizedDescription)"
)
throw error
}
try? inputHandle.close()
}

let didExitBeforeTimeout = exitSemaphore.wait(timeout: .now() + max(0, timeout)) == .success
if !didExitBeforeTimeout, process.isRunning {
if let operation, operation.isCancelled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public protocol RemoteSessionProcessRunning: Sendable {
/// - operation: Optional transfer-cancellation token; when it cancels,
/// the process is terminated and `operation.cancellationError` is
/// thrown.
/// - Throws: The legacy launch-failure (`cmux.remote.process` code 1) and
/// timeout (code 2) errors, or `operation.cancellationError`.
/// - Throws: Launch-failure (`cmux.remote.process` code 1), timeout
/// (code 2), stdin-write (code 3), or `operation.cancellationError`.
func run(
_ request: RemoteProcessRequest,
operation: (any RemoteTransferCancelling)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,42 @@ struct RemoteSessionProcessRunnerTests {
#expect(result.stdout == "hello-stdin")
}

@Test("A child closing stdin before a large write does not abort the runner")
func childClosingStdinDoesNotAbortRunner() throws {
let runner = RemoteSessionProcessRunner()
let result = try runner.run(
RemoteProcessRequest(
executable: "/usr/bin/true",
arguments: [],
stdin: Data(repeating: 0x41, count: 1_048_576),
timeout: 5
),
operation: nil
)

#expect(result.status == 0)
}

@Test("An unexpected stdin write error keeps the pinned runner error")
func unexpectedStdinWriteErrorKeepsPinnedRunnerError() {
let invalidHandle = FileHandle(fileDescriptor: -1, closeOnDealloc: false)

#expect {
try RemoteSessionProcessRunner.StdinWriter.write(
Data("payload".utf8),
to: invalidHandle,
executableName: "sleep"
)
} throws: { error in
let nsError = error as NSError
let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? POSIXError
return nsError.domain == "cmux.remote.process"
&& nsError.code == 3
&& nsError.localizedDescription.hasPrefix("Failed to write stdin for sleep:")
&& underlyingError?.code == .EBADF
}
}
Comment thread
lawrencecchen marked this conversation as resolved.
Resolved

@Test("Streams a local file through stdin")
func streamsFileStdin() throws {
let fileManager = FileManager.default
Expand Down