Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions Sources/LocaleApp/DNSProxyController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ actor DNSProxyController {
let manager = NEDNSProxyManager.shared()

do {
try await manager.loadFromPreferences()
try await manager.loadFromPreferencesWithTimeout()
let providerProtocol = NEDNSProxyProviderProtocol()
providerProtocol.providerBundleIdentifier = LocaleDNSConstants.extensionBundleIdentifier
providerProtocol.serverAddress = "Locale DNS Proxy"
Expand All @@ -59,7 +59,7 @@ actor DNSProxyController {
manager.localizedDescription = "Locale"
manager.providerProtocol = providerProtocol
manager.isEnabled = isEnabled
try await manager.saveToPreferences()
try await manager.saveToPreferencesWithTimeout()
} catch {
throw DNSProxyControllerError.preferencesFailed(error.localizedDescription)
}
Expand Down Expand Up @@ -157,27 +157,73 @@ private final class SystemExtensionRequestDelegate: NSObject, OSSystemExtensionR
}

private extension NEDNSProxyManager {
func loadFromPreferences() async throws {
func loadFromPreferencesWithTimeout() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let operation = PreferenceOperationContinuation(continuation: continuation, operationName: "load DNS proxy preferences")
operation.scheduleTimeout()
loadFromPreferences { error in
if let error {
continuation.resume(throwing: error)
operation.resume(throwing: error)
} else {
continuation.resume()
operation.resume()
}
}
}
}

func saveToPreferences() async throws {
func saveToPreferencesWithTimeout() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let operation = PreferenceOperationContinuation(continuation: continuation, operationName: "save DNS proxy preferences")
operation.scheduleTimeout()
saveToPreferences { error in
if let error {
continuation.resume(throwing: error)
operation.resume(throwing: error)
} else {
continuation.resume()
operation.resume()
}
}
}
}
}

private final class PreferenceOperationContinuation: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<Void, Error>?
private let operationName: String

init(continuation: CheckedContinuation<Void, Error>, operationName: String) {
self.continuation = continuation
self.operationName = operationName
}

func scheduleTimeout() {
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retain the timeout operation until it fires

When loadFromPreferences/saveToPreferences never invoke or retain the completion handler, the only reference left to PreferenceOperationContinuation is this weak capture, so it can be deallocated before the 10-second deadline and the timeout block returns without resuming the checked continuation. That leaves Apply/Revert suspended and preserves the continuation leak in exactly the entitlement/provisioning failure path this change is trying to handle; the timeout closure needs to keep the operation alive until it either fires or an earlier callback clears the continuation.

Useful? React with 👍 / 👎.

guard let self else { return }
self.resume(
throwing: DNSProxyControllerError.preferencesFailed(
"Timed out while trying to \(self.operationName). Check that Locale is signed with the DNS Proxy Network Extension entitlement."
)
)
}
}

func resume() {
finish { $0.resume() }
}

func resume(throwing error: Error) {
finish { $0.resume(throwing: error) }
}

private func finish(_ resume: (CheckedContinuation<Void, Error>) -> Void) {
lock.lock()
guard let continuation else {
lock.unlock()
return
}
self.continuation = nil
lock.unlock()

resume(continuation)
}
}
Loading