A macOS CLI tool that exports original photos and videos from the macOS Photos library using PhotoKit.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix process hang: cancel timeout timer when osascript exits

The DispatchQueue.global().asyncAfter timeout timer was never cancelled
on normal process exit, keeping the Swift runtime alive for up to 10
minutes per AppleScript export. Store the DispatchWorkItem on
ProcessHandle and cancel+nil it in terminationHandler.

+10 -2
+10 -2
Sources/LadderKit/AppleScriptExporter.swift
··· 110 110 private final class ProcessHandle: @unchecked Sendable { 111 111 let process = Process() 112 112 let stderrPipe = Pipe() 113 + var timeoutWork: DispatchWorkItem? 113 114 114 115 func configure(script: String) { 115 116 process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") ··· 135 136 handle.configure(script: script) 136 137 137 138 return try await withCheckedThrowingContinuation { continuation in 138 - // Timeout: terminate process after deadline 139 - DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { 139 + // Timeout: terminate process after deadline (cancellable to allow clean exit). 140 + // Stored on handle (@unchecked Sendable) so it can be cancelled from 141 + // terminationHandler without capturing a non-Sendable local. 142 + handle.timeoutWork = DispatchWorkItem { 140 143 if handle.process.isRunning { 141 144 handle.process.terminate() 142 145 } 143 146 } 147 + DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: handle.timeoutWork!) 144 148 145 149 handle.process.terminationHandler = { proc in 150 + handle.timeoutWork?.cancel() 151 + handle.timeoutWork = nil 146 152 let stderr = handle.readStderr() 147 153 // Detect our timeout by checking for uncaught signal (SIGTERM = 15) 148 154 let timedOut = proc.terminationReason == .uncaughtSignal ··· 156 162 do { 157 163 try handle.process.run() 158 164 } catch { 165 + handle.timeoutWork?.cancel() 166 + handle.timeoutWork = nil 159 167 continuation.resume(throwing: error) 160 168 } 161 169 }