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 // Extend the lifetime of the pipes to avoid file descriptors being closed until the AsyncStream is finished being consumed.
86106 return try await withExtendedLifetime ( ( Pipe ( ) , Pipe ( ) ) ) { ( stdoutPipe, stderrPipe) in
@@ -110,9 +130,32 @@ extension Process {
110130 return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
111131 }
112132 }
133+ #endif
113134 }
114135
115136 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
116159 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
117160 // Extend the lifetime of the pipe to avoid file descriptors being closed until the AsyncStream is finished being consumed.
118161 return try await withExtendedLifetime ( Pipe ( ) ) { pipe in
@@ -138,6 +181,7 @@ extension Process {
138181 return ( exitStatus: exitStatus, output: Data ( output) )
139182 }
140183 }
184+ #endif
141185 }
142186
143187 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 {
203247 case exit( _ code: Int32 )
204248 case uncaughtSignal( _ signal: Int32 )
205249
206- public init ? ( rawValue: Int32 ) {
207- #if os(Windows)
208- let dwExitCode = DWORD ( bitPattern: rawValue)
250+ #if os(Windows)
251+ public init ( dwExitCode: DWORD ) {
209252 // Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
210253 if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
211254 || ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -215,6 +258,12 @@ public enum Processes: Sendable {
215258 } else {
216259 self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
217260 }
261+ }
262+ #endif
263+
264+ public init ? ( rawValue: Int32 ) {
265+ #if os(Windows)
266+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
218267 #else
219268 func WSTOPSIG( _ status: Int32 ) -> Int32 {
220269 return status >> 8
@@ -294,6 +343,25 @@ public enum Processes: Sendable {
294343 }
295344}
296345
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+
297365extension Processes . ExitStatus {
298366 public init ( _ process: Process ) throws {
299367 assert ( !process. isRunning)
0 commit comments