@@ -178,12 +178,20 @@ public final class Process: ObjectIdentifierProtocol {
178178 }
179179 }
180180
181+ // process execution mutable state
182+ private enum State {
183+ case idle
184+ case readingOutput( stdout: Thread , stderr: Thread ? )
185+ case outputReady( stdout: Result < [ UInt8 ] , Swift . Error > , stderr: Result < [ UInt8 ] , Swift . Error > )
186+ case complete( ProcessResult )
187+ }
188+
181189 /// Typealias for process id type.
182- #if !os(Windows)
190+ #if !os(Windows)
183191 public typealias ProcessID = pid_t
184- #else
192+ #else
185193 public typealias ProcessID = DWORD
186- #endif
194+ #endif
187195
188196 /// Typealias for stdout/stderr output closure.
189197 public typealias OutputClosure = ( [ UInt8 ] ) -> Void
@@ -210,45 +218,47 @@ public final class Process: ObjectIdentifierProtocol {
210218 public let workingDirectory : AbsolutePath ?
211219
212220 /// The process id of the spawned process, available after the process is launched.
213- #if os(Windows)
221+ #if os(Windows)
214222 private var _process : Foundation . Process ?
215223 public var processID : ProcessID {
216224 return DWORD ( _process? . processIdentifier ?? 0 )
217225 }
218- #else
226+ #else
219227 public private( set) var processID = ProcessID ( )
220- #endif
228+ #endif
229+
230+ // process execution mutable state
231+ private var state : State = . idle
221232
222- /// If the subprocess has launched.
223- /// Note: This property is not protected by the serial queue because it is only mutated in `launch()`, which will be
224- /// called only once.
225- public private( set) var launched = false
233+ /// Lock to protect execution state
234+ private let stateLock = Lock ( )
226235
227236 /// The result of the process execution. Available after process is terminated.
237+ /// This will block while the process is running, as such equivalent to `waitUntilExit`
238+ @available ( * , deprecated, message: " use waitUntilExit instead " )
228239 public var result : ProcessResult ? {
229- return self . serialQueue. sync {
230- self . _result
240+ return self . stateLock. withLock {
241+ switch self . state {
242+ case . complete( let result) :
243+ return result
244+ default :
245+ return nil
246+ }
231247 }
232248 }
233249
234- /// How process redirects its output.
235- public let outputRedirection : OutputRedirection
250+ // ideally we would use the state for this, but we need to access it while the waitForExit is locking state
251+ private var _launched = false
252+ private let launchedLock = Lock ( )
236253
237- /// The result of the process execution. Available after process is terminated.
238- private var _result : ProcessResult ?
239-
240- /// If redirected, stdout result and reference to the thread reading the output.
241- private var stdout : ( result: Result < [ UInt8 ] , Swift . Error > , thread: Thread ? ) = ( . success( [ ] ) , nil )
242-
243- /// If redirected, stderr result and reference to the thread reading the output.
244- private var stderr : ( result: Result < [ UInt8 ] , Swift . Error > , thread: Thread ? ) = ( . success( [ ] ) , nil )
245-
246- /// Queue to protect concurrent reads.
247- private let serialQueue = DispatchQueue ( label: " org.swift.swiftpm.process " )
254+ public var launched : Bool {
255+ return self . launchedLock. withLock {
256+ return self . _launched
257+ }
258+ }
248259
249- /// Queue to protect reading/writing on map of validated executables.
250- private static let executablesQueue = DispatchQueue (
251- label: " org.swift.swiftpm.process.findExecutable " )
260+ /// How process redirects its output.
261+ public let outputRedirection : OutputRedirection
252262
253263 /// Indicates if a new progress group is created for the child process.
254264 private let startNewProcessGroup : Bool
@@ -257,7 +267,10 @@ public final class Process: ObjectIdentifierProtocol {
257267 ///
258268 /// Key: Executable name or path.
259269 /// Value: Path to the executable, if found.
260- static private var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
270+ private static var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
271+
272+ // Lock to protect reading/writing on validatedExecutablesMap.
273+ private static let validatedExecutablesMapLock = Lock ( )
261274
262275 /// Create a new process instance.
263276 ///
@@ -348,7 +361,7 @@ public final class Process: ObjectIdentifierProtocol {
348361 }
349362 // This should cover the most common cases, i.e. when the cache is most helpful.
350363 if workingDirectory == localFileSystem. currentWorkingDirectory {
351- return Process . executablesQueue . sync {
364+ return Process . validatedExecutablesMapLock . withLock {
352365 if let value = Process . validatedExecutablesMap [ program] {
353366 return value
354367 }
@@ -364,10 +377,11 @@ public final class Process: ObjectIdentifierProtocol {
364377 /// Launch the subprocess.
365378 public func launch( ) throws {
366379 precondition ( arguments. count > 0 && !arguments[ 0 ] . isEmpty, " Need at least one argument to launch the process. " )
367- precondition ( !launched, " It is not allowed to launch the same process object again. " )
368380
369- // Set the launch bool to true.
370- launched = true
381+ self . launchedLock. withLock {
382+ precondition ( !self . _launched, " It is not allowed to launch the same process object again. " )
383+ self . _launched = true
384+ }
371385
372386 // Print the arguments if we are verbose.
373387 if self . verbose {
@@ -381,7 +395,7 @@ public final class Process: ObjectIdentifierProtocol {
381395 throw Process . Error. missingExecutableProgram ( program: executable)
382396 }
383397
384- #if os(Windows)
398+ #if os(Windows)
385399 _process = Foundation . Process ( )
386400 _process? . arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
387401 _process? . executableURL = executablePath. asURL
@@ -409,13 +423,13 @@ public final class Process: ObjectIdentifierProtocol {
409423 }
410424
411425 try _process? . run ( )
412- #else
426+ #else
413427 // Initialize the spawn attributes.
414- #if canImport(Darwin) || os(Android)
428+ #if canImport(Darwin) || os(Android)
415429 var attributes : posix_spawnattr_t ? = nil
416- #else
430+ #else
417431 var attributes = posix_spawnattr_t ( )
418- #endif
432+ #endif
419433 posix_spawnattr_init ( & attributes)
420434 defer { posix_spawnattr_destroy ( & attributes) }
421435
@@ -425,13 +439,13 @@ public final class Process: ObjectIdentifierProtocol {
425439 posix_spawnattr_setsigmask ( & attributes, & noSignals)
426440
427441 // Reset all signals to default behavior.
428- #if os(macOS)
442+ #if os(macOS)
429443 var mostSignals = sigset_t ( )
430444 sigfillset ( & mostSignals)
431445 sigdelset ( & mostSignals, SIGKILL)
432446 sigdelset ( & mostSignals, SIGSTOP)
433447 posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
434- #else
448+ #else
435449 // On Linux, this can only be used to reset signals that are legal to
436450 // modify, so we have to take care about the set we use.
437451 var mostSignals = sigset_t ( )
@@ -443,7 +457,7 @@ public final class Process: ObjectIdentifierProtocol {
443457 sigaddset ( & mostSignals, i)
444458 }
445459 posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
446- #endif
460+ #endif
447461
448462 // Set the attribute flags.
449463 var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
@@ -456,31 +470,31 @@ public final class Process: ObjectIdentifierProtocol {
456470 posix_spawnattr_setflags ( & attributes, Int16 ( flags) )
457471
458472 // Setup the file actions.
459- #if canImport(Darwin) || os(Android)
473+ #if canImport(Darwin) || os(Android)
460474 var fileActions : posix_spawn_file_actions_t ? = nil
461- #else
475+ #else
462476 var fileActions = posix_spawn_file_actions_t ( )
463- #endif
477+ #endif
464478 posix_spawn_file_actions_init ( & fileActions)
465479 defer { posix_spawn_file_actions_destroy ( & fileActions) }
466480
467481 if let workingDirectory = workingDirectory? . pathString {
468- #if os(macOS)
482+ #if os(macOS)
469483 // The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
470484 // to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
471485 // exists here to make the compiler happy.
472486 if #available( macOS 10 . 15 , * ) {
473487 posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
474488 }
475- #elseif os(Linux)
489+ #elseif os(Linux)
476490 guard SPM_posix_spawn_file_actions_addchdir_np_supported ( ) else {
477491 throw Process . Error. workingDirectoryNotSupported
478492 }
479493
480494 SPM_posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
481- #else
495+ #else
482496 throw Process . Error. workingDirectoryNotSupported
483- #endif
497+ #endif
484498 }
485499
486500 // Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
@@ -534,43 +548,84 @@ public final class Process: ObjectIdentifierProtocol {
534548 throw SystemError . posix_spawn ( rv, arguments)
535549 }
536550
537- if outputRedirection. redirectsOutput {
551+ if !outputRedirection. redirectsOutput {
552+ // no stdout or stderr in this case
553+ self . stateLock. withLock {
554+ self . state = . outputReady( stdout: . success( [ ] ) , stderr: . success( [ ] ) )
555+ }
556+ } else {
557+ var outputResult : ( stdout: Result < [ UInt8 ] , Swift . Error > ? , stderr: Result < [ UInt8 ] , Swift . Error > ? )
558+ let outputResultLock = Lock ( )
559+
538560 let outputClosures = outputRedirection. outputClosures
539561
540562 // Close the write end of the output pipe.
541563 try close ( fd: & outputPipe[ 1 ] )
542564
543565 // Create a thread and start reading the output on it.
544- var thread = Thread { [ weak self] in
566+ let stdoutThread = Thread { [ weak self] in
545567 if let readResult = self ? . readOutput ( onFD: outputPipe [ 0 ] , outputClosure: outputClosures? . stdoutClosure) {
546- self ? . stdout. result = readResult
568+ outputResultLock. withLock {
569+ if let stderrResult = outputResult. stderr {
570+ self ? . stateLock. withLock {
571+ self ? . state = . outputReady( stdout: readResult, stderr: stderrResult)
572+ }
573+ } else {
574+ outputResult. stdout = readResult
575+ }
576+ }
577+ } else if let stderrResult = ( outputResultLock. withLock { outputResult. stderr } ) {
578+ // TODO: this is more of an error
579+ self ? . stateLock. withLock {
580+ self ? . state = . outputReady( stdout: . success( [ ] ) , stderr: stderrResult)
581+ }
547582 }
548583 }
549- thread. start ( )
550- self . stdout. thread = thread
551584
552585 // Only schedule a thread for stderr if no redirect was requested.
586+ var stderrThread : Thread ? = nil
553587 if !outputRedirection. redirectStderr {
554588 // Close the write end of the stderr pipe.
555589 try close ( fd: & stderrPipe[ 1 ] )
556590
557591 // Create a thread and start reading the stderr output on it.
558- thread = Thread { [ weak self] in
592+ stderrThread = Thread { [ weak self] in
559593 if let readResult = self ? . readOutput ( onFD: stderrPipe [ 0 ] , outputClosure: outputClosures? . stderrClosure) {
560- self ? . stderr. result = readResult
594+ outputResultLock. withLock {
595+ if let stdoutResult = outputResult. stdout {
596+ self ? . stateLock. withLock {
597+ self ? . state = . outputReady( stdout: stdoutResult, stderr: readResult)
598+ }
599+ } else {
600+ outputResult. stderr = readResult
601+ }
602+ }
603+ } else if let stdoutResult = ( outputResultLock. withLock { outputResult. stdout } ) {
604+ // TODO: this is more of an error
605+ self ? . stateLock. withLock {
606+ self ? . state = . outputReady( stdout: stdoutResult, stderr: . success( [ ] ) )
607+ }
561608 }
562609 }
563- thread. start ( )
564- self . stderr. thread = thread
610+ } else {
611+ outputResultLock. withLock {
612+ outputResult. stderr = . success( [ ] ) // no stderr in this case
613+ }
614+ }
615+ // first set state then start reading threads
616+ self . stateLock. withLock {
617+ self . state = . readingOutput( stdout: stdoutThread, stderr: stderrThread)
565618 }
619+ stdoutThread. start ( )
620+ stderrThread? . start ( )
566621 }
567- #endif // POSIX implementation
622+ #endif // POSIX implementation
568623 }
569624
570625 /// Blocks the calling process until the subprocess finishes execution.
571626 @discardableResult
572627 public func waitUntilExit( ) throws -> ProcessResult {
573- #if os(Windows)
628+ #if os(Windows)
574629 precondition ( _process != nil , " The process is not yet launched. " )
575630 let p = _process!
576631 p. waitUntilExit ( )
@@ -585,19 +640,23 @@ public final class Process: ObjectIdentifierProtocol {
585640 stderrOutput: stderr. result
586641 )
587642 return executionResult
588- #else
589- return try serialQueue. sync {
590- precondition ( launched, " The process is not yet launched. " )
591-
592- // If the process has already finsihed, return it.
593- if let existingResult = _result {
594- return existingResult
595- }
596-
643+ #else
644+ self . stateLock. lock ( )
645+ switch self . state {
646+ case . idle:
647+ defer { self . stateLock. unlock ( ) }
648+ preconditionFailure ( " The process is not yet launched. " )
649+ case . complete( let result) :
650+ defer { self . stateLock. unlock ( ) }
651+ return result
652+ case . readingOutput( let stdoutThread, let stderrThread) :
653+ self . stateLock. unlock ( ) // unlock early since output read thread need to change state
597654 // If we're reading output, make sure that is finished.
598- stdout. thread? . join ( )
599- stderr. thread? . join ( )
600-
655+ stdoutThread. join ( )
656+ stderrThread? . join ( )
657+ return try self . waitUntilExit ( )
658+ case . outputReady( let stdoutResult, let stderrResult) :
659+ defer { self . stateLock. unlock ( ) }
601660 // Wait until process finishes execution.
602661 var exitStatusCode : Int32 = 0
603662 var result = waitpid ( processID, & exitStatusCode, 0 )
@@ -613,13 +672,13 @@ public final class Process: ObjectIdentifierProtocol {
613672 arguments: arguments,
614673 environment: environment,
615674 exitStatusCode: exitStatusCode,
616- output: stdout . result ,
617- stderrOutput: stderr . result
675+ output: stdoutResult ,
676+ stderrOutput: stderrResult
618677 )
619- self . _result = executionResult
678+ self . state = . complete ( executionResult)
620679 return executionResult
621680 }
622- #endif
681+ #endif
623682 }
624683
625684 #if !os(Windows)
@@ -671,16 +730,16 @@ public final class Process: ObjectIdentifierProtocol {
671730 ///
672731 /// Note: This will signal all processes in the process group.
673732 public func signal( _ signal: Int32 ) {
674- #if os(Windows)
733+ #if os(Windows)
675734 if signal == SIGINT {
676- _process? . interrupt ( )
735+ _process? . interrupt ( )
677736 } else {
678- _process? . terminate ( )
737+ _process? . terminate ( )
679738 }
680- #else
681- assert ( launched, " The process is not yet launched. " )
739+ #else
740+ assert ( self . launched, " The process is not yet launched. " )
682741 _ = TSCLibc . kill ( startNewProcessGroup ? - processID : processID, signal)
683- #endif
742+ #endif
684743 }
685744}
686745
0 commit comments