@@ -173,20 +173,31 @@ package final class AsyncProcess {
173
173
case stdinUnavailable
174
174
}
175
175
176
- package typealias OutputStream = AsyncStream < [ UInt8 ] >
176
+ package typealias ReadableStream = AsyncStream < [ UInt8 ] >
177
177
178
- package enum OutputRedirection {
178
+ package enum OutputRedirection : Sendable {
179
179
/// Do not redirect the output
180
180
case none
181
- /// Collect stdout and stderr output and provide it back via ProcessResult object. If redirectStderr is true,
182
- /// stderr be redirected to stdout.
181
+
182
+ /// Collect stdout and stderr output and provide it back via ``AsyncProcessResult`` object. If
183
+ /// `redirectStderr` is `true`, `stderr` be redirected to `stdout`.
183
184
case collect( redirectStderr: Bool )
184
- /// Stream stdout and stderr via the corresponding closures. If redirectStderr is true, stderr be redirected to
185
- /// stdout.
185
+
186
+ /// Stream `stdout` and `stderr` via the corresponding closures. If `redirectStderr` is `true`, `stderr` will
187
+ /// be redirected to `stdout`.
186
188
case stream( stdout: OutputClosure , stderr: OutputClosure , redirectStderr: Bool )
187
189
190
+ /// Stream stdout and stderr as `AsyncSequence` provided as an argument to closures passed to
191
+ /// ``AsyncProcess/launch(stdoutStream:stderrStream:)``.
192
+ case asyncStream(
193
+ stdoutStream: ReadableStream ,
194
+ stdoutContinuation: ReadableStream . Continuation ,
195
+ stderrStream: ReadableStream ,
196
+ stderrContinuation: ReadableStream . Continuation
197
+ )
198
+
188
199
/// Default collect OutputRedirection that defaults to not redirect stderr. Provided for API compatibility.
189
- package static let collect : OutputRedirection = . collect( redirectStderr: false )
200
+ package static let collect : Self = . collect( redirectStderr: false )
190
201
191
202
/// Default stream OutputRedirection that defaults to not redirect stderr. Provided for API compatibility.
192
203
package static func stream( stdout: @escaping OutputClosure , stderr: @escaping OutputClosure ) -> Self {
@@ -197,15 +208,19 @@ package final class AsyncProcess {
197
208
switch self {
198
209
case . none:
199
210
false
200
- case . collect, . stream:
211
+ case . collect, . stream, . asyncStream :
201
212
true
202
213
}
203
214
}
204
215
205
216
package var outputClosures : ( stdoutClosure: OutputClosure , stderrClosure: OutputClosure ) ? {
206
217
switch self {
207
- case . stream( let stdoutClosure, let stderrClosure, _) :
218
+ case let . stream( stdoutClosure, stderrClosure, _) :
208
219
( stdoutClosure: stdoutClosure, stderrClosure: stderrClosure)
220
+
221
+ case let . asyncStream( stdoutStream, stdoutContinuation, stderrStream, stderrContinuation) :
222
+ ( stdoutClosure: { stdoutContinuation. yield ( $0) } , stderrClosure: { stderrContinuation. yield ( $0) } )
223
+
209
224
case . collect, . none:
210
225
nil
211
226
}
@@ -946,6 +961,65 @@ extension AsyncProcess {
946
961
try await self . popen ( arguments: args, environment: environment, loggingHandler: loggingHandler)
947
962
}
948
963
964
+ package typealias DuplexStreamHandler =
965
+ @Sendable ( _ stdinStream: WritableByteStream , _ stdoutStream: ReadableStream ) async throws -> ( )
966
+ package typealias ReadableStreamHandler =
967
+ @Sendable ( _ stderrStream: ReadableStream ) async throws -> ( )
968
+
969
+ /// Launches a new `AsyncProcess` instances, allowing the caller to consume `stdout` and `stderr` output
970
+ /// with handlers that support structured concurrency.
971
+ /// - Parameters:
972
+ /// - arguments: CLI command used to launch the process.
973
+ /// - environment: environment variables passed to the launched process.
974
+ /// - loggingHandler: handler used for logging,
975
+ /// - stdoutHandler: asynchronous bidirectional handler closure that receives `stdin` and `stdout` streams as
976
+ /// arguments.
977
+ /// - stderrHandler: asynchronous unidirectional handler closure that receives `stderr` stream as an argument.
978
+ /// - Returns: ``AsyncProcessResult`` value as received from the underlying ``AsyncProcess/waitUntilExit()`` call
979
+ /// made on ``AsyncProcess`` instance.
980
+ package static func popen(
981
+ arguments: [ String ] ,
982
+ environment: Environment = . current,
983
+ loggingHandler: LoggingHandler ? = . none,
984
+ stdoutHandler: @escaping DuplexStreamHandler ,
985
+ stderrHandler: ReadableStreamHandler ? = nil
986
+ ) async throws -> AsyncProcessResult {
987
+ let ( stdoutStream, stdoutContinuation) = ReadableStream . makeStream ( )
988
+ let ( stderrStream, stderrContinuation) = ReadableStream . makeStream ( )
989
+
990
+ let process = AsyncProcess (
991
+ arguments: arguments,
992
+ environment: environment,
993
+ outputRedirection: . stream {
994
+ stdoutContinuation. yield ( $0)
995
+ } stderr: {
996
+ stderrContinuation. yield ( $0)
997
+ } ,
998
+ loggingHandler: loggingHandler
999
+ )
1000
+
1001
+ return try await withThrowingTaskGroup ( of: Void . self) { group in
1002
+ let stdinStream = try process. launch ( )
1003
+
1004
+ group. addTask {
1005
+ try await stdoutHandler ( stdinStream, stdoutStream)
1006
+ }
1007
+
1008
+ if let stderrHandler {
1009
+ group. addTask {
1010
+ try await stderrHandler ( stderrStream)
1011
+ }
1012
+ }
1013
+
1014
+ defer {
1015
+ stdoutContinuation. finish ( )
1016
+ stderrContinuation. finish ( )
1017
+ }
1018
+
1019
+ return try await process. waitUntilExit ( )
1020
+ }
1021
+ }
1022
+
949
1023
/// Execute a subprocess and get its (UTF-8) output if it has a non zero exit.
950
1024
///
951
1025
/// - Parameters:
0 commit comments