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,23 @@ 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 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 , platformOptions: platformOptions, body: { execution, inputWriter, outputReader, errorReader in
94
+ try await inputWriter. finish ( )
95
+ async let stdoutBytes = outputReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
96
+ async let stderrBytes = errorReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
97
+ return try await ( stdoutBytes, stderrBytes)
98
+ } )
99
+ return Processes . ExecutionResult ( exitStatus: . init( result. terminationStatus) , stdout: Data ( result. value. 0 ) , stderr: Data ( result. value. 1 ) )
100
+ #else
101
+ throw StubError . error ( " Process spawning is unavailable " )
102
+ #endif
103
+ #else
84
104
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85
105
// Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed.
86
106
return try await withExtendedLifetime ( ( Pipe ( ) , Pipe ( ) ) ) { ( stdoutPipe, stderrPipe) in
@@ -110,9 +130,32 @@ extension Process {
110
130
return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
111
131
}
112
132
}
133
+ #endif
113
134
}
114
135
115
136
public static func getMergedOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> ( exitStatus: Processes . ExitStatus , output: Data ) {
137
+ #if canImport(Subprocess)
138
+ #if !canImport(Darwin) || os(macOS)
139
+ let ( readEnd, writeEnd) = try FileDescriptor . pipe ( )
140
+ return try await readEnd. closeAfter {
141
+ // 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).
142
+ var platformOptions = PlatformOptions ( )
143
+ if interruptible {
144
+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
145
+ }
146
+ 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 , platformOptions: platformOptions, output: . fileDescriptor( writeEnd, closeAfterSpawningProcess: true ) , error: . fileDescriptor( writeEnd, closeAfterSpawningProcess: false ) , body: { execution in
147
+ if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
148
+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . dataStream ( ) . collect ( ) ) )
149
+ } else {
150
+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . _dataStream ( ) . collect ( ) ) )
151
+ }
152
+ } )
153
+ return ( . init( result. terminationStatus) , Data ( result. value) )
154
+ }
155
+ #else
156
+ throw StubError . error ( " Process spawning is unavailable " )
157
+ #endif
158
+ #else
116
159
if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
117
160
// Extend the lifetime of the pipe to avoid file descriptors being closed until the AsyncStream is finished being consumed.
118
161
return try await withExtendedLifetime ( Pipe ( ) ) { pipe in
@@ -138,6 +181,7 @@ extension Process {
138
181
return ( exitStatus: exitStatus, output: Data ( output) )
139
182
}
140
183
}
184
+ #endif
141
185
}
142
186
143
187
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 ) {
@@ -203,9 +247,8 @@ public enum Processes: Sendable {
203
247
case exit( _ code: Int32 )
204
248
case uncaughtSignal( _ signal: Int32 )
205
249
206
- public init ? ( rawValue: Int32 ) {
207
- #if os(Windows)
208
- let dwExitCode = DWORD ( bitPattern: rawValue)
250
+ #if os(Windows)
251
+ public init ( dwExitCode: DWORD ) {
209
252
// Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
210
253
if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
211
254
|| ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -215,6 +258,12 @@ public enum Processes: Sendable {
215
258
} else {
216
259
self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
217
260
}
261
+ }
262
+ #endif
263
+
264
+ public init ? ( rawValue: Int32 ) {
265
+ #if os(Windows)
266
+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
218
267
#else
219
268
func WSTOPSIG( _ status: Int32 ) -> Int32 {
220
269
return status >> 8
@@ -294,6 +343,25 @@ public enum Processes: Sendable {
294
343
}
295
344
}
296
345
346
+ #if canImport(Subprocess)
347
+ extension Processes . ExitStatus {
348
+ init ( _ terminationStatus: TerminationStatus ) {
349
+ switch terminationStatus {
350
+ case let . exited( code) :
351
+ self = . exit( numericCast ( code) )
352
+ case let . unhandledException( code) :
353
+ #if os(Windows)
354
+ // Currently swift-subprocess returns the original raw GetExitCodeProcess value as uncaughtSignal for all values other than zero.
355
+ // See also: https://github.com/swiftlang/swift-subprocess/issues/114
356
+ self = . init( dwExitCode: code)
357
+ #else
358
+ self = . uncaughtSignal( code)
359
+ #endif
360
+ }
361
+ }
362
+ }
363
+ #endif
364
+
297
365
extension Processes . ExitStatus {
298
366
public init ( _ process: Process ) throws {
299
367
assert ( !process. isRunning)
0 commit comments