1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-01 17:20:09 +02:00

🐛 Fix race condition on RealShell.attach()

This commit is contained in:
2025-12-23 12:07:28 +01:00
parent 8097502a76
commit c28e3e562c

View File

@@ -227,19 +227,29 @@ class RealShell: ShellProtocol {
process.standardError = errorPipe process.standardError = errorPipe
let output = ShellOutput.empty() let output = ShellOutput.empty()
// Only access `resumed`, `output` from serialQueue to ensure thread safety
let serialQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_output") let serialQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_output")
return try await withCheckedThrowingContinuation({ continuation in return try await withCheckedThrowingContinuation({ continuation in
let timeoutTask = Task { // Guard against resuming the continuation twice (race between timeout and termination)
try? await Task.sleep(nanoseconds: timeout.nanoseconds) var resumed = false
// Only terminate if the process is still running
// We are using GCD here because we're already using a serial queue anyway
let timeoutTaskTermination = DispatchWorkItem {
if process.isRunning { if process.isRunning {
process.terminationHandler = nil process.terminationHandler = nil
process.terminate() process.terminate()
continuation.resume(throwing: ShellError.timedOut) if !resumed {
resumed = true
continuation.resume(throwing: ShellError.timedOut)
}
} }
} }
// Let's make sure that once our timeout occurs, our process is terminated
serialQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTaskTermination)
// Set up background reading for stdout // Set up background reading for stdout
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
let data = fileHandle.availableData let data = fileHandle.availableData
@@ -263,7 +273,7 @@ class RealShell: ShellProtocol {
} }
process.terminationHandler = { process in process.terminationHandler = { process in
timeoutTask.cancel() timeoutTaskTermination.cancel()
// Clean up readability handlers // Clean up readability handlers
outputPipe.fileHandleForReading.readabilityHandler = nil outputPipe.fileHandleForReading.readabilityHandler = nil
@@ -284,7 +294,10 @@ class RealShell: ShellProtocol {
didReceiveOutput(string, .stdErr) didReceiveOutput(string, .stdErr)
} }
continuation.resume(returning: (process, output)) if !resumed {
resumed = true
continuation.resume(returning: (process, output))
}
} }
} }