@@ -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( Thread , Thread ? )
185
+ case outputReady( Result < [ UInt8 ] , Swift . Error > , 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,46 @@ 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
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,10 @@ 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
+
271
+ // Lock to protect reading/writing on validatedExecutablesMap.
272
+ private static let validatedExecutablesMapLock = Lock ( )
261
273
262
274
/// Create a new process instance.
263
275
///
@@ -316,7 +328,7 @@ public final class Process: ObjectIdentifierProtocol {
316
328
///
317
329
/// The program can be executable name, relative path or absolute path.
318
330
public static func findExecutable( _ program: String ) -> AbsolutePath ? {
319
- return Process . executablesQueue . sync {
331
+ return Process . validatedExecutablesMapLock . withLock {
320
332
// Check if we already have a value for the program.
321
333
if let value = Process . validatedExecutablesMap [ program] {
322
334
return value
@@ -337,10 +349,11 @@ public final class Process: ObjectIdentifierProtocol {
337
349
/// Launch the subprocess.
338
350
public func launch( ) throws {
339
351
precondition ( arguments. count > 0 && !arguments[ 0 ] . isEmpty, " Need at least one argument to launch the process. " )
340
- precondition ( !launched, " It is not allowed to launch the same process object again. " )
341
352
342
- // Set the launch bool to true.
343
- launched = true
353
+ self . launchedLock. withLock {
354
+ precondition ( !self . _launched, " It is not allowed to launch the same process object again. " )
355
+ self . _launched = true
356
+ }
344
357
345
358
// Print the arguments if we are verbose.
346
359
if self . verbose {
@@ -354,7 +367,7 @@ public final class Process: ObjectIdentifierProtocol {
354
367
throw Process . Error. missingExecutableProgram ( program: executable)
355
368
}
356
369
357
- #if os(Windows)
370
+ #if os(Windows)
358
371
_process = Foundation . Process ( )
359
372
_process? . arguments = Array ( arguments. dropFirst ( ) ) // Avoid including the executable URL twice.
360
373
_process? . executableURL = executablePath. asURL
@@ -382,13 +395,13 @@ public final class Process: ObjectIdentifierProtocol {
382
395
}
383
396
384
397
try _process? . run ( )
385
- #else
398
+ #else
386
399
// Initialize the spawn attributes.
387
- #if canImport(Darwin) || os(Android)
400
+ #if canImport(Darwin) || os(Android)
388
401
var attributes : posix_spawnattr_t ? = nil
389
- #else
402
+ #else
390
403
var attributes = posix_spawnattr_t ( )
391
- #endif
404
+ #endif
392
405
posix_spawnattr_init ( & attributes)
393
406
defer { posix_spawnattr_destroy ( & attributes) }
394
407
@@ -398,13 +411,13 @@ public final class Process: ObjectIdentifierProtocol {
398
411
posix_spawnattr_setsigmask ( & attributes, & noSignals)
399
412
400
413
// Reset all signals to default behavior.
401
- #if os(macOS)
414
+ #if os(macOS)
402
415
var mostSignals = sigset_t ( )
403
416
sigfillset ( & mostSignals)
404
417
sigdelset ( & mostSignals, SIGKILL)
405
418
sigdelset ( & mostSignals, SIGSTOP)
406
419
posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
407
- #else
420
+ #else
408
421
// On Linux, this can only be used to reset signals that are legal to
409
422
// modify, so we have to take care about the set we use.
410
423
var mostSignals = sigset_t ( )
@@ -416,7 +429,7 @@ public final class Process: ObjectIdentifierProtocol {
416
429
sigaddset ( & mostSignals, i)
417
430
}
418
431
posix_spawnattr_setsigdefault ( & attributes, & mostSignals)
419
- #endif
432
+ #endif
420
433
421
434
// Set the attribute flags.
422
435
var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
@@ -429,31 +442,31 @@ public final class Process: ObjectIdentifierProtocol {
429
442
posix_spawnattr_setflags ( & attributes, Int16 ( flags) )
430
443
431
444
// Setup the file actions.
432
- #if canImport(Darwin) || os(Android)
445
+ #if canImport(Darwin) || os(Android)
433
446
var fileActions : posix_spawn_file_actions_t ? = nil
434
- #else
447
+ #else
435
448
var fileActions = posix_spawn_file_actions_t ( )
436
- #endif
449
+ #endif
437
450
posix_spawn_file_actions_init ( & fileActions)
438
451
defer { posix_spawn_file_actions_destroy ( & fileActions) }
439
452
440
453
if let workingDirectory = workingDirectory? . pathString {
441
- #if os(macOS)
454
+ #if os(macOS)
442
455
// The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
443
456
// to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
444
457
// exists here to make the compiler happy.
445
458
if #available( macOS 10 . 15 , * ) {
446
459
posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
447
460
}
448
- #elseif os(Linux)
461
+ #elseif os(Linux)
449
462
guard SPM_posix_spawn_file_actions_addchdir_np_supported ( ) else {
450
463
throw Process . Error. workingDirectoryNotSupported
451
464
}
452
465
453
466
SPM_posix_spawn_file_actions_addchdir_np ( & fileActions, workingDirectory)
454
- #else
467
+ #else
455
468
throw Process . Error. workingDirectoryNotSupported
456
- #endif
469
+ #endif
457
470
}
458
471
459
472
// Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
@@ -503,43 +516,84 @@ public final class Process: ObjectIdentifierProtocol {
503
516
throw SystemError . posix_spawn ( rv, arguments)
504
517
}
505
518
506
- if outputRedirection. redirectsOutput {
519
+ if !outputRedirection. redirectsOutput {
520
+ // no stdout or stderr in this case
521
+ self . stateLock. withLock {
522
+ self . state = . outputReady( . success( [ ] ) , . success( [ ] ) )
523
+ }
524
+ } else {
525
+ var outputResult : ( stdout: Result < [ UInt8 ] , Swift . Error > ? , stderr: Result < [ UInt8 ] , Swift . Error > ? )
526
+ let outputResultLock = Lock ( )
527
+
507
528
let outputClosures = outputRedirection. outputClosures
508
529
509
530
// Close the write end of the output pipe.
510
531
try close ( fd: & outputPipe[ 1 ] )
511
532
512
533
// Create a thread and start reading the output on it.
513
- var thread = Thread { [ weak self] in
534
+ let stdoutThread = Thread { [ weak self] in
514
535
if let readResult = self ? . readOutput ( onFD: outputPipe [ 0 ] , outputClosure: outputClosures? . stdoutClosure) {
515
- self ? . stdout. result = readResult
536
+ outputResultLock. withLock {
537
+ if let stderrResult = outputResult. stderr {
538
+ self ? . stateLock. withLock {
539
+ self ? . state = . outputReady( readResult, stderrResult)
540
+ }
541
+ } else {
542
+ outputResult. stdout = readResult
543
+ }
544
+ }
545
+ } else if let stderrResult = ( outputResultLock. withLock { outputResult. stderr } ) {
546
+ // TODO: this is more of an error
547
+ self ? . stateLock. withLock {
548
+ self ? . state = . outputReady( . success( [ ] ) , stderrResult)
549
+ }
516
550
}
517
551
}
518
- thread. start ( )
519
- self . stdout. thread = thread
520
552
521
553
// Only schedule a thread for stderr if no redirect was requested.
554
+ var stderrThread : Thread ? = nil
522
555
if !outputRedirection. redirectStderr {
523
556
// Close the write end of the stderr pipe.
524
557
try close ( fd: & stderrPipe[ 1 ] )
525
558
526
559
// Create a thread and start reading the stderr output on it.
527
- thread = Thread { [ weak self] in
560
+ stderrThread = Thread { [ weak self] in
528
561
if let readResult = self ? . readOutput ( onFD: stderrPipe [ 0 ] , outputClosure: outputClosures? . stderrClosure) {
529
- self ? . stderr. result = readResult
562
+ outputResultLock. withLock {
563
+ if let stdoutResult = outputResult. stdout {
564
+ self ? . stateLock. withLock {
565
+ self ? . state = . outputReady( stdoutResult, readResult)
566
+ }
567
+ } else {
568
+ outputResult. stderr = readResult
569
+ }
570
+ }
571
+ } else if let stdoutResult = ( outputResultLock. withLock { outputResult. stdout } ) {
572
+ // TODO: this is more of an error
573
+ self ? . stateLock. withLock {
574
+ self ? . state = . outputReady( stdoutResult, . success( [ ] ) )
575
+ }
530
576
}
531
577
}
532
- thread. start ( )
533
- self . stderr. thread = thread
578
+ } else {
579
+ outputResultLock. withLock {
580
+ outputResult. stderr = . success( [ ] ) // no stderr in this case
581
+ }
582
+ }
583
+ // first set state then start reading threads
584
+ self . stateLock. withLock {
585
+ self . state = . readingOutput( stdoutThread, stderrThread)
534
586
}
587
+ stdoutThread. start ( )
588
+ stderrThread? . start ( )
535
589
}
536
- #endif // POSIX implementation
590
+ #endif // POSIX implementation
537
591
}
538
592
539
593
/// Blocks the calling process until the subprocess finishes execution.
540
594
@discardableResult
541
595
public func waitUntilExit( ) throws -> ProcessResult {
542
- #if os(Windows)
596
+ #if os(Windows)
543
597
precondition ( _process != nil , " The process is not yet launched. " )
544
598
let p = _process!
545
599
p. waitUntilExit ( )
@@ -554,19 +608,22 @@ public final class Process: ObjectIdentifierProtocol {
554
608
stderrOutput: stderr. result
555
609
)
556
610
return executionResult
557
- #else
558
- return try serialQueue. sync {
559
- precondition ( launched, " The process is not yet launched. " )
560
-
561
- // If the process has already finsihed, return it.
562
- if let existingResult = _result {
563
- return existingResult
564
- }
565
-
611
+ #else
612
+ self . stateLock. lock ( )
613
+ switch self . state {
614
+ case . idle:
615
+ defer { self . stateLock. unlock ( ) }
616
+ preconditionFailure ( " The process is not yet launched. " )
617
+ case . complete( let result) :
618
+ return result
619
+ case . readingOutput( let stdoutThread, let stderrThread) :
620
+ self . stateLock. unlock ( ) // unlock early since output read thread need to change state
566
621
// If we're reading output, make sure that is finished.
567
- stdout. thread? . join ( )
568
- stderr. thread? . join ( )
569
-
622
+ stdoutThread. join ( )
623
+ stderrThread? . join ( )
624
+ return try self . waitUntilExit ( )
625
+ case . outputReady( let stdoutResult, let stderrResult) :
626
+ defer { self . stateLock. unlock ( ) }
570
627
// Wait until process finishes execution.
571
628
var exitStatusCode : Int32 = 0
572
629
var result = waitpid ( processID, & exitStatusCode, 0 )
@@ -582,13 +639,13 @@ public final class Process: ObjectIdentifierProtocol {
582
639
arguments: arguments,
583
640
environment: environment,
584
641
exitStatusCode: exitStatusCode,
585
- output: stdout . result ,
586
- stderrOutput: stderr . result
642
+ output: stdoutResult ,
643
+ stderrOutput: stderrResult
587
644
)
588
- self . _result = executionResult
645
+ self . state = . complete ( executionResult)
589
646
return executionResult
590
647
}
591
- #endif
648
+ #endif
592
649
}
593
650
594
651
#if !os(Windows)
@@ -640,16 +697,16 @@ public final class Process: ObjectIdentifierProtocol {
640
697
///
641
698
/// Note: This will signal all processes in the process group.
642
699
public func signal( _ signal: Int32 ) {
643
- #if os(Windows)
700
+ #if os(Windows)
644
701
if signal == SIGINT {
645
- _process? . interrupt ( )
702
+ _process? . interrupt ( )
646
703
} else {
647
- _process? . terminate ( )
704
+ _process? . terminate ( )
648
705
}
649
- #else
650
- assert ( launched, " The process is not yet launched. " )
706
+ #else
707
+ assert ( self . launched, " The process is not yet launched. " )
651
708
_ = TSCLibc . kill ( startNewProcessGroup ? - processID : processID, signal)
652
- #endif
709
+ #endif
653
710
}
654
711
}
655
712
0 commit comments