Skip to content

Commit 2e817c9

Browse files
Fix orphaned whisper-server processes accumulating across app sessions
Kill stale whisper-server processes before spawning a new one and terminate the managed server on app exit via applicationShouldTerminate with terminateLater to ensure async shutdown completes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a3adeae commit 2e817c9

4 files changed

Lines changed: 40 additions & 3 deletions

File tree

macos/Sources/Quedo/AppController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ actor AppControllerActor {
228228
func shutdown() async {
229229
await audioEngine.cancelRecording()
230230
hotkeyManager.deactivate()
231+
await transcriptionPipeline.shutdown()
231232
try? await lifecycle.transition(to: .shuttingDown)
232233
await pushUI()
233234
}

macos/Sources/Quedo/AppDelegate.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
3131
}
3232
}
3333

34-
func applicationWillTerminate(_ notification: Notification) {
35-
_ = notification
34+
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
3635
if let didBecomeActiveObserver {
3736
NotificationCenter.default.removeObserver(didBecomeActiveObserver)
3837
self.didBecomeActiveObserver = nil
3938
}
4039
permissionRecoveryTask?.cancel()
4140
permissionRecoveryTask = nil
41+
42+
guard let appController else {
43+
return .terminateNow
44+
}
45+
4246
Task {
43-
await appController?.shutdown()
47+
await appController.shutdown()
48+
sender.reply(toApplicationShouldTerminate: true)
4449
}
50+
return .terminateLater
4551
}
4652

4753
private func bootstrap(menuBar: MenuBarController) async {

macos/Sources/QuedoCore/Providers/WhisperCppProvider.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public struct WhisperCppProvider: TranscriptionProvider {
4848
return try await transcribe(request: request, modelURL: modelURL, runtime: runtime)
4949
}
5050

51+
/// Stops any managed whisper-server process.
52+
public func shutdownServer() async {
53+
await serverManager.shutdown()
54+
}
55+
5156
/// Checks if configured whisper.cpp runtime is reachable.
5257
public func checkHealth(timeoutSeconds: Int) async -> Bool {
5358
let timeout = max(1, timeoutSeconds)
@@ -433,6 +438,7 @@ private actor WhisperCppServerManager {
433438
}
434439

435440
stopServer()
441+
await killOrphanedServers()
436442

437443
for port in candidatePorts() {
438444
let baseURL = URL(string: "http://127.0.0.1:\(port)")!
@@ -468,6 +474,21 @@ private actor WhisperCppServerManager {
468474
throw ProviderError.networkFailure
469475
}
470476

477+
func shutdown() {
478+
stopServer()
479+
}
480+
481+
private func killOrphanedServers() async {
482+
let killProcess = Process()
483+
killProcess.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
484+
killProcess.arguments = ["-x", "whisper-server"]
485+
killProcess.standardOutput = FileHandle.nullDevice
486+
killProcess.standardError = FileHandle.nullDevice
487+
try? killProcess.run()
488+
// Give processes time to exit and release ports.
489+
try? await Task.sleep(for: .milliseconds(200))
490+
}
491+
471492
private func stopServer() {
472493
if let process, process.isRunning {
473494
process.terminate()

macos/Sources/QuedoCore/TranscriptionPipeline.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ public actor TranscriptionPipeline {
159159
}
160160
}
161161

162+
/// Shuts down any provider-managed background processes.
163+
public func shutdown() async {
164+
for provider in providers.values {
165+
if let whisper = provider as? WhisperCppProvider {
166+
await whisper.shutdownServer()
167+
}
168+
}
169+
}
170+
162171
/// Performs a provider connectivity probe with 6-second timeout budget.
163172
public func connectivityCheck(primary: ProviderKind, fallback: ProviderKind) async -> (primaryOK: Bool, fallbackOK: Bool) {
164173
guard let primaryProvider = providers[primary], let fallbackProvider = providers[fallback] else {

0 commit comments

Comments
 (0)