mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(macos): coalesce location permission requests (#116183)
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- **Linux gateway service ownership:** refuse user-scope systemd publication and activation when the same gateway unit name is already owned or cannot be verified in the system scope, including `--force`, with actionable recovery guidance instead of creating restart-looping dual managers. Fixes #116129.
|
||||
- **macOS remote tunnel lifecycle:** prevent cancelled or superseded restart backoffs from recreating SSH tunnels, and join a tunnel create that another caller started while the actor was suspended.
|
||||
- **macOS location permission requests:** coalesce concurrent prompts so every caller resumes, and stop cancelled timeouts from opening Settings or completing a newer request.
|
||||
- **Control UI update reconciliation:** preserve an unresolved managed-update request across disconnects, accept the replacement Gateway version when it proves success, and otherwise show explicit recovery guidance instead of trusting an unrelated cached update result or failing silently. Fixes #116075. Thanks @shakkernerd.
|
||||
- **Control UI model readiness:** put AI setup first when no model is selectable, distinguish signed-in credentials from ready providers, and route accounts with no exposed models directly to provider recovery instead of leading with disabled default controls.
|
||||
- **Control UI Talk session isolation:** stop active realtime Talk media and retire its callbacks before chat session changes, Gateway disconnects, or pane disposal so previous-session audio, transcript, camera, and status updates cannot leak into the next view. Thanks @shakkernerd.
|
||||
|
||||
@@ -295,12 +295,42 @@ enum LocationPermissionHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LocationPermissionRequestCoordinator {
|
||||
private var continuations: [CheckedContinuation<CLAuthorizationStatus, Never>] = []
|
||||
|
||||
var hasPendingRequests: Bool {
|
||||
!self.continuations.isEmpty
|
||||
}
|
||||
|
||||
var pendingRequestCount: Int {
|
||||
self.continuations.count
|
||||
}
|
||||
|
||||
func wait(onEnqueue: (_ isFirstRequest: Bool) -> Void) async -> CLAuthorizationStatus {
|
||||
await withCheckedContinuation { continuation in
|
||||
let isFirstRequest = self.continuations.isEmpty
|
||||
self.continuations.append(continuation)
|
||||
onEnqueue(isFirstRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func finish(status: CLAuthorizationStatus) {
|
||||
let continuations = self.continuations
|
||||
self.continuations.removeAll()
|
||||
for continuation in continuations {
|
||||
continuation.resume(returning: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
|
||||
static let shared = LocationPermissionRequester()
|
||||
private let manager = CLLocationManager()
|
||||
private var continuation: CheckedContinuation<CLAuthorizationStatus, Never>?
|
||||
private let requests = LocationPermissionRequestCoordinator()
|
||||
private var timeoutTask: Task<Void, Never>?
|
||||
private var requestedAlways = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -318,35 +348,47 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
|
||||
return current
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { cont in
|
||||
self.continuation = cont
|
||||
self.timeoutTask?.cancel()
|
||||
self.timeoutTask = Task { [weak self] in
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return }
|
||||
guard self.continuation != nil else { return }
|
||||
LocationPermissionHelper.openSettings()
|
||||
self.finish(status: self.manager.authorizationStatus)
|
||||
}
|
||||
}
|
||||
if always {
|
||||
return await self.requests.wait { isFirstRequest in
|
||||
if isFirstRequest {
|
||||
self.requestedAlways = always
|
||||
self.scheduleTimeout()
|
||||
self.requestAuthorization(always: always)
|
||||
// On macOS, requesting an actual fix makes the prompt more reliable.
|
||||
self.manager.requestLocation()
|
||||
} else if always, !self.requestedAlways {
|
||||
self.requestedAlways = true
|
||||
self.manager.requestAlwaysAuthorization()
|
||||
} else {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On macOS, requesting an actual fix makes the prompt more reliable.
|
||||
self.manager.requestLocation()
|
||||
private func requestAuthorization(always: Bool) {
|
||||
if always {
|
||||
self.manager.requestAlwaysAuthorization()
|
||||
} else {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleTimeout() {
|
||||
self.timeoutTask?.cancel()
|
||||
self.timeoutTask = Task { [weak self] in
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 3_000_000_000)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
guard !Task.isCancelled, let self, self.requests.hasPendingRequests else { return }
|
||||
LocationPermissionHelper.openSettings()
|
||||
self.finish(status: self.manager.authorizationStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(status: CLAuthorizationStatus) {
|
||||
self.timeoutTask?.cancel()
|
||||
self.timeoutTask = nil
|
||||
guard let cont = self.continuation else { return }
|
||||
self.continuation = nil
|
||||
cont.resume(returning: status)
|
||||
self.requestedAlways = false
|
||||
self.requests.finish(status: status)
|
||||
}
|
||||
|
||||
/// nonisolated for Swift 6 strict concurrency compatibility
|
||||
|
||||
@@ -2,7 +2,33 @@ import CoreLocation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct PermissionManagerLocationTests {
|
||||
@Test
|
||||
func `concurrent request waiters all resume`() async {
|
||||
let coordinator = LocationPermissionRequestCoordinator()
|
||||
let first = Task { @MainActor in
|
||||
await coordinator.wait { _ in }
|
||||
}
|
||||
while coordinator.pendingRequestCount < 1 {
|
||||
await Task.yield()
|
||||
}
|
||||
|
||||
let second = Task { @MainActor in
|
||||
await coordinator.wait { _ in }
|
||||
}
|
||||
while coordinator.pendingRequestCount < 2 {
|
||||
await Task.yield()
|
||||
}
|
||||
|
||||
coordinator.finish(status: .denied)
|
||||
|
||||
#expect(await first.value == .denied)
|
||||
#expect(await second.value == .denied)
|
||||
#expect(coordinator.pendingRequestCount == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `authorizedAlways counts for both modes`() {
|
||||
#expect(PermissionManager.isLocationAuthorized(status: .authorizedAlways, requireAlways: false))
|
||||
|
||||
Reference in New Issue
Block a user