@@ -178,20 +178,12 @@ 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-
189181 /// Typealias for process id type.
190- #if !os(Windows)
182+ #if !os(Windows)
191183 public typealias ProcessID = pid_t
192- #else
184+ #else
193185 public typealias ProcessID = DWORD
194- #endif
186+ #endif
195187
196188 /// Typealias for stdout/stderr output closure.
197189 public typealias OutputClosure = ( [ UInt8 ] ) -> Void
@@ -218,59 +210,54 @@ public final class Process: ObjectIdentifierProtocol {
218210 public let workingDirectory : AbsolutePath ?
219211
220212 /// The process id of the spawned process, available after the process is launched.
221- #if os(Windows)
213+ #if os(Windows)
222214 private var _process : Foundation . Process ?
223215 public var processID : ProcessID {
224216 return DWORD ( _process? . processIdentifier ?? 0 )
225217 }
226- #else
218+ #else
227219 public private( set) var processID = ProcessID ( )
228- #endif
229-
230- // process execution mutable state
231- private var state : State = . idle
220+ #endif
232221
233- /// Lock to protect execution state
234- private let stateLock = Lock ( )
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
235226
236227 /// 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 " )
239228 public var result : ProcessResult ? {
240- return self . stateLock. withLock {
241- switch self . state {
242- case . complete( let result) :
243- return result
244- default :
245- return nil
246- }
247- }
248- }
249-
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 ( )
253-
254- public var launched : Bool {
255- return self . launchedLock. withLock {
256- return self . _launched
229+ return self . serialQueue. sync {
230+ self . _result
257231 }
258232 }
259233
260234 /// How process redirects its output.
261235 public let outputRedirection : OutputRedirection
262236
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 " )
248+
249+ /// Queue to protect reading/writing on map of validated executables.
250+ private static let executablesQueue = DispatchQueue (
251+ label: " org.swift.swiftpm.process.findExecutable " )
252+
263253 /// Indicates if a new progress group is created for the child process.
264254 private let startNewProcessGroup : Bool
265255
266256 /// Cache of validated executables.
267257 ///
268258 /// Key: Executable name or path.
269259 /// Value: Path to the executable, if found.
270- private static var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
271-
272- // Lock to protect reading/writing on validatedExecutablesMap.
273- private static let validatedExecutablesMapLock = Lock ( )
260+ static private var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
274261
275262 /// Create a new process instance.
276263 ///
@@ -361,7 +348,7 @@ public final class Process: ObjectIdentifierProtocol {
361348 }
362349 // This should cover the most common cases, i.e. when the cache is most helpful.
363350 if workingDirectory == localFileSystem. currentWorkingDirectory {
364- return Process . validatedExecutablesMapLock . withLock {
351+ return Process . executablesQueue . sync {
365352 if let value = Process . validatedExecutablesMap [ program] {
366353 return value
367354 }
@@ -377,11 +364,10 @@ public final class Process: ObjectIdentifierProtocol {
377364 /// Launch the subprocess.
378365 public func launch( ) throws {
379366 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. " )
380368
381- self . launchedLock. withLock {
382- precondition ( !self . _launched, " It is not allowed to launch the same process object again. " )
383- self . _launched = true
384- }
369+ // Set the launch bool to true.
370+ launched = true
385371
386372 // Print the arguments if we are verbose.
387373 if self . verbose {
@@ -395,7 +381,7 @@ public final class Process: ObjectIdentifierProtocol {
395381 throw Process . Error. missingExecutableProgram ( program: executable)
396382 }
397383
398- #if os(Windows)
384+ #if os(Windows)
399385 _process = Foundation . Process ( )
400386 _process? . arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
401387 _process? . executableURL = executablePath. asURL
@@ -423,13 +409,13 @@ public final class Process: ObjectIdentifierProtocol {
423409 }
424410
425411 try _process? . run ( )
426- #else
412+ #else
427413 // Initialize the spawn attributes.
428- #if canImport(Darwin) || os(Android)
414+ #if canImport(Darwin) || os(Android)
429415 var attributes : posix_spawnattr_t ? = nil
430- #else
416+ #else
431417 var attributes = posix_spawnattr_t ( )
432- #endif
418+ #endif
433419 posix_spawnattr_init ( & attributes)
434420 defer { posix_spawnattr_destroy ( & attributes) }
435421
@@ -439,13 +425,13 @@ public final class Process: ObjectIdentifierProtocol {
439425 posix_spawnattr_setsigmask ( & attributes, & noSignals)
440426
441427 // Reset all signals to default behavior.
442- #if os(macOS)
428+ #if os(macOS)
443429 var mostSignals = sigset_t ( )
444430 sigfillset ( & mostSignals)
445431 sigdelset ( & mostSignals, SIGKILL)
446432 sigdelset ( & mostSignals, SIGSTOP)
447433 posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
448- #else
434+ #else
449435 // On Linux, this can only be used to reset signals that are legal to
450436 // modify, so we have to take care about the set we use.
451437 var mostSignals = sigset_t ( )
@@ -457,7 +443,7 @@ public final class Process: ObjectIdentifierProtocol {
457443 sigaddset ( & mostSignals, i)
458444 }
459445 posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
460- #endif
446+ #endif
461447
462448 // Set the attribute flags.
463449 var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
@@ -470,31 +456,31 @@ public final class Process: ObjectIdentifierProtocol {
470456 posix_spawnattr_setflags ( & attributes, Int16 ( flags) )
471457
472458 // Setup the file actions.
473- #if canImport(Darwin) || os(Android)
459+ #if canImport(Darwin) || os(Android)
474460 var fileActions : posix_spawn_file_actions_t ? = nil
475- #else
461+ #else
476462 var fileActions = posix_spawn_file_actions_t ( )
477- #endif
463+ #endif
478464 posix_spawn_file_actions_init ( & fileActions)
479465 defer { posix_spawn_file_actions_destroy ( & fileActions) }
480466
481467 if let workingDirectory = workingDirectory? . pathString {
482- #if os(macOS)
468+ #if os(macOS)
483469 // The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
484470 // to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
485471 // exists here to make the compiler happy.
486472 if #available( macOS 10 . 15 , * ) {
487473 posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
488474 }
489- #elseif os(Linux)
475+ #elseif os(Linux)
490476 guard SPM_posix_spawn_file_actions_addchdir_np_supported ( ) else {
491477 throw Process . Error. workingDirectoryNotSupported
492478 }
493479
494480 SPM_posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
495- #else
481+ #else
496482 throw Process . Error. workingDirectoryNotSupported
497- #endif
483+ #endif
498484 }
499485
500486 // Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
@@ -548,84 +534,43 @@ public final class Process: ObjectIdentifierProtocol {
548534 throw SystemError . posix_spawn ( rv, arguments)
549535 }
550536
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-
537+ if outputRedirection. redirectsOutput {
560538 let outputClosures = outputRedirection. outputClosures
561539
562540 // Close the write end of the output pipe.
563541 try close ( fd: & outputPipe[ 1 ] )
564542
565543 // Create a thread and start reading the output on it.
566- let stdoutThread = Thread { [ weak self] in
544+ var thread = Thread { [ weak self] in
567545 if let readResult = self ? . readOutput ( onFD: outputPipe [ 0 ] , outputClosure: outputClosures? . stdoutClosure) {
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- }
546+ self ? . stdout. result = readResult
582547 }
583548 }
549+ thread. start ( )
550+ self . stdout. thread = thread
584551
585552 // Only schedule a thread for stderr if no redirect was requested.
586- var stderrThread : Thread ? = nil
587553 if !outputRedirection. redirectStderr {
588554 // Close the write end of the stderr pipe.
589555 try close ( fd: & stderrPipe[ 1 ] )
590556
591557 // Create a thread and start reading the stderr output on it.
592- stderrThread = Thread { [ weak self] in
558+ thread = Thread { [ weak self] in
593559 if let readResult = self ? . readOutput ( onFD: stderrPipe [ 0 ] , outputClosure: outputClosures? . stderrClosure) {
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- }
560+ self ? . stderr. result = readResult
608561 }
609562 }
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)
563+ thread. start ( )
564+ self . stderr. thread = thread
618565 }
619- stdoutThread. start ( )
620- stderrThread? . start ( )
621566 }
622- #endif // POSIX implementation
567+ #endif // POSIX implementation
623568 }
624569
625570 /// Blocks the calling process until the subprocess finishes execution.
626571 @discardableResult
627572 public func waitUntilExit( ) throws -> ProcessResult {
628- #if os(Windows)
573+ #if os(Windows)
629574 precondition ( _process != nil , " The process is not yet launched. " )
630575 let p = _process!
631576 p. waitUntilExit ( )
@@ -640,23 +585,19 @@ public final class Process: ObjectIdentifierProtocol {
640585 stderrOutput: stderr. result
641586 )
642587 return executionResult
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
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+
654597 // If we're reading output, make sure that is finished.
655- stdoutThread. join ( )
656- stderrThread? . join ( )
657- return try self . waitUntilExit ( )
658- case . outputReady( let stdoutResult, let stderrResult) :
659- defer { self . stateLock. unlock ( ) }
598+ stdout. thread? . join ( )
599+ stderr. thread? . join ( )
600+
660601 // Wait until process finishes execution.
661602 var exitStatusCode : Int32 = 0
662603 var result = waitpid ( processID, & exitStatusCode, 0 )
@@ -672,13 +613,13 @@ public final class Process: ObjectIdentifierProtocol {
672613 arguments: arguments,
673614 environment: environment,
674615 exitStatusCode: exitStatusCode,
675- output: stdoutResult ,
676- stderrOutput: stderrResult
616+ output: stdout . result ,
617+ stderrOutput: stderr . result
677618 )
678- self . state = . complete ( executionResult)
619+ self . _result = executionResult
679620 return executionResult
680621 }
681- #endif
622+ #endif
682623 }
683624
684625 #if !os(Windows)
@@ -730,16 +671,16 @@ public final class Process: ObjectIdentifierProtocol {
730671 ///
731672 /// Note: This will signal all processes in the process group.
732673 public func signal( _ signal: Int32 ) {
733- #if os(Windows)
674+ #if os(Windows)
734675 if signal == SIGINT {
735- _process? . interrupt ( )
676+ _process? . interrupt ( )
736677 } else {
737- _process? . terminate ( )
678+ _process? . terminate ( )
738679 }
739- #else
740- assert ( self . launched, " The process is not yet launched. " )
680+ #else
681+ assert ( launched, " The process is not yet launched. " )
741682 _ = TSCLibc . kill ( startNewProcessGroup ? - processID : processID, signal)
742- #endif
683+ #endif
743684 }
744685}
745686
0 commit comments