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 )
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,23 @@ 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 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
84104 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85105 let stdoutPipe = Pipe ( )
86106 let stderrPipe = Pipe ( )
@@ -118,9 +138,32 @@ extension Process {
118138 }
119139 return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
120140 }
141+ #endif
121142 }
122143
123144 public static func getMergedOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> ( exitStatus: Processes . ExitStatus , output: Data ) {
145+ #if canImport(Subprocess)
146+ #if !canImport(Darwin) || os(macOS)
147+ let ( readEnd, writeEnd) = try FileDescriptor . pipe ( )
148+ return try await readEnd. closeAfter {
149+ // 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).
150+ var platformOptions = PlatformOptions ( )
151+ if interruptible {
152+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
153+ }
154+ 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
155+ if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
156+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . dataStream ( ) . collect ( ) ) )
157+ } else {
158+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . _dataStream ( ) . collect ( ) ) )
159+ }
160+ } )
161+ return ( . init( result. terminationStatus) , Data ( result. value) )
162+ }
163+ #else
164+ throw StubError . error ( " Process spawning is unavailable " )
165+ #endif
166+ #else
124167 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
125168 let pipe = Pipe ( )
126169
@@ -150,6 +193,7 @@ extension Process {
150193 }
151194 return ( exitStatus: exitStatus, output: Data ( output) )
152195 }
196+ #endif
153197 }
154198
155199 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 ) {
@@ -215,9 +259,8 @@ public enum Processes: Sendable {
215259 case exit( _ code: Int32 )
216260 case uncaughtSignal( _ signal: Int32 )
217261
218- public init ? ( rawValue: Int32 ) {
219- #if os(Windows)
220- let dwExitCode = DWORD ( bitPattern: rawValue)
262+ #if os(Windows)
263+ public init ( dwExitCode: DWORD ) {
221264 // Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
222265 if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
223266 || ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -227,6 +270,12 @@ public enum Processes: Sendable {
227270 } else {
228271 self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
229272 }
273+ }
274+ #endif
275+
276+ public init ? ( rawValue: Int32 ) {
277+ #if os(Windows)
278+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
230279 #else
231280 func WSTOPSIG( _ status: Int32 ) -> Int32 {
232281 return status >> 8
@@ -306,6 +355,25 @@ public enum Processes: Sendable {
306355 }
307356}
308357
358+ #if canImport(Subprocess)
359+ extension Processes . ExitStatus {
360+ init ( _ terminationStatus: TerminationStatus ) {
361+ switch terminationStatus {
362+ case let . exited( code) :
363+ self = . exit( numericCast ( code) )
364+ case let . unhandledException( code) :
365+ #if os(Windows)
366+ // Currently swift-subprocess returns the original raw GetExitCodeProcess value as uncaughtSignal for all values other than zero.
367+ // See also: https://github.com/swiftlang/swift-subprocess/issues/114
368+ self = . init( dwExitCode: code)
369+ #else
370+ self = . uncaughtSignal( code)
371+ #endif
372+ }
373+ }
374+ }
375+ #endif
376+
309377extension Processes . ExitStatus {
310378 public init ( _ process: Process ) throws {
311379 assert ( !process. isRunning)
0 commit comments