@@ -178,6 +178,15 @@ public final class Process: ObjectIdentifierProtocol {
178
178
}
179
179
}
180
180
181
+ // process execution mutable state
182
+ private enum State {
183
+ case idle
184
+ case readingOutputThread( stdout: Thread , stderr: Thread ? )
185
+ case readingOutputPipe( sync: DispatchGroup )
186
+ case outputReady( stdout: Result < [ UInt8 ] , Swift . Error > , stderr: Result < [ UInt8 ] , Swift . Error > )
187
+ case complete( ProcessResult )
188
+ }
189
+
181
190
/// Typealias for process id type.
182
191
#if !os(Windows)
183
192
public typealias ProcessID = pid_t
@@ -219,36 +228,36 @@ public final class Process: ObjectIdentifierProtocol {
219
228
public private( set) var processID = ProcessID ( )
220
229
#endif
221
230
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
231
+ // process execution mutable state
232
+ private var state : State = . idle
233
+ private let stateLock = Lock ( )
226
234
227
235
/// The result of the process execution. Available after process is terminated.
236
+ /// This will block while the process is awaiting result
237
+ @available ( * , deprecated, message: " use waitUntilExit instead " )
228
238
public var result : ProcessResult ? {
229
- return self . serialQueue. sync {
230
- self . _result
239
+ return self . stateLock. withLock {
240
+ switch self . state {
241
+ case . complete( let result) :
242
+ return result
243
+ default :
244
+ return nil
245
+ }
231
246
}
232
247
}
233
248
234
- /// How process redirects its output.
235
- public let outputRedirection : OutputRedirection
249
+ // ideally we would use the state for this, but we need to access it while the waitForExit is locking state
250
+ private var _launched = false
251
+ private let launchedLock = Lock ( )
236
252
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 " )
253
+ public var launched : Bool {
254
+ return self . launchedLock. withLock {
255
+ return self . _launched
256
+ }
257
+ }
248
258
249
- /// Queue to protect reading/writing on map of validated executables.
250
- private static let executablesQueue = DispatchQueue (
251
- label: " org.swift.swiftpm.process.findExecutable " )
259
+ /// How process redirects its output.
260
+ public let outputRedirection : OutputRedirection
252
261
253
262
/// Indicates if a new progress group is created for the child process.
254
263
private let startNewProcessGroup : Bool
@@ -257,7 +266,8 @@ public final class Process: ObjectIdentifierProtocol {
257
266
///
258
267
/// Key: Executable name or path.
259
268
/// Value: Path to the executable, if found.
260
- static private var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
269
+ private static var validatedExecutablesMap = [ String: AbsolutePath? ] ( )
270
+ private static let validatedExecutablesMapLock = Lock ( )
261
271
262
272
/// Create a new process instance.
263
273
///
@@ -348,7 +358,7 @@ public final class Process: ObjectIdentifierProtocol {
348
358
}
349
359
// This should cover the most common cases, i.e. when the cache is most helpful.
350
360
if workingDirectory == localFileSystem. currentWorkingDirectory {
351
- return Process . executablesQueue . sync {
361
+ return Process . validatedExecutablesMapLock . withLock {
352
362
if let value = Process . validatedExecutablesMap [ program] {
353
363
return value
354
364
}
@@ -367,10 +377,11 @@ public final class Process: ObjectIdentifierProtocol {
367
377
@discardableResult
368
378
public func launch( ) throws -> WritableByteStream {
369
379
precondition ( arguments. count > 0 && !arguments[ 0 ] . isEmpty, " Need at least one argument to launch the process. " )
370
- precondition ( !launched, " It is not allowed to launch the same process object again. " )
371
380
372
- // Set the launch bool to true.
373
- 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
+ }
374
385
375
386
// Print the arguments if we are verbose.
376
387
if self . verbose {
@@ -393,30 +404,69 @@ public final class Process: ObjectIdentifierProtocol {
393
404
let stdinPipe = Pipe ( )
394
405
_process? . standardInput = stdinPipe
395
406
407
+ let group = DispatchGroup ( )
408
+
409
+ var stdout : [ UInt8 ] = [ ]
410
+ let stdoutLock = Lock ( )
411
+
412
+ var stderr : [ UInt8 ] = [ ]
413
+ let stderrLock = Lock ( )
414
+
396
415
if outputRedirection. redirectsOutput {
397
416
let stdoutPipe = Pipe ( )
398
417
let stderrPipe = Pipe ( )
418
+
419
+ group. enter ( )
399
420
stdoutPipe. fileHandleForReading. readabilityHandler = { ( fh : FileHandle ) -> Void in
400
- let contents = fh. readDataToEndOfFile ( )
401
- self . outputRedirection. outputClosures? . stdoutClosure ( [ UInt8] ( contents) )
402
- if case . success( let data) = self . stdout. result {
403
- self . stdout. result = . success( data + contents)
421
+ let data = fh. availableData
422
+ if ( data. count == 0 ) {
423
+ stdoutPipe. fileHandleForReading. readabilityHandler = nil
424
+ group. leave ( )
425
+ } else {
426
+ let contents = data. withUnsafeBytes { Array < UInt8 > ( $0) }
427
+ self . outputRedirection. outputClosures? . stdoutClosure ( contents)
428
+ stdoutLock. withLock {
429
+ stdout += contents
430
+ }
404
431
}
405
432
}
433
+
434
+ group. enter ( )
406
435
stderrPipe. fileHandleForReading. readabilityHandler = { ( fh : FileHandle ) -> Void in
407
- let contents = fh. readDataToEndOfFile ( )
408
- self . outputRedirection. outputClosures? . stderrClosure ( [ UInt8] ( contents) )
409
- if case . success( let data) = self . stderr. result {
410
- self . stderr. result = . success( data + contents)
436
+ let data = fh. availableData
437
+ if ( data. count == 0 ) {
438
+ stderrPipe. fileHandleForReading. readabilityHandler = nil
439
+ group. leave ( )
440
+ } else {
441
+ let contents = data. withUnsafeBytes { Array < UInt8 > ( $0) }
442
+ self . outputRedirection. outputClosures? . stderrClosure ( contents)
443
+ stderrLock. withLock {
444
+ stderr += contents
445
+ }
411
446
}
412
447
}
448
+
413
449
_process? . standardOutput = stdoutPipe
414
450
_process? . standardError = stderrPipe
415
451
}
416
452
453
+ // first set state then start reading threads
454
+ let sync = DispatchGroup ( )
455
+ sync. enter ( )
456
+ self . stateLock. withLock {
457
+ self . state = . readingOutputPipe( sync: sync)
458
+ }
459
+
460
+ group. notify ( queue: . global( ) ) {
461
+ self . stateLock. withLock {
462
+ self . state = . outputReady( stdout: . success( stdout) , stderr: . success( stderr) )
463
+ }
464
+ sync. leave ( )
465
+ }
466
+
417
467
try _process? . run ( )
418
468
return stdinPipe. fileHandleForWriting
419
- #else
469
+ #else
420
470
// Initialize the spawn attributes.
421
471
#if canImport(Darwin) || os(Android)
422
472
var attributes : posix_spawnattr_t ? = nil
@@ -547,72 +597,112 @@ public final class Process: ObjectIdentifierProtocol {
547
597
// Close the local read end of the input pipe.
548
598
try close ( fd: stdinPipe [ 0 ] )
549
599
550
- if outputRedirection. redirectsOutput {
600
+ if !outputRedirection. redirectsOutput {
601
+ // no stdout or stderr in this case
602
+ self . stateLock. withLock {
603
+ self . state = . outputReady( stdout: . success( [ ] ) , stderr: . success( [ ] ) )
604
+ }
605
+ } else {
606
+ var pending : Result < [ UInt8 ] , Swift . Error > ?
607
+ let pendingLock = Lock ( )
608
+
551
609
let outputClosures = outputRedirection. outputClosures
552
610
553
611
// Close the local write end of the output pipe.
554
612
try close ( fd: outputPipe [ 1 ] )
555
613
556
614
// Create a thread and start reading the output on it.
557
- var thread = Thread { [ weak self] in
615
+ let stdoutThread = Thread { [ weak self] in
558
616
if let readResult = self ? . readOutput ( onFD: outputPipe [ 0 ] , outputClosure: outputClosures? . stdoutClosure) {
559
- self ? . stdout. result = readResult
617
+ pendingLock. withLock {
618
+ if let stderrResult = pending {
619
+ self ? . stateLock. withLock {
620
+ self ? . state = . outputReady( stdout: readResult, stderr: stderrResult)
621
+ }
622
+ } else {
623
+ pending = readResult
624
+ }
625
+ }
626
+ } else if let stderrResult = ( pendingLock. withLock { pending } ) {
627
+ // TODO: this is more of an error
628
+ self ? . stateLock. withLock {
629
+ self ? . state = . outputReady( stdout: . success( [ ] ) , stderr: stderrResult)
630
+ }
560
631
}
561
632
}
562
- thread. start ( )
563
- self . stdout. thread = thread
564
633
565
634
// Only schedule a thread for stderr if no redirect was requested.
635
+ var stderrThread : Thread ? = nil
566
636
if !outputRedirection. redirectStderr {
567
637
// Close the local write end of the stderr pipe.
568
638
try close ( fd: stderrPipe [ 1 ] )
569
639
570
640
// Create a thread and start reading the stderr output on it.
571
- thread = Thread { [ weak self] in
641
+ stderrThread = Thread { [ weak self] in
572
642
if let readResult = self ? . readOutput ( onFD: stderrPipe [ 0 ] , outputClosure: outputClosures? . stderrClosure) {
573
- self ? . stderr. result = readResult
643
+ pendingLock. withLock {
644
+ if let stdoutResult = pending {
645
+ self ? . stateLock. withLock {
646
+ self ? . state = . outputReady( stdout: stdoutResult, stderr: readResult)
647
+ }
648
+ } else {
649
+ pending = readResult
650
+ }
651
+ }
652
+ } else if let stdoutResult = ( pendingLock. withLock { pending } ) {
653
+ // TODO: this is more of an error
654
+ self ? . stateLock. withLock {
655
+ self ? . state = . outputReady( stdout: stdoutResult, stderr: . success( [ ] ) )
656
+ }
574
657
}
575
658
}
576
- thread. start ( )
577
- self . stderr. thread = thread
659
+ } else {
660
+ pendingLock. withLock {
661
+ pending = . success( [ ] ) // no stderr in this case
662
+ }
663
+ }
664
+ // first set state then start reading threads
665
+ self . stateLock. withLock {
666
+ self . state = . readingOutputThread( stdout: stdoutThread, stderr: stderrThread)
578
667
}
668
+ stdoutThread. start ( )
669
+ stderrThread? . start ( )
579
670
}
671
+
580
672
return stdinStream
581
- #endif // POSIX implementation
673
+ #endif // POSIX implementation
582
674
}
583
675
584
676
/// Blocks the calling process until the subprocess finishes execution.
585
677
@discardableResult
586
678
public func waitUntilExit( ) throws -> ProcessResult {
587
- #if os(Windows)
588
- precondition ( _process != nil , " The process is not yet launched. " )
589
- let p = _process!
590
- p. waitUntilExit ( )
591
- stdout. thread? . join ( )
592
- stderr. thread? . join ( )
593
-
594
- let executionResult = ProcessResult (
595
- arguments: arguments,
596
- environment: environment,
597
- exitStatusCode: p. terminationStatus,
598
- output: stdout. result,
599
- stderrOutput: stderr. result
600
- )
601
- return executionResult
602
- #else
603
- return try serialQueue. sync {
604
- precondition ( launched, " The process is not yet launched. " )
605
-
606
- // If the process has already finsihed, return it.
607
- if let existingResult = _result {
608
- return existingResult
609
- }
610
-
679
+ self . stateLock. lock ( )
680
+ switch self . state {
681
+ case . idle:
682
+ defer { self . stateLock. unlock ( ) }
683
+ preconditionFailure ( " The process is not yet launched. " )
684
+ case . complete( let result) :
685
+ defer { self . stateLock. unlock ( ) }
686
+ return result
687
+ case . readingOutputThread( let stdoutThread, let stderrThread) :
688
+ self . stateLock. unlock ( ) // unlock early since output read thread need to change state
611
689
// If we're reading output, make sure that is finished.
612
- stdout. thread? . join ( )
613
- stderr. thread? . join ( )
614
-
690
+ stdoutThread. join ( )
691
+ stderrThread? . join ( )
692
+ return try self . waitUntilExit ( )
693
+ case . readingOutputPipe( let sync) :
694
+ self . stateLock. unlock ( ) // unlock early since output read thread need to change state
695
+ sync. wait ( )
696
+ return try self . waitUntilExit ( )
697
+ case . outputReady( let stdoutResult, let stderrResult) :
698
+ defer { self . stateLock. unlock ( ) }
615
699
// Wait until process finishes execution.
700
+ #if os(Windows)
701
+ precondition ( _process != nil , " The process is not yet launched. " )
702
+ let p = _process!
703
+ p. waitUntilExit ( )
704
+ let exitStatusCode = p. terminationStatus
705
+ #else
616
706
var exitStatusCode : Int32 = 0
617
707
var result = waitpid ( processID, & exitStatusCode, 0 )
618
708
while result == - 1 && errno == EINTR {
@@ -621,19 +711,19 @@ public final class Process: ObjectIdentifierProtocol {
621
711
if result == - 1 {
622
712
throw SystemError . waitpid ( errno)
623
713
}
714
+ #endif
624
715
625
716
// Construct the result.
626
717
let executionResult = ProcessResult (
627
718
arguments: arguments,
628
719
environment: environment,
629
720
exitStatusCode: exitStatusCode,
630
- output: stdout . result ,
631
- stderrOutput: stderr . result
721
+ output: stdoutResult ,
722
+ stderrOutput: stderrResult
632
723
)
633
- self . _result = executionResult
724
+ self . state = . complete ( executionResult)
634
725
return executionResult
635
726
}
636
- #endif
637
727
}
638
728
639
729
#if !os(Windows)
@@ -687,12 +777,12 @@ public final class Process: ObjectIdentifierProtocol {
687
777
public func signal( _ signal: Int32 ) {
688
778
#if os(Windows)
689
779
if signal == SIGINT {
690
- _process? . interrupt ( )
780
+ _process? . interrupt ( )
691
781
} else {
692
- _process? . terminate ( )
782
+ _process? . terminate ( )
693
783
}
694
784
#else
695
- assert ( launched, " The process is not yet launched. " )
785
+ assert ( self . launched, " The process is not yet launched. " )
696
786
_ = TSCLibc . kill ( startNewProcessGroup ? - processID : processID, signal)
697
787
#endif
698
788
}
0 commit comments