Merge 801dbb508a6d2c5c473c894938c7a4c8067988ac into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Mathias Caldas 2026-03-21 01:58:40 +00:00 committed by GitHub
commit 144f4c8580
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,6 +2,29 @@ import OpenClawProtocol
import Foundation
import OSLog
/// Thread-safe one-shot wrapper around CheckedContinuation to prevent double-resume crashes.
private final class OneShotContinuation: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<Void, Error>?
init(_ continuation: CheckedContinuation<Void, Error>) {
self.continuation = continuation
}
func resume(error: Error?) {
lock.lock()
defer { lock.unlock() }
let cont = self.continuation
self.continuation = nil
guard let cont else {
// URLSessionWebSocketTask fired pong handler more than once (iOS 18+ race).
// The continuation was already resumed; silently drop the duplicate.
return
}
ThrowingContinuationSupport.resumeVoid(cont, error: error)
}
}
public protocol WebSocketTasking: AnyObject {
var state: URLSessionTask.State { get }
func resume()
@ -44,8 +67,12 @@ public struct WebSocketTaskBox: @unchecked Sendable {
public func sendPing() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
// URLSessionWebSocketTask.sendPing can invoke the handler more than once when
// the task is cancelled mid-flight (observed on iOS 18+). Guard with a one-shot
// wrapper to avoid a CheckedContinuation double-resume crash.
let once = OneShotContinuation(continuation)
self.task.sendPing { error in
ThrowingContinuationSupport.resumeVoid(continuation, error: error)
once.resume(error: error)
}
}
}