Skip to content

Commit e6bd7ef

Browse files
authored
Revert "improve process state management (#203)"
This reverts commit e626b7e.
1 parent f2de70b commit e6bd7ef

File tree

2 files changed

+82
-141
lines changed

2 files changed

+82
-141
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 81 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)