@@ -180,10 +180,10 @@ public final class Process {
180180 // process execution mutable state
181181 private enum State {
182182 case idle
183- case readingOutputThread( stdout: Thread , stderr: Thread ? )
184- case readingOutputPipe( sync: DispatchGroup )
183+ case readingOutput( sync: DispatchGroup )
185184 case outputReady( stdout: Result < [ UInt8 ] , Swift . Error > , stderr: Result < [ UInt8 ] , Swift . Error > )
186185 case complete( ProcessResult )
186+ case failed( Swift . Error )
187187 }
188188
189189 /// Typealias for process id type.
@@ -230,6 +230,8 @@ public final class Process {
230230 // process execution mutable state
231231 private var state : State = . idle
232232 private let stateLock = Lock ( )
233+ private static let sharedCompletionQueue = DispatchQueue ( label: " org.swift.tools-support-core.process-completion " )
234+ private var completionQueue = Process . sharedCompletionQueue
233235
234236 /// The result of the process execution. Available after process is terminated.
235237 /// This will block while the process is awaiting result
@@ -395,13 +397,14 @@ public final class Process {
395397 }
396398
397399 #if os(Windows)
398- _process = Foundation . Process ( )
399- _process? . arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
400- _process? . executableURL = executablePath. asURL
401- _process? . environment = environment
400+ let process = Foundation . Process ( )
401+ _process = process
402+ process. arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
403+ process. executableURL = executablePath. asURL
404+ process. environment = environment
402405
403406 let stdinPipe = Pipe ( )
404- _process ? . standardInput = stdinPipe
407+ process . standardInput = stdinPipe
405408
406409 let group = DispatchGroup ( )
407410
@@ -445,25 +448,25 @@ public final class Process {
445448 }
446449 }
447450
448- _process ? . standardOutput = stdoutPipe
449- _process ? . standardError = stderrPipe
451+ process . standardOutput = stdoutPipe
452+ process . standardError = stderrPipe
450453 }
451454
452455 // first set state then start reading threads
453456 let sync = DispatchGroup ( )
454457 sync. enter ( )
455458 self . stateLock. withLock {
456- self . state = . readingOutputPipe ( sync: sync)
459+ self . state = . readingOutput ( sync: sync)
457460 }
458461
459- group. notify ( queue: . global ( ) ) {
462+ group. notify ( queue: self . completionQueue ) {
460463 self . stateLock. withLock {
461464 self . state = . outputReady( stdout: . success( stdout) , stderr: . success( stderr) )
462465 }
463466 sync. leave ( )
464467 }
465468
466- try _process ? . run ( )
469+ try process . run ( )
467470 return stdinPipe. fileHandleForWriting
468471 #elseif (!canImport(Darwin) || os(macOS))
469472 // Initialize the spawn attributes.
@@ -596,6 +599,7 @@ public final class Process {
596599 // Close the local read end of the input pipe.
597600 try close ( fd: stdinPipe [ 0 ] )
598601
602+ let group = DispatchGroup ( )
599603 if !outputRedirection. redirectsOutput {
600604 // no stdout or stderr in this case
601605 self . stateLock. withLock {
@@ -611,6 +615,7 @@ public final class Process {
611615 try close ( fd: outputPipe [ 1 ] )
612616
613617 // Create a thread and start reading the output on it.
618+ group. enter ( )
614619 let stdoutThread = Thread { [ weak self] in
615620 if let readResult = self ? . readOutput ( onFD: outputPipe [ 0 ] , outputClosure: outputClosures? . stdoutClosure) {
616621 pendingLock. withLock {
@@ -622,11 +627,13 @@ public final class Process {
622627 pending = readResult
623628 }
624629 }
630+ group. leave ( )
625631 } else if let stderrResult = ( pendingLock. withLock { pending } ) {
626632 // TODO: this is more of an error
627633 self ? . stateLock. withLock {
628634 self ? . state = . outputReady( stdout: . success( [ ] ) , stderr: stderrResult)
629635 }
636+ group. leave ( )
630637 }
631638 }
632639
@@ -637,6 +644,7 @@ public final class Process {
637644 try close ( fd: stderrPipe [ 1 ] )
638645
639646 // Create a thread and start reading the stderr output on it.
647+ group. enter ( )
640648 stderrThread = Thread { [ weak self] in
641649 if let readResult = self ? . readOutput ( onFD: stderrPipe [ 0 ] , outputClosure: outputClosures? . stderrClosure) {
642650 pendingLock. withLock {
@@ -648,22 +656,26 @@ public final class Process {
648656 pending = readResult
649657 }
650658 }
659+ group. leave ( )
651660 } else if let stdoutResult = ( pendingLock. withLock { pending } ) {
652661 // TODO: this is more of an error
653662 self ? . stateLock. withLock {
654663 self ? . state = . outputReady( stdout: stdoutResult, stderr: . success( [ ] ) )
655664 }
665+ group. leave ( )
656666 }
657667 }
658668 } else {
659669 pendingLock. withLock {
660670 pending = . success( [ ] ) // no stderr in this case
661671 }
662672 }
673+
663674 // first set state then start reading threads
664675 self . stateLock. withLock {
665- self . state = . readingOutputThread ( stdout : stdoutThread , stderr : stderrThread )
676+ self . state = . readingOutput ( sync : group )
666677 }
678+
667679 stdoutThread. start ( )
668680 stderrThread? . start ( )
669681 }
@@ -677,24 +689,35 @@ public final class Process {
677689 /// Blocks the calling process until the subprocess finishes execution.
678690 @discardableResult
679691 public func waitUntilExit( ) throws -> ProcessResult {
692+ let group = DispatchGroup ( )
693+ group. enter ( )
694+ var processResult : Result < ProcessResult , Swift . Error > ?
695+ self . waitUntilExit ( ) { result in
696+ processResult = result
697+ group. leave ( )
698+ }
699+ group. wait ( )
700+ return try processResult. unsafelyUnwrapped. get ( )
701+ }
702+
703+ /// Executes the process I/O state machine, calling completion block when finished.
704+ private func waitUntilExit( _ completion: @escaping ( Result < ProcessResult , Swift . Error > ) -> Void ) {
680705 self . stateLock. lock ( )
681706 switch self . state {
682707 case . idle:
683708 defer { self . stateLock. unlock ( ) }
684709 preconditionFailure ( " The process is not yet launched. " )
685710 case . complete( let result) :
686- defer { self . stateLock. unlock ( ) }
687- return result
688- case . readingOutputThread( let stdoutThread, let stderrThread) :
689- self . stateLock. unlock ( ) // unlock early since output read thread need to change state
690- // If we're reading output, make sure that is finished.
691- stdoutThread. join ( )
692- stderrThread? . join ( )
693- return try self . waitUntilExit ( )
694- case . readingOutputPipe( let sync) :
695- self . stateLock. unlock ( ) // unlock early since output read thread need to change state
696- sync. wait ( )
697- return try self . waitUntilExit ( )
711+ self . stateLock. unlock ( )
712+ completion ( . success( result) )
713+ case . failed( let error) :
714+ self . stateLock. unlock ( )
715+ completion ( . failure( error) )
716+ case . readingOutput( let sync) :
717+ self . stateLock. unlock ( )
718+ sync. notify ( queue: self . completionQueue) {
719+ self . waitUntilExit ( completion)
720+ }
698721 case . outputReady( let stdoutResult, let stderrResult) :
699722 defer { self . stateLock. unlock ( ) }
700723 // Wait until process finishes execution.
@@ -710,7 +733,7 @@ public final class Process {
710733 result = waitpid ( processID, & exitStatusCode, 0 )
711734 }
712735 if result == - 1 {
713- throw SystemError . waitpid ( errno)
736+ self . state = . failed ( SystemError . waitpid ( errno) )
714737 }
715738 #endif
716739
@@ -723,7 +746,9 @@ public final class Process {
723746 stderrOutput: stderrResult
724747 )
725748 self . state = . complete( executionResult)
726- return executionResult
749+ self . completionQueue. async {
750+ self . waitUntilExit ( completion)
751+ }
727752 }
728753 }
729754
@@ -790,6 +815,25 @@ public final class Process {
790815}
791816
792817extension Process {
818+ /// Execute a subprocess and calls completion block when it finishes execution
819+ ///
820+ /// - Parameters:
821+ /// - arguments: The arguments for the subprocess.
822+ /// - environment: The environment to pass to subprocess. By default the current process environment
823+ /// will be inherited.
824+ /// - Returns: The process result.
825+ static public func popen( arguments: [ String ] , environment: [ String : String ] = ProcessEnv . vars,
826+ queue: DispatchQueue ? = nil , completion: @escaping ( Result < ProcessResult , Swift . Error > ) -> Void ) {
827+ do {
828+ let process = Process ( arguments: arguments, environment: environment, outputRedirection: . collect)
829+ process. completionQueue = queue ?? Self . sharedCompletionQueue
830+ try process. launch ( )
831+ process. waitUntilExit ( completion)
832+ } catch {
833+ completion ( . failure( error) )
834+ }
835+ }
836+
793837 /// Execute a subprocess and block until it finishes execution
794838 ///
795839 /// - Parameters:
0 commit comments