Detecting screen recording settings on macOS Catalina

后端 未结 7 1412
清歌不尽
清歌不尽 2020-11-30 23:01

What\'s is a reliable way to detect if user has enabled this API?

CGWindowListCreateImage returns a valid object even if screen recording API is disable

相关标签:
7条回答
  • 2020-11-30 23:33

    I'm not aware of an API that's specifically for getting the screen recording permission status. Besides creating a CGDisplayStream and checking for nil, the Advances in macOS Security WWDC presentation also mentioned that certain metadata from the CGWindowListCopyWindowInfo() API will not be returned unless permission is granted. So something like this does seem to work, although it has the same issue of relying on implementation details of that function:

    private func canRecordScreen() -> Bool {
        guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
        return windows.allSatisfy({ window in
            let windowName = window[kCGWindowName as String] as? String
            return windowName != nil
        })
    }
    
    0 讨论(0)
  • 2020-11-30 23:36

    The above answer is not working fine. Below is the correct answer.

    private var canRecordScreen : Bool {
        guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
        return windows.allSatisfy({ window in
            let windowName = window[kCGWindowName as String] as? String
            let isSharingEnabled = window[kCGWindowSharingState as String] as? Int
            return windowName != nil || isSharingEnabled == 1
        })
      }
    
    0 讨论(0)
  • 2020-11-30 23:38

    All of the solutions presented here have a flaw in one way or another. The root of the problem is that there's no correlation between your permission to know about a window (via the name in the window list), your permission to know about the process owner of the window (such as WindowServer and Dock). Your permission to view the pixels on screen is a combination of two sparse sets of information.

    Here is a heuristic that covers all the cases as of macOS 10.15.1:

    BOOL canRecordScreen = YES;
    if (@available(macOS 10.15, *)) {
        canRecordScreen = NO;
        NSRunningApplication *runningApplication = NSRunningApplication.currentApplication;
        NSNumber *ourProcessIdentifier = [NSNumber numberWithInteger:runningApplication.processIdentifier];
    
        CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
        NSUInteger numberOfWindows = CFArrayGetCount(windowList);
        for (int index = 0; index < numberOfWindows; index++) {
            // get information for each window
            NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, index);
            NSString *windowName = windowInfo[(id)kCGWindowName];
            NSNumber *processIdentifier = windowInfo[(id)kCGWindowOwnerPID];
    
            // don't check windows owned by this process
            if (! [processIdentifier isEqual:ourProcessIdentifier]) {
                // get process information for each window
                pid_t pid = processIdentifier.intValue;
                NSRunningApplication *windowRunningApplication = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
                if (! windowRunningApplication) {
                    // ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
                }
                else {
                    NSString *windowExecutableName = windowRunningApplication.executableURL.lastPathComponent;
                    if (windowName) {
                        if ([windowExecutableName isEqual:@"Dock"]) {
                            // ignore the Dock, which provides the desktop picture
                        }
                        else {
                            canRecordScreen = YES;
                            break;
                        }
                    }
                }
            }
        }
        CFRelease(windowList);
    }
    

    If canRecordScreen is not set, you'll need to put up some kind of dialog that warns the user that they'll only be able to see the menubar, desktop picture, and the app's own windows. Here's how we presented it in our app xScope.

    And yes, I'm still bitter that these protections were introduced with little regard to usability.

    0 讨论(0)
  • 2020-11-30 23:43

    @marek-h posted a good example that can detect the screen recording setting without showing privacy alert. Btw, @jordan-h mentioned that this solution doesn't work when the app presents an alert via beginSheetModalForWindow.

    I found that SystemUIServer process is always creating some windows with names: AppleVolumeExtra, AppleClockExtra, AppleBluetoothExtra ...

    We can't get the names of these windows, before the screen recording is enabled in Privacy preferences. And when we can get one of these names at least, then it means that the user has enabled screen recording.

    So we can check the names of the windows (created by SystemUIServer process) to detect the screen recording preference, and it works fine on macOS Catalina.

    #include <AppKit/AppKit.h>
    #include <libproc.h>
    
    bool isScreenRecordingEnabled()
    {
        if (@available(macos 10.15, *)) {
            bool bRet = false;
            CFArrayRef list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
            if (list) {
                int n = (int)(CFArrayGetCount(list));
                for (int i = 0; i < n; i++) {
                    NSDictionary* info = (NSDictionary*)(CFArrayGetValueAtIndex(list, (CFIndex)i));
                    NSString* name = info[(id)kCGWindowName];
                    NSNumber* pid = info[(id)kCGWindowOwnerPID];
                    if (pid != nil && name != nil) {
                        int nPid = [pid intValue];
                        char path[PROC_PIDPATHINFO_MAXSIZE+1];
                        int lenPath = proc_pidpath(nPid, path, PROC_PIDPATHINFO_MAXSIZE);
                        if (lenPath > 0) {
                            path[lenPath] = 0;
                            if (strcmp(path, "/System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer") == 0) {
                                bRet = true;
                                break;
                            }
                        }
                    }
                }
                CFRelease(list);
            }
            return bRet;
        } else {
            return true;
        }
    }
    
    0 讨论(0)
  • 2020-11-30 23:43

    As of MacOS 10.15.7 the heuristics of obtaining window-names for visible windows, and so know we have screen-capture permission, doesn't always work. Sometimes we just don't find valid windows we can query, and would wrongly deduce we don't have permissions.

    However, I found another way to directly query (using sqlite) the Apple TCC database - the model where permissions are persisted. The screen-recording permissions are to be found in the "System level" TCC database ( residing in /Library/Application Support/com.apple.TCC/TCC.db). If you open the database using sqlite, and query: SELECT allowed FROM access WHERE client="com.myCompany.myApp" AND service="kTCCServiceScreenCapture" you'll get your answer.

    Two downsides comparing to other answers:

    • to open this TCC.db database, your app must have "Full Disk Access" permission. It doesn't need to run with 'root' privileges, and root privileges won't help if you don't have the "Full disk access".
    • it takes about 15 millisec to run, which is slower than querying the window list.

    The up side -- it's a direct query of the actual thing, and does not rely on any windows, or processes to exist at the time of query.

    Here's some draft code to do this:

    NSString *client = @"com.myCompany.myApp";
    sqlite3 *tccDb = NULL;
    sqlite3_stmt *statement = NULL;
    
    NSString *pathToSystemTCCDB = @"/Library/Application Support/com.apple.TCC/TCC.db";
    const char *pathToDBFile = [pathToSystemTCCDB fileSystemRepresentation];
    if (sqlite3_open(pathToDBFile, &tccDb) != SQLITE_OK)
       return nil;
        
    const char *query = [[NSString stringWithFormat: @"SELECT allowed FROM access WHERE client=\"%@\" AND service=\"kTCCServiceScreenCapture\"",client] UTF8String];
    if (sqlite3_prepare_v2(tccDb, query , -1, &statement, nil) != SQLITE_OK)
       return nil;
        
    BOOL allowed = NO;
    while (sqlite3_step(statement) == SQLITE_ROW)
        allowed |= (sqlite3_column_int(statement, 0) == 1);
    
    if (statement)
        sqlite3_finalize(statement);
    
    if (tccDb)
        sqlite3_close(tccDb);
    
    return @(allowed);
    

    }

    0 讨论(0)
  • 2020-11-30 23:47

    As of Nov19 chockenberry has correct answer.

    As @onelittlefish pointed out the kCGWindowName is being omitted in case user has not enabled the screen recording access in privacy pane. This method also doesn't trigger the privacy alert.

    - (BOOL)canRecordScreen
    {
        if (@available(macOS 10.15, *)) {
            CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
            NSUInteger numberOfWindows = CFArrayGetCount(windowList);
            NSUInteger numberOfWindowsWithName = 0;
            for (int idx = 0; idx < numberOfWindows; idx++) {
                NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
                NSString *windowName = windowInfo[(id)kCGWindowName];
                if (windowName) {
                    numberOfWindowsWithName++;
                } else {
                    //no kCGWindowName detected -> not enabled
                    break; //breaking early, numberOfWindowsWithName not increased
                }
    
            }
            CFRelease(windowList);
            return numberOfWindows == numberOfWindowsWithName;
        }
        return YES;
    }
    
    0 讨论(0)
提交回复
热议问题