@@ -178,12 +178,20 @@ 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 readingOutput( stdout: Thread , stderr: Thread ? )
185
+ case outputReady( stdout: Result < [ UInt8 ] , Swift . Error > , stderr: Result < [ UInt8 ] , Swift . Error > )
186
+ case complete( ProcessResult )
187
+ }
188
+
181
189
/// Typealias for process id type.
182
- #if !os(Windows)
190
+ #if !os(Windows)
183
191
public typealias ProcessID = pid_t
184
- #else
192
+ #else
185
193
public typealias ProcessID = DWORD
186
- #endif
194
+ #endif
187
195
188
196
/// Typealias for stdout/stderr output closure.
189
197
public typealias OutputClosure = ( [ UInt8 ] ) -> Void
@@ -210,45 +218,47 @@ public final class Process: ObjectIdentifierProtocol {
210
218
public let workingDirectory : AbsolutePath ?
211
219
212
220
/// The process id of the spawned process, available after the process is launched.
213
- #if os(Windows)
221
+ #if os(Windows)
214
222
private var _process : Foundation . Process ?
215
223
public var processID : ProcessID {
216
224
return DWORD ( _process? . processIdentifier ?? 0 )
217
225
}
218
- #else
226
+ #else
219
227
public private( set) var processID = ProcessID ( )
220
- #endif
228
+ #endif
229
+
230
+ // process execution mutable state
231
+ private var state : State = . idle
221
232
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 ( )
226
235
227
236
/// 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 " )
228
239
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
+ }
231
247
}
232
248
}
233
249
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 ( )
236
253
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
+ }
248
259
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
252
262
253
263
/// Indicates if a new progress group is created for the child process.
254
264
private let startNewProcessGroup : Bool
@@ -257,7 +267,10 @@ public final class Process: ObjectIdentifierProtocol {
257
267
///
258
268
/// Key: Executable name or path.
259
269
/// 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 ( )
261
274
262
275
/// Create a new process instance.
263
276
///
@@ -348,7 +361,7 @@ public final class Process: ObjectIdentifierProtocol {
348
361
}
349
362
// This should cover the most common cases, i.e. when the cache is most helpful.
350
363
if workingDirectory == localFileSystem. currentWorkingDirectory {
351
- return Process . executablesQueue . sync {
364
+ return Process . validatedExecutablesMapLock . withLock {
352
365
if let value = Process . validatedExecutablesMap [ program] {
353
366
return value
354
367
}
@@ -364,10 +377,11 @@ public final class Process: ObjectIdentifierProtocol {
364
377
/// Launch the subprocess.
365
378
public func launch( ) throws {
366
379
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. " )
368
380
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
+ }
371
385
372
386
// Print the arguments if we are verbose.
373
387
if self . verbose {
@@ -381,7 +395,7 @@ public final class Process: ObjectIdentifierProtocol {
381
395
throw Process . Error. missingExecutableProgram ( program: executable)
382
396
}
383
397
384
- #if os(Windows)
398
+ #if os(Windows)
385
399
_process = Foundation . Process ( )
386
400
_process? . arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
387
401
_process? . executableURL = executablePath. asURL
@@ -409,13 +423,13 @@ public final class Process: ObjectIdentifierProtocol {
409
423
}
410
424
411
425
try _process? . run ( )
412
- #else
426
+ #else
413
427
// Initialize the spawn attributes.
414
- #if canImport(Darwin) || os(Android)
428
+ #if canImport(Darwin) || os(Android)
415
429
var attributes : posix_spawnattr_t ? = nil
416
- #else
430
+ #else
417
431
var attributes = posix_spawnattr_t ( )
418
- #endif
432
+ #endif
419
433
posix_spawnattr_init ( & attributes)
420
434
defer { posix_spawnattr_destroy ( & attributes) }
421
435
@@ -425,13 +439,13 @@ public final class Process: ObjectIdentifierProtocol {
425
439
posix_spawnattr_setsigmask ( & attributes, & noSignals)
426
440
427
441
// Reset all signals to default behavior.
428
- #if os(macOS)
442
+ #if os(macOS)
429
443
var mostSignals = sigset_t ( )
430
444
sigfillset ( & mostSignals)
431
445
sigdelset ( & mostSignals, SIGKILL)
432
446
sigdelset ( & mostSignals, SIGSTOP)
433
447
posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
434
- #else
448
+ #else
435
449
// On Linux, this can only be used to reset signals that are legal to
436
450
// modify, so we have to take care about the set we use.
437
451
var mostSignals = sigset_t ( )
@@ -443,7 +457,7 @@ public final class Process: ObjectIdentifierProtocol {
443
457
sigaddset ( & mostSignals, i)
444
458
}
445
459
posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
446
- #endif
460
+ #endif
447
461
448
462
// Set the attribute flags.
449
463
var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
@@ -456,31 +470,31 @@ public final class Process: ObjectIdentifierProtocol {
456
470
posix_spawnattr_setflags ( & attributes, Int16 ( flags) )
457
471
458
472
// Setup the file actions.
459
- #if canImport(Darwin) || os(Android)
473
+ #if canImport(Darwin) || os(Android)
460
474
var fileActions : posix_spawn_file_actions_t ? = nil
461
- #else
475
+ #else
462
476
var fileActions = posix_spawn_file_actions_t ( )
463
- #endif
477
+ #endif
464
478
posix_spawn_file_actions_init ( & fileActions)
465
479
defer { posix_spawn_file_actions_destroy ( & fileActions) }
466
480
467
481
if let workingDirectory = workingDirectory? . pathString {
468
- #if os(macOS)
482
+ #if os(macOS)
469
483
// The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
470
484
// to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
471
485
// exists here to make the compiler happy.
472
486
if #available( macOS 10 . 15 , * ) {
473
487
posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
474
488
}
475
- #elseif os(Linux)
489
+ #elseif os(Linux)
476
490
guard SPM_posix_spawn_file_actions_addchdir_np_supported ( ) else {
477
491
throw Process . Error. workingDirectoryNotSupported
478
492
}
479
493
480
494
SPM_posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
481
- #else
495
+ #else
482
496
throw Process . Error. workingDirectoryNotSupported
483
- #endif
497
+ #endif
484
498
}
485
499
486
500
// Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
@@ -534,43 +548,84 @@ public final class Process: ObjectIdentifierProtocol {
534
548
throw SystemError . posix_spawn ( rv, arguments)
535
549
}
536
550
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
+
538
560
let outputClosures = outputRedirection. outputClosures
539
561
540
562
// Close the write end of the output pipe.
541
563
try close ( fd: & outputPipe[ 1 ] )
542
564
543
565
// Create a thread and start reading the output on it.
544
- var thread = Thread { [ weak self] in
566
+ let stdoutThread = Thread { [ weak self] in
545
567
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
+ }
547
582
}
548
583
}
549
- thread. start ( )
550
- self . stdout. thread = thread
551
584
552
585
// Only schedule a thread for stderr if no redirect was requested.
586
+ var stderrThread : Thread ? = nil
553
587
if !outputRedirection. redirectStderr {
554
588
// Close the write end of the stderr pipe.
555
589
try close ( fd: & stderrPipe[ 1 ] )
556
590
557
591
// Create a thread and start reading the stderr output on it.
558
- thread = Thread { [ weak self] in
592
+ stderrThread = Thread { [ weak self] in
559
593
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
+ }
561
608
}
562
609
}
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)
565
618
}
619
+ stdoutThread. start ( )
620
+ stderrThread? . start ( )
566
621
}
567
- #endif // POSIX implementation
622
+ #endif // POSIX implementation
568
623
}
569
624
570
625
/// Blocks the calling process until the subprocess finishes execution.
571
626
@discardableResult
572
627
public func waitUntilExit( ) throws -> ProcessResult {
573
- #if os(Windows)
628
+ #if os(Windows)
574
629
precondition ( _process != nil , " The process is not yet launched. " )
575
630
let p = _process!
576
631
p. waitUntilExit ( )
@@ -585,19 +640,23 @@ public final class Process: ObjectIdentifierProtocol {
585
640
stderrOutput: stderr. result
586
641
)
587
642
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
597
654
// 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 ( ) }
601
660
// Wait until process finishes execution.
602
661
var exitStatusCode : Int32 = 0
603
662
var result = waitpid ( processID, & exitStatusCode, 0 )
@@ -613,13 +672,13 @@ public final class Process: ObjectIdentifierProtocol {
613
672
arguments: arguments,
614
673
environment: environment,
615
674
exitStatusCode: exitStatusCode,
616
- output: stdout . result ,
617
- stderrOutput: stderr . result
675
+ output: stdoutResult ,
676
+ stderrOutput: stderrResult
618
677
)
619
- self . _result = executionResult
678
+ self . state = . complete ( executionResult)
620
679
return executionResult
621
680
}
622
- #endif
681
+ #endif
623
682
}
624
683
625
684
#if !os(Windows)
@@ -671,16 +730,16 @@ public final class Process: ObjectIdentifierProtocol {
671
730
///
672
731
/// Note: This will signal all processes in the process group.
673
732
public func signal( _ signal: Int32 ) {
674
- #if os(Windows)
733
+ #if os(Windows)
675
734
if signal == SIGINT {
676
- _process? . interrupt ( )
735
+ _process? . interrupt ( )
677
736
} else {
678
- _process? . terminate ( )
737
+ _process? . terminate ( )
679
738
}
680
- #else
681
- assert ( launched, " The process is not yet launched. " )
739
+ #else
740
+ assert ( self . launched, " The process is not yet launched. " )
682
741
_ = TSCLibc . kill ( startNewProcessGroup ? - processID : processID, signal)
683
- #endif
742
+ #endif
684
743
}
685
744
}
686
745
0 commit comments