1111//===----------------------------------------------------------------------===//
1212
1313public import Foundation
14- import SWBLibc
14+ public import SWBLibc
15+ import Synchronization
1516
16- #if os(Windows )
17- public typealias pid_t = Int32
17+ #if canImport(Subprocess) && (!canImport(Darwin) || os(macOS) )
18+ import Subprocess
1819#endif
1920
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
2629#endif
2730
2831#if (!canImport(Foundation.NSTask) || targetEnvironment(macCatalyst)) && canImport(Darwin)
@@ -64,7 +67,7 @@ public typealias Process = Foundation.Process
6467#endif
6568
6669extension Process {
67- public static var hasUnsafeWorkingDirectorySupport : Bool {
70+ fileprivate static var hasUnsafeWorkingDirectorySupport : Bool {
6871 get throws {
6972 switch try ProcessInfo . processInfo. hostOperatingSystem ( ) {
7073 case . linux:
@@ -81,6 +84,30 @@ extension Process {
8184
8285extension Process {
8386 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
84111 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85112 let stdoutPipe = Pipe ( )
86113 let stderrPipe = Pipe ( )
@@ -118,9 +145,39 @@ extension Process {
118145 }
119146 return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
120147 }
148+ #endif
121149 }
122150
123151 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
124181 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
125182 let pipe = Pipe ( )
126183
@@ -150,6 +207,7 @@ extension Process {
150207 }
151208 return ( exitStatus: exitStatus, output: Data ( output) )
152209 }
210+ #endif
153211 }
154212
155213 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 {
215273 case exit( _ code: Int32 )
216274 case uncaughtSignal( _ signal: Int32 )
217275
218- public init ? ( rawValue: Int32 ) {
219- #if os(Windows)
220- let dwExitCode = DWORD ( bitPattern: rawValue)
276+ #if os(Windows)
277+ public init ( dwExitCode: DWORD ) {
221278 // Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
222279 if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
223280 || ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -227,6 +284,12 @@ public enum Processes: Sendable {
227284 } else {
228285 self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
229286 }
287+ }
288+ #endif
289+
290+ public init ? ( rawValue: Int32 ) {
291+ #if os(Windows)
292+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
230293 #else
231294 func WSTOPSIG( _ status: Int32 ) -> Int32 {
232295 return status >> 8
@@ -306,6 +369,25 @@ public enum Processes: Sendable {
306369 }
307370}
308371
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+
309391extension Processes . ExitStatus {
310392 public init ( _ process: Process ) throws {
311393 assert ( !process. isRunning)
0 commit comments