11
11
//===----------------------------------------------------------------------===//
12
12
13
13
public import Foundation
14
- import SWBLibc
14
+ public import SWBLibc
15
+ import Synchronization
15
16
16
- #if os(Windows )
17
- public typealias pid_t = Int32
17
+ #if canImport(Subprocess )
18
+ import Subprocess
18
19
#endif
19
20
20
- #if !canImport(Darwin)
21
- extension ProcessInfo {
22
- public var isMacCatalystApp : Bool {
23
- false
24
- }
25
- }
21
+ #if canImport(System)
22
+ public import System
23
+ #else
24
+ public import SystemPackage
25
+ #endif
26
+
27
+ #if os(Windows)
28
+ public typealias pid_t = Int32
26
29
#endif
27
30
28
31
#if (!canImport(Foundation.NSTask) || targetEnvironment(macCatalyst)) && canImport(Darwin)
@@ -64,7 +67,7 @@ public typealias Process = Foundation.Process
64
67
#endif
65
68
66
69
extension Process {
67
- public static var hasUnsafeWorkingDirectorySupport : Bool {
70
+ fileprivate static var hasUnsafeWorkingDirectorySupport : Bool {
68
71
get throws {
69
72
switch try ProcessInfo . processInfo. hostOperatingSystem ( ) {
70
73
case . linux:
@@ -81,6 +84,36 @@ extension Process {
81
84
82
85
extension Process {
83
86
public static func getOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> Processes . ExecutionResult {
87
+ #if canImport(Subprocess)
88
+ #if !canImport(Darwin) || os(macOS)
89
+ let result = try await Subprocess . run ( . path( FilePath ( url. filePath. str) ) , arguments: . init( arguments) , environment: environment. map { . custom( . init( $0) ) } ?? . inherit, workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil , body: { execution, inputWriter, outputReader, errorReader in
90
+ try await inputWriter. finish ( )
91
+ let cancellationPromise = Promise < Bool , Never > ( )
92
+ return try await withTaskCancellationHandler {
93
+ async let cancellationListener : ( ) = {
94
+ if await cancellationPromise. value, interruptible {
95
+ try execution. send ( signal: . terminate)
96
+ }
97
+ } ( )
98
+ async let stdoutBytesAsync = outputReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
99
+ async let stderrBytesAsync = errorReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
100
+ let stdoutBytes = try await stdoutBytesAsync
101
+ let stderrBytes = try await stderrBytesAsync
102
+ cancellationPromise. fulfill ( with: false )
103
+ try await cancellationListener
104
+ if interruptible {
105
+ try Task . checkCancellation ( )
106
+ }
107
+ return ( stdoutBytes, stderrBytes)
108
+ } onCancel: {
109
+ cancellationPromise. fulfill ( with: true )
110
+ }
111
+ } )
112
+ return Processes . ExecutionResult ( exitStatus: . init( result. terminationStatus) , stdout: Data ( result. value. 0 ) , stderr: Data ( result. value. 1 ) )
113
+ #else
114
+ throw StubError . error ( " Process spawning is unavailable " )
115
+ #endif
116
+ #else
84
117
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85
118
// Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed.
86
119
return try await withExtendedLifetime ( ( Pipe ( ) , Pipe ( ) ) ) { ( stdoutPipe, stderrPipe) in
@@ -110,9 +143,45 @@ extension Process {
110
143
return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
111
144
}
112
145
}
146
+ #endif
113
147
}
114
148
115
149
public static func getMergedOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> ( exitStatus: Processes . ExitStatus , output: Data ) {
150
+ #if canImport(Subprocess)
151
+ #if !canImport(Darwin) || os(macOS)
152
+ let ( readEnd, writeEnd) = try FileDescriptor . pipe ( )
153
+ return try await readEnd. closeAfter {
154
+ // Direct both stdout and stderr to the same fd. Only set `closeAfterSpawningProcess` on one of the outputs so it isn't double-closed (similarly avoid using closeAfter for the same reason).
155
+ let result = try await Subprocess . run ( . path( FilePath ( url. filePath. str) ) , arguments: . init( arguments) , environment: environment. map { . custom( . init( $0) ) } ?? . inherit, workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil , output: . fileDescriptor( writeEnd, closeAfterSpawningProcess: true ) , error: . fileDescriptor( writeEnd, closeAfterSpawningProcess: false ) , body: { execution in
156
+ let cancellationPromise = Promise < Bool , Never > ( )
157
+ return try await withTaskCancellationHandler {
158
+ async let cancellationListener : ( ) = {
159
+ if await cancellationPromise. value, interruptible {
160
+ try execution. send ( signal: . terminate)
161
+ }
162
+ } ( )
163
+ let bytes : [ UInt8 ]
164
+ if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
165
+ bytes = try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . dataStream ( ) . collect ( ) ) )
166
+ } else {
167
+ bytes = try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . _dataStream ( ) . collect ( ) ) )
168
+ }
169
+ cancellationPromise. fulfill ( with: false )
170
+ try await cancellationListener
171
+ if interruptible {
172
+ try Task . checkCancellation ( )
173
+ }
174
+ return bytes
175
+ } onCancel: {
176
+ cancellationPromise. fulfill ( with: true )
177
+ }
178
+ } )
179
+ return ( . init( result. terminationStatus) , Data ( result. value) )
180
+ }
181
+ #else
182
+ throw StubError . error ( " Process spawning is unavailable " )
183
+ #endif
184
+ #else
116
185
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
117
186
// Extend the lifetime of the pipe to avoid file descriptors being closed until the AsyncStream is finished being consumed.
118
187
return try await withExtendedLifetime ( Pipe ( ) ) { pipe in
@@ -138,6 +207,7 @@ extension Process {
138
207
return ( exitStatus: exitStatus, output: Data ( output) )
139
208
}
140
209
}
210
+ #endif
141
211
}
142
212
143
213
private static func _getOutput< T, U> ( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? , environment: Environment ? , interruptible: Bool , setup: ( Process ) -> T , collect: ( T ) async throws -> U ) async throws -> ( exitStatus: Processes . ExitStatus , output: U ) {
@@ -294,6 +364,19 @@ public enum Processes: Sendable {
294
364
}
295
365
}
296
366
367
+ #if canImport(Subprocess)
368
+ extension Processes . ExitStatus {
369
+ init ( _ terminationStatus: TerminationStatus ) {
370
+ switch terminationStatus {
371
+ case let . exited( code) :
372
+ self = . exit( code)
373
+ case let . unhandledException( code) :
374
+ self = . uncaughtSignal( code)
375
+ }
376
+ }
377
+ }
378
+ #endif
379
+
297
380
extension Processes . ExitStatus {
298
381
public init ( _ process: Process ) throws {
299
382
assert ( !process. isRunning)
0 commit comments