fix: avoid macos bug where many permission dialogs pop (closes #5106)

This commit is contained in:
lwouis committed 2026-01-04 22:53:54 +01:00
1 parent 7821d7c979
commit 5c4199ec74
3 files changed
+53 -5

No files matched your search

+30 -2
View File
@@ -56,11 +56,12 @@ class WindowCaptureScreenshots {
}
private static func oneTimeCapture(_ scWindow: SCWindow, _ source: RefreshCausedBy) {
guard !App.app.isTerminating else { return }
let config = SCStreamConfiguration.forWindow(scWindow, false)
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
SCScreenshotManager.captureSampleBuffer(contentFilter: filter, configuration: config) { sampleBuffer, error in
guard let sampleBuffer, error == nil else { Logger.error { "\(sampleBuffer == nil) \(error)" }; return }
ActiveWindowCaptures.increment()
SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) { cgImage, error in
ActiveWindowCaptures.decrement()
guard let cgImage, error == nil else { Logger.error { "\(cgImage == nil) \(error)" }; return }
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
DispatchQueue.main.async {
@@ -89,9 +90,12 @@ class WindowCaptureScreenshotsPrivateApi {
}
private static func oneTimeCapture(_ wid: CGWindowID) -> CGImage? {
guard !App.app.isTerminating else { return nil }
// we use CGSHWCaptureWindowList because it can screenshot minimized windows, which CGWindowListCreateImage can't
var windowId_ = wid
ActiveWindowCaptures.increment()
let list = CGSHWCaptureWindowList(CGS_CONNECTION, &windowId_, 1, [.ignoreGlobalClipShape, .bestResolution, .fullSize]).takeRetainedValue() as! [CGImage]
ActiveWindowCaptures.decrement()
return list.first
}
}
@@ -282,3 +286,27 @@ extension CMSampleBuffer {
return device.makeTexture(descriptor: desc, iosurface: surface, plane: 0)
}
}
class ActiveWindowCaptures {
private static var count: Int = 0
private static let semaphore = DispatchSemaphore(value: 1)
static func increment() {
semaphore.wait()
count += 1
semaphore.signal()
}
static func decrement() {
semaphore.wait()
count -= 1
semaphore.signal()
}
static func value() -> Int {
semaphore.wait()
let current = count
semaphore.signal()
return current
}
}
+16 -3
View File
@@ -5,9 +5,9 @@ if let command = CliClient.detectCommand() {
CliClient.sendCommandAndProcessResponse(command)
}
// - if the app is quit/force-quit from Activity Monitor, it will receive SIGTERM and applicationWillTerminate won't be called
// - if the app crashes in swift code (e.g. unexpected nil object), SIGTRAP is sent
// we intercept these signals, and do an emergency exit
// - SIGTERM: if the app is quit/force-quit from Activity Monitor, it will receive SIGTERM and applicationWillTerminate won't be called
// - SIGTRAP: if the app crashes in swift code (e.g. unexpected nil object), SIGTRAP is sent
// - SIGKILL: if we stop the app using SIGKILL (e.g. stopping from IntelliJ, or from the terminal), there is no chance to intercept it
[SIGTERM, SIGTRAP].forEach {
signal($0) { s in
emergencyExit("Exiting after receiving signal", s)
@@ -34,5 +34,18 @@ fileprivate func emergencyExit(_ logs: Any?...) {
setNativeCommandTabEnabled(true)
print(logs)
printStackTrace()
makeSureAllCapturesAreFinished()
exit(0)
}
func makeSureAllCapturesAreFinished() {
App.app.isTerminating = true
let timeout = 5.0
let startTime = DispatchTime.now()
var elapsedTime = 0.0
while ActiveWindowCaptures.value() > 0 && elapsedTime <= timeout {
Logger.warning { "There are \(ActiveWindowCaptures.value()) screenshots in progress. We need to wait for them to avoid a bug where macOS shows permission dialogs to the user for no reason." }
Thread.sleep(forTimeInterval: 0.1)
elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
}
}
+7
View File
@@ -17,6 +17,7 @@ class App: AppCenterApplication {
static let website = "https://alt-tab-macos.netlify.app"
static let appIcon = CGImage.named("app.icns")
static var app: App!
var isTerminating = false
var thumbnailsPanel: ThumbnailsPanel!
var previewPanel: PreviewPanel!
var preferencesWindow: PreferencesWindow!
@@ -331,6 +332,12 @@ extension App: NSApplicationDelegate {
// symbolic hotkeys state persist after the app is quit; we restore this shortcut before quitting
setNativeCommandTabEnabled(true)
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
Logger.info { "" }
makeSureAllCapturesAreFinished()
return .terminateNow
}
}
enum RefreshCausedBy {