fix: harden process termination and retain print task #67
@@ -179,6 +179,14 @@ public actor ProcessManager {
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
// Fallback watchdog: very fast child exits can race past the
|
||||
// terminationHandler delivery on a loaded host. waitUntilExit()
|
||||
// blocks the detached thread and guarantees didTerminate runs.
|
||||
Task.detached { [weak self, process] in
|
||||
process.waitUntilExit()
|
||||
guard let self else { return }
|
||||
await self.didTerminate(id: id, code: process.terminationStatus)
|
||||
}
|
||||
} catch {
|
||||
preKillHooks.removeValue(forKey: id)
|
||||
children.removeValue(forKey: id)
|
||||
@@ -273,6 +281,14 @@ public actor ProcessManager {
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
// Fallback watchdog: very fast child exits can race past the
|
||||
// terminationHandler delivery on a loaded host. waitUntilExit()
|
||||
// blocks the detached thread and resumes the box if the handler
|
||||
// did not already do so (#50, #52).
|
||||
Task.detached { [capturedProcess] in
|
||||
capturedProcess.waitUntilExit()
|
||||
_ = box.resume(with: capturedProcess.terminationStatus)
|
||||
}
|
||||
} catch {
|
||||
_ = box.resume(with: -1)
|
||||
captured.removeValue(forKey: id)
|
||||
|
||||
@@ -109,6 +109,9 @@ final class TargetWorkflowViewModel {
|
||||
var printNotice: String?
|
||||
var printNoticeIsError = false
|
||||
var isPrinting = false
|
||||
/// Strong reference to the active print task so the unstructured
|
||||
/// `Task` is not dropped before it resumes.
|
||||
private var printTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
@@ -480,7 +483,9 @@ final class TargetWorkflowViewModel {
|
||||
func printAllPages() {
|
||||
guard let result = printtargResult, !isPrinting else { return }
|
||||
isPrinting = true
|
||||
Task { @MainActor in
|
||||
let task = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
defer { self.printTask = nil }
|
||||
var printed = 0
|
||||
for page in result.pages {
|
||||
do {
|
||||
@@ -498,13 +503,16 @@ final class TargetWorkflowViewModel {
|
||||
printNoticeIsError = false
|
||||
isPrinting = false
|
||||
}
|
||||
printTask = task
|
||||
}
|
||||
|
||||
/// `#btnPrintPage-N` — one TIFF.
|
||||
func printPage(_ page: GalleryPage) {
|
||||
guard !isPrinting else { return }
|
||||
isPrinting = true
|
||||
Task { @MainActor in
|
||||
let task = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
defer { self.printTask = nil }
|
||||
do {
|
||||
try await spool(page, index: page.index)
|
||||
printNotice = "Sent \(page.page.filename) to \(selectedPrinter)."
|
||||
@@ -515,6 +523,7 @@ final class TargetWorkflowViewModel {
|
||||
}
|
||||
isPrinting = false
|
||||
}
|
||||
printTask = task
|
||||
}
|
||||
|
||||
private func spool(_ page: GalleryPage, index: Int) async throws {
|
||||
|
||||
@@ -170,6 +170,24 @@ struct ProcessManagerTests {
|
||||
#expect(result.exitCode == 3)
|
||||
}
|
||||
|
||||
@Test func capturedRunFastExit() async throws {
|
||||
let pm = ProcessManager()
|
||||
let bin = try script("fast.sh", "#!/bin/sh\nexit 7\n")
|
||||
let result = try await pm.runCaptured(id: "fast", binary: bin, arguments: [])
|
||||
#expect(result.exitCode == 7)
|
||||
#expect(result.stdout == "")
|
||||
#expect(result.stderr == "")
|
||||
}
|
||||
|
||||
@Test func capturedRunStderrOnly() async throws {
|
||||
let pm = ProcessManager()
|
||||
let bin = try script("stderr-only.sh", "#!/bin/sh\necho 'mock lp failure' 1>&2\nexit 1\n")
|
||||
let result = try await pm.runCaptured(id: "stderr-only", binary: bin, arguments: [])
|
||||
#expect(result.exitCode == 1)
|
||||
#expect(result.stdout == "")
|
||||
#expect(result.stderr.contains("mock lp failure"))
|
||||
}
|
||||
|
||||
@Test func capturedRunDoesNotDeadlockOnLargeOutput() async throws {
|
||||
let pm = ProcessManager()
|
||||
// 5000 lines each stream exceeds the 64 KiB pipe buffer.
|
||||
|
||||
Reference in New Issue
Block a user