Merge pull request #344 from ebrindley/fix/double-resume-checked-continuation
fix: prevent double resume of checked continuations causing EXC_BREAKPOINT crashes
This commit is contained in:
commit
d73f5bb926
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import os
|
import os
|
||||||
import Zip
|
import Zip
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Atomics
|
||||||
|
|
||||||
|
|
||||||
struct WhisperModel: Identifiable {
|
struct WhisperModel: Identifiable {
|
||||||
@ -34,13 +35,17 @@ struct WhisperModel: Identifiable {
|
|||||||
|
|
||||||
private class TaskDelegate: NSObject, URLSessionTaskDelegate {
|
private class TaskDelegate: NSObject, URLSessionTaskDelegate {
|
||||||
private let continuation: CheckedContinuation<Void, Never>
|
private let continuation: CheckedContinuation<Void, Never>
|
||||||
|
private let finished = ManagedAtomic(false)
|
||||||
|
|
||||||
init(_ continuation: CheckedContinuation<Void, Never>) {
|
init(_ continuation: CheckedContinuation<Void, Never>) {
|
||||||
self.continuation = continuation
|
self.continuation = continuation
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||||
continuation.resume()
|
// Ensure continuation is resumed only once, even if called multiple times
|
||||||
|
if finished.exchange(true, ordering: .acquiring) == false {
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,16 +105,25 @@ extension WhisperState {
|
|||||||
let destinationURL = modelsDirectory.appendingPathComponent(UUID().uuidString)
|
let destinationURL = modelsDirectory.appendingPathComponent(UUID().uuidString)
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
|
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
|
||||||
|
// Guard to prevent double resume
|
||||||
|
let finished = ManagedAtomic(false)
|
||||||
|
|
||||||
|
func finishOnce(_ result: Result<Data, Error>) {
|
||||||
|
if finished.exchange(true, ordering: .acquiring) == false {
|
||||||
|
continuation.resume(with: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let task = URLSession.shared.downloadTask(with: url) { tempURL, response, error in
|
let task = URLSession.shared.downloadTask(with: url) { tempURL, response, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
continuation.resume(throwing: error)
|
finishOnce(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse,
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
(200...299).contains(httpResponse.statusCode),
|
(200...299).contains(httpResponse.statusCode),
|
||||||
let tempURL = tempURL else {
|
let tempURL = tempURL else {
|
||||||
continuation.resume(throwing: URLError(.badServerResponse))
|
finishOnce(.failure(URLError(.badServerResponse)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,12 +133,12 @@ extension WhisperState {
|
|||||||
|
|
||||||
// Read the file in chunks to avoid memory pressure
|
// Read the file in chunks to avoid memory pressure
|
||||||
let data = try Data(contentsOf: destinationURL, options: .mappedIfSafe)
|
let data = try Data(contentsOf: destinationURL, options: .mappedIfSafe)
|
||||||
continuation.resume(returning: data)
|
finishOnce(.success(data))
|
||||||
|
|
||||||
// Clean up the temporary file
|
// Clean up the temporary file
|
||||||
try? FileManager.default.removeItem(at: destinationURL)
|
try? FileManager.default.removeItem(at: destinationURL)
|
||||||
} catch {
|
} catch {
|
||||||
continuation.resume(throwing: error)
|
finishOnce(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +165,10 @@ extension WhisperState {
|
|||||||
Task {
|
Task {
|
||||||
await withTaskCancellationHandler {
|
await withTaskCancellationHandler {
|
||||||
observation.invalidate()
|
observation.invalidate()
|
||||||
|
// Also ensure continuation is resumed with cancellation if task is cancelled
|
||||||
|
if finished.exchange(true, ordering: .acquiring) == false {
|
||||||
|
continuation.resume(throwing: CancellationError())
|
||||||
|
}
|
||||||
} operation: {
|
} operation: {
|
||||||
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
|
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
|
||||||
}
|
}
|
||||||
@ -211,13 +229,21 @@ extension WhisperState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func unzipCoreMLFile(_ zipPath: URL, to destination: URL) async throws {
|
private func unzipCoreMLFile(_ zipPath: URL, to destination: URL) async throws {
|
||||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
let finished = ManagedAtomic(false)
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||||
|
func finishOnce(_ result: Result<Void, Error>) {
|
||||||
|
if finished.exchange(true, ordering: .acquiring) == false {
|
||||||
|
continuation.resume(with: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
|
||||||
try Zip.unzipFile(zipPath, destination: destination, overwrite: true, password: nil)
|
try Zip.unzipFile(zipPath, destination: destination, overwrite: true, password: nil)
|
||||||
continuation.resume()
|
finishOnce(.success(()))
|
||||||
} catch {
|
} catch {
|
||||||
continuation.resume(throwing: error)
|
finishOnce(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user