fix: detect screen recording permission revocation during active monitoring#5820
fix: detect screen recording permission revocation during active monitoring#5820
Conversation
…toring - Add periodic permission recheck every 60 seconds during capture - Stop monitoring gracefully when permission is revoked - Send permissionLost event for UI notification - Prevents silent data loss when user revokes permission via System Settings - Fixes #5792
Greptile SummaryThis PR addresses issue #5792 by adding a 60-second periodic recheck of screen recording permission inside the active capture loop in The intent is correct and the scope is minimal, but there is a critical performance bug introduced by the implementation:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Timer as captureTimer (every ~1s)
participant CF as captureFrame() [@MainActor]
participant SCS as ScreenCaptureService
participant Proc as /usr/sbin/screencapture subprocess
participant SM as stopMonitoring()
participant UI as UI Event Handler
Timer->>CF: fires (every ~1s)
CF->>CF: now.timeIntervalSince(lastPermissionCheckTime) >= 60?
alt Every 60th second
CF->>SCS: checkPermission() [BLOCKING, main thread]
SCS->>Proc: process.run() + waitUntilExit()
Note over CF,Proc: Main thread BLOCKED until subprocess exits
Proc-->>SCS: exit code + file check
SCS-->>CF: Bool (granted or not)
alt Permission revoked
CF->>UI: sendEvent("permissionLost")
CF->>SM: stopMonitoring()
SM->>UI: sendEvent("monitoringStopped")
Note over UI: Two events arrive in quick succession
SM-->>CF: returns
CF-->>CF: return (exit frame)
end
else Normal path (no recheck needed)
CF->>CF: continue normal capture logic
end
Last reviewed commit: "fix: detect screen r..." |
| let now = Date() | ||
| if now.timeIntervalSince(lastPermissionCheckTime) >= permissionCheckInterval { | ||
| lastPermissionCheckTime = now | ||
| let permissionGranted = ScreenCaptureService.checkPermission() |
There was a problem hiding this comment.
Main thread blocked by synchronous subprocess every 60 seconds
ScreenCaptureService.checkPermission() internally calls testCapturePermission(), which spawns a /usr/sbin/screencapture subprocess and calls process.waitUntilExit() — a synchronous blocking call. Since captureFrame() is isolated to @MainActor, this will freeze the main thread (and the entire UI) every 60 seconds for however long the screenshot subprocess takes (typically hundreds of milliseconds, potentially longer under load).
This was acceptable in startMonitoring() (a one-shot user-triggered action), but introducing it in the hot path of the 60-second capture loop creates a recurring UI freeze.
Fix: Either perform only the fast, non-blocking CGPreflightScreenCaptureAccess() check for the periodic recheck, or dispatch the permission test to a background queue:
// Option 1 – fast, non-blocking preflight only (good enough for revocation detection)
let permissionGranted = CGPreflightScreenCaptureAccess()
// Option 2 – offload the blocking subprocess to a background thread
let permissionGranted = await Task.detached(priority: .utility) {
ScreenCaptureService.checkPermission()
}.value| sendEvent(type: "permissionLost", data: ["permission": "screenRecording"]) | ||
| stopMonitoring() |
There was a problem hiding this comment.
Double event emission on permission revocation
sendEvent(type: "permissionLost", ...) is called at line 595, and then stopMonitoring() is called at line 596 — which unconditionally emits a second sendEvent(type: "monitoringStopped", data: [:]) at its end (line 488 of the file). The UI will therefore receive two back-to-back events: permissionLost immediately followed by monitoringStopped.
If the UI has a generic handler for monitoringStopped that dismisses or resets the banner/notification, it could silently swallow the permissionLost message before the user sees it. There is no synchronisation mechanism between the two events since stopMonitoring() is synchronous and fires its event in the same run-loop iteration.
Consider either:
- Passing a reason/cause parameter to
stopMonitoring()so it can embed it in themonitoringStoppedpayload, avoiding a separatepermissionLostevent, or - Skipping the separate
sendEventhere and relying on areason: "permissionLost"key insidemonitoringStopped.
| private var lastPermissionCheckTime: Date = .distantPast | ||
| private let permissionCheckInterval: TimeInterval = 60 |
There was a problem hiding this comment.
lastPermissionCheckTime not reset in stopMonitoring()
lastPermissionCheckTime retains its value across monitoring sessions. If monitoring is stopped and restarted within 60 seconds of the last check, the first periodic recheck in the new session will fire sooner than the configured interval (or not at all if the restart happens right at the boundary). More significantly, if monitoring restarts at T+59s after a check at T+0s, the first recheck in the new session will be delayed until T+60s — i.e., only 1 second after restart rather than 60 seconds, which is fine — but conversely if it restarts at T+1s the first check won't happen until T+60s (nearly a full minute into the new session while startMonitoring has already validated permission).
Consider resetting it in stopMonitoring() for consistent behavior:
| private var lastPermissionCheckTime: Date = .distantPast | |
| private let permissionCheckInterval: TimeInterval = 60 | |
| lastPermissionCheckTime = .distantPast | |
| private var lastPermissionCheckTime: Date = .distantPast |
Or, more practically, reset it alongside the other session state in stopMonitoring():
// Inside stopMonitoring(), alongside frameCount = 0 etc.
lastPermissionCheckTime = .distantPast
Fixes #5792
Problem: If the user revokes Screen Recording permission via System Settings while monitoring is active, the app silently fails on every capture attempt — losing Rewind data without any user notification.
Fix: Added periodic permission recheck (every 60 seconds) inside the capture loop:
ScreenCaptureService.checkPermission()on a 60s intervalpermissionLostevent so UI can show a notification/bannerScope: Minimal bug fix — no new features or refactoring.