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