From 3b339646152e7b5622887b52118b987b9538893d Mon Sep 17 00:00:00 2001 From: Robert Connell Date: Tue, 12 Aug 2025 21:01:38 -0700 Subject: [PATCH 1/4] Alter usage of async withExtendedLifetime that is causing crash in swift concurrency --- Sources/SWBUtil/Process.swift | 53 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift index 12434bf2..e5fbc247 100644 --- a/Sources/SWBUtil/Process.swift +++ b/Sources/SWBUtil/Process.swift @@ -82,33 +82,42 @@ extension Process { extension Process { public static func getOutput(url: URL, arguments: [String], currentDirectoryURL: URL? = nil, environment: Environment? = nil, interruptible: Bool = true) async throws -> Processes.ExecutionResult { if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) { + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - return try await withExtendedLifetime((Pipe(), Pipe())) { (stdoutPipe, stderrPipe) in - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in - let stdoutStream = process.makeStream(for: \.standardOutputPipe, using: stdoutPipe) - let stderrStream = process.makeStream(for: \.standardErrorPipe, using: stderrPipe) - return (stdoutStream, stderrStream) - } collect: { (stdoutStream, stderrStream) in - let stdoutData = try await stdoutStream.collect() - let stderrData = try await stderrStream.collect() - return (stdoutData: stdoutData, stderrData: stderrData) - } - return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData)) + defer { withExtendedLifetime(stdoutPipe) {} } + defer { withExtendedLifetime(stderrPipe) {} } + + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in + let stdoutStream = process.makeStream(for: \.standardOutputPipe, using: stdoutPipe) + let stderrStream = process.makeStream(for: \.standardErrorPipe, using: stderrPipe) + return (stdoutStream, stderrStream) + } collect: { (stdoutStream, stderrStream) in + let stdoutData = try await stdoutStream.collect() + let stderrData = try await stderrStream.collect() + return (stdoutData: stdoutData, stderrData: stderrData) } + return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData)) } else { + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - return try await withExtendedLifetime((Pipe(), Pipe())) { (stdoutPipe, stderrPipe) in - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in - let stdoutStream = process._makeStream(for: \.standardOutputPipe, using: stdoutPipe) - let stderrStream = process._makeStream(for: \.standardErrorPipe, using: stderrPipe) - return (stdoutStream, stderrStream) - } collect: { (stdoutStream, stderrStream) in - let stdoutData = try await stdoutStream.collect() - let stderrData = try await stderrStream.collect() - return (stdoutData: stdoutData, stderrData: stderrData) - } - return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData)) + defer { withExtendedLifetime(stdoutPipe) {} } + defer { withExtendedLifetime(stderrPipe) {} } + + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in + let stdoutStream = process._makeStream(for: \.standardOutputPipe, using: stdoutPipe) + let stderrStream = process._makeStream(for: \.standardErrorPipe, using: stderrPipe) + return (stdoutStream, stderrStream) + } collect: { (stdoutStream, stderrStream) in + let stdoutData = try await stdoutStream.collect() + let stderrData = try await stderrStream.collect() + return (stdoutData: stdoutData, stderrData: stderrData) } + + return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData)) } } From 630c3e045fd86252bb9a381e39e8e6bbccfd6c1c Mon Sep 17 00:00:00 2001 From: Robert Connell Date: Tue, 12 Aug 2025 21:15:15 -0700 Subject: [PATCH 2/4] Additional crash fixes when consuming process output --- Sources/SWBUtil/Process.swift | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift index e5fbc247..75c27b81 100644 --- a/Sources/SWBUtil/Process.swift +++ b/Sources/SWBUtil/Process.swift @@ -116,36 +116,39 @@ extension Process { let stderrData = try await stderrStream.collect() return (stdoutData: stdoutData, stderrData: stderrData) } - return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData)) } } public static func getMergedOutput(url: URL, arguments: [String], currentDirectoryURL: URL? = nil, environment: Environment? = nil, interruptible: Bool = true) async throws -> (exitStatus: Processes.ExitStatus, output: Data) { if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) { - // Extend the lifetime of the pipe to avoid file descriptors being closed until the AsyncStream is finished being consumed. - return try await withExtendedLifetime(Pipe()) { pipe in - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in - process.standardOutputPipe = pipe - process.standardErrorPipe = pipe - return pipe.fileHandleForReading.bytes() - } collect: { stream in - try await stream.collect() - } - return (exitStatus: exitStatus, output: Data(output)) + let pipe = Pipe() + + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(pipe) {} } + + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in + process.standardOutputPipe = pipe + process.standardErrorPipe = pipe + return pipe.fileHandleForReading.bytes() + } collect: { stream in + try await stream.collect() } + return (exitStatus: exitStatus, output: Data(output)) } else { - // Extend the lifetime of the pipe to avoid file descriptors being closed until the AsyncStream is finished being consumed. - return try await withExtendedLifetime(Pipe()) { pipe in - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in - process.standardOutputPipe = pipe - process.standardErrorPipe = pipe - return pipe.fileHandleForReading._bytes() - } collect: { stream in - try await stream.collect() - } - return (exitStatus: exitStatus, output: Data(output)) + let pipe = Pipe() + + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(pipe) {} } + + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in + process.standardOutputPipe = pipe + process.standardErrorPipe = pipe + return pipe.fileHandleForReading._bytes() + } collect: { stream in + try await stream.collect() } + return (exitStatus: exitStatus, output: Data(output)) } } From 412d14545a8ec1379272906834a567476c5d7d0a Mon Sep 17 00:00:00 2001 From: Robert Connell Date: Tue, 12 Aug 2025 22:10:58 -0700 Subject: [PATCH 3/4] Remove calls to defer { withExtendedLifetime } --- Sources/SWBUtil/Process.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift index 75c27b81..9bc6ef4a 100644 --- a/Sources/SWBUtil/Process.swift +++ b/Sources/SWBUtil/Process.swift @@ -85,10 +85,6 @@ extension Process { let stdoutPipe = Pipe() let stderrPipe = Pipe() - // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - defer { withExtendedLifetime(stdoutPipe) {} } - defer { withExtendedLifetime(stderrPipe) {} } - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in let stdoutStream = process.makeStream(for: \.standardOutputPipe, using: stdoutPipe) let stderrStream = process.makeStream(for: \.standardErrorPipe, using: stderrPipe) @@ -103,10 +99,6 @@ extension Process { let stdoutPipe = Pipe() let stderrPipe = Pipe() - // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - defer { withExtendedLifetime(stdoutPipe) {} } - defer { withExtendedLifetime(stderrPipe) {} } - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in let stdoutStream = process._makeStream(for: \.standardOutputPipe, using: stdoutPipe) let stderrStream = process._makeStream(for: \.standardErrorPipe, using: stderrPipe) @@ -124,9 +116,6 @@ extension Process { if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) { let pipe = Pipe() - // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - defer { withExtendedLifetime(pipe) {} } - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in process.standardOutputPipe = pipe process.standardErrorPipe = pipe @@ -138,9 +127,6 @@ extension Process { } else { let pipe = Pipe() - // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. - defer { withExtendedLifetime(pipe) {} } - let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in process.standardOutputPipe = pipe process.standardErrorPipe = pipe From 04e5c7a0300936fcddfbc550494404ed859ec88c Mon Sep 17 00:00:00 2001 From: Robert Connell Date: Tue, 12 Aug 2025 22:15:14 -0700 Subject: [PATCH 4/4] Revert "Remove calls to defer { withExtendedLifetime }" This reverts commit 412d14545a8ec1379272906834a567476c5d7d0a. --- Sources/SWBUtil/Process.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift index 9bc6ef4a..75c27b81 100644 --- a/Sources/SWBUtil/Process.swift +++ b/Sources/SWBUtil/Process.swift @@ -85,6 +85,10 @@ extension Process { let stdoutPipe = Pipe() let stderrPipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(stdoutPipe) {} } + defer { withExtendedLifetime(stderrPipe) {} } + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in let stdoutStream = process.makeStream(for: \.standardOutputPipe, using: stdoutPipe) let stderrStream = process.makeStream(for: \.standardErrorPipe, using: stderrPipe) @@ -99,6 +103,10 @@ extension Process { let stdoutPipe = Pipe() let stderrPipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(stdoutPipe) {} } + defer { withExtendedLifetime(stderrPipe) {} } + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in let stdoutStream = process._makeStream(for: \.standardOutputPipe, using: stdoutPipe) let stderrStream = process._makeStream(for: \.standardErrorPipe, using: stderrPipe) @@ -116,6 +124,9 @@ extension Process { if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) { let pipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(pipe) {} } + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in process.standardOutputPipe = pipe process.standardErrorPipe = pipe @@ -127,6 +138,9 @@ extension Process { } else { let pipe = Pipe() + // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed. + defer { withExtendedLifetime(pipe) {} } + let (exitStatus, output) = try await _getOutput(url: url, arguments: arguments, currentDirectoryURL: currentDirectoryURL, environment: environment, interruptible: interruptible) { process in process.standardOutputPipe = pipe process.standardErrorPipe = pipe