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) && (!canImport(Darwin) || os(macOS) )
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,30 @@ 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
+ var platformOptions = PlatformOptions ( )
90
+ if interruptible {
91
+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
92
+ }
93
+ let configuration = try Subprocess . Configuration (
94
+ . path( FilePath ( url. filePath. str) ) ,
95
+ arguments: . init( arguments) ,
96
+ environment: environment. map { . custom( . init( $0) ) } ?? . inherit,
97
+ workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil ,
98
+ platformOptions: platformOptions
99
+ )
100
+ let result = try await Subprocess . run ( configuration, body: { execution, inputWriter, outputReader, errorReader in
101
+ async let stdoutBytes = outputReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
102
+ async let stderrBytes = errorReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
103
+ try await inputWriter. finish ( )
104
+ return try await ( stdoutBytes, stderrBytes)
105
+ } )
106
+ return Processes . ExecutionResult ( exitStatus: . init( result. terminationStatus) , stdout: Data ( result. value. 0 ) , stderr: Data ( result. value. 1 ) )
107
+ #else
108
+ throw StubError . error ( " Process spawning is unavailable " )
109
+ #endif
110
+ #else
84
111
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85
112
let stdoutPipe = Pipe ( )
86
113
let stderrPipe = Pipe ( )
@@ -118,9 +145,39 @@ extension Process {
118
145
}
119
146
return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
120
147
}
148
+ #endif
121
149
}
122
150
123
151
public static func getMergedOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> ( exitStatus: Processes . ExitStatus , output: Data ) {
152
+ #if canImport(Subprocess)
153
+ #if !canImport(Darwin) || os(macOS)
154
+ let ( readEnd, writeEnd) = try FileDescriptor . pipe ( )
155
+ return try await readEnd. closeAfter {
156
+ // 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).
157
+ var platformOptions = PlatformOptions ( )
158
+ if interruptible {
159
+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
160
+ }
161
+ let configuration = try Subprocess . Configuration (
162
+ . path( FilePath ( url. filePath. str) ) ,
163
+ arguments: . init( arguments) ,
164
+ environment: environment. map { . custom( . init( $0) ) } ?? . inherit,
165
+ workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil ,
166
+ platformOptions: platformOptions
167
+ )
168
+ let result = try await Subprocess . run ( configuration, output: . fileDescriptor( writeEnd, closeAfterSpawningProcess: true ) , error: . fileDescriptor( writeEnd, closeAfterSpawningProcess: false ) , body: { execution in
169
+ if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
170
+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . dataStream ( ) . collect ( ) ) )
171
+ } else {
172
+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . _dataStream ( ) . collect ( ) ) )
173
+ }
174
+ } )
175
+ return ( . init( result. terminationStatus) , Data ( result. value) )
176
+ }
177
+ #else
178
+ throw StubError . error ( " Process spawning is unavailable " )
179
+ #endif
180
+ #else
124
181
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
125
182
let pipe = Pipe ( )
126
183
@@ -150,6 +207,7 @@ extension Process {
150
207
}
151
208
return ( exitStatus: exitStatus, output: Data ( output) )
152
209
}
210
+ #endif
153
211
}
154
212
155
213
private static func _getOutput< T, U> ( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? , environment: Environment ? , interruptible: Bool , setup: ( Process ) -> T , collect: @Sendable ( T) async throws -> U ) async throws -> ( exitStatus: Processes . ExitStatus , output: U ) {
@@ -215,9 +273,8 @@ public enum Processes: Sendable {
215
273
case exit( _ code: Int32 )
216
274
case uncaughtSignal( _ signal: Int32 )
217
275
218
- public init ? ( rawValue: Int32 ) {
219
- #if os(Windows)
220
- let dwExitCode = DWORD ( bitPattern: rawValue)
276
+ #if os(Windows)
277
+ public init ( dwExitCode: DWORD ) {
221
278
// Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
222
279
if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
223
280
|| ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -227,6 +284,12 @@ public enum Processes: Sendable {
227
284
} else {
228
285
self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
229
286
}
287
+ }
288
+ #endif
289
+
290
+ public init ? ( rawValue: Int32 ) {
291
+ #if os(Windows)
292
+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
230
293
#else
231
294
func WSTOPSIG( _ status: Int32 ) -> Int32 {
232
295
return status >> 8
@@ -306,6 +369,25 @@ public enum Processes: Sendable {
306
369
}
307
370
}
308
371
372
+ #if canImport(Subprocess) && (!canImport(Darwin) || os(macOS))
373
+ extension Processes . ExitStatus {
374
+ init ( _ terminationStatus: TerminationStatus ) {
375
+ switch terminationStatus {
376
+ case let . exited( code) :
377
+ self = . exit( numericCast ( code) )
378
+ case let . unhandledException( code) :
379
+ #if os(Windows)
380
+ // Currently swift-subprocess returns the original raw GetExitCodeProcess value as uncaughtSignal for all values other than zero.
381
+ // See also: https://github.com/swiftlang/swift-subprocess/issues/114
382
+ self = . init( dwExitCode: code)
383
+ #else
384
+ self = . uncaughtSignal( code)
385
+ #endif
386
+ }
387
+ }
388
+ }
389
+ #endif
390
+
309
391
extension Processes . ExitStatus {
310
392
public init ( _ process: Process ) throws {
311
393
assert ( !process. isRunning)
0 commit comments