Search code examples
macosaccessibilitycore-graphicscursor-position

macOS: simulated mouse event only works when launching binary, not application bundle


I have the following code which moves the mouse cursor on a Mac:

void moveCursorPos()
{
  CGPoint ppos;
  ppos.x = 100;
  ppos.y = 100;

  CGEventRef e = CGEventCreateMouseEvent(nullptr, kCGEventMouseMoved, ppos, kCGMouseButtonLeft);
  CGEventPost(kCGHIDEventTap, e);
  CFRelease(e);
}

It works when I run my software directly from its binary - e.g. ./foo.app/Contents/MacOS/foo . It does not not work when I start it through open foo.app. Doing open ./foo.app/Contents/MacOS/foo works.

The first time I launched it and called that function, macOS asked me for the permission to use accessibility APIs (or something in that vein), which I granted - if I go to the "Accessibility > Privacy" pane of the security settings of macOS, all checkboxes are checked, everything is granted to foo, etc...

  • How do I fix it on my machine ?
  • What can I do in terms of code so that the users of my software do not ever encounter that issue as it breaks a core UI interaction of my software ?

Solution

  • How do I fix it on my machine?

    It really depends on what you did. I usually broke these things when I run an app from Xcode directly, distribution, ... and all of them have same bundle identifier (one entry in System Preferences, but more binaries around).

    To reset Accessibility for your app just run:

    sudo tccutil reset Accessibility com.your.bundle.Identifier
    

    It will reset Accessibility settings for all occurrences of your binaries and you can start again.

    What can I do in terms of code so that the users of my software do not ever encounter that issue as it breaks a core UI interaction of my software?

    Do the following in your AppDelegate.applicationDidFinishLaunching:

    acquireAccessibilityPrivileges(acquired: {
        self.moveCursorPos()
    }, nope: {
        NSApp.terminate(self)
    })
    

    Other functions:

    func acquireAccessibilityPrivileges(acquired: @escaping () -> Void, nope: @escaping () -> Void) {
        let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true]
        let enabled = AXIsProcessTrustedWithOptions(options as CFDictionary)
    
        if enabled {
            acquired()
        } else {
            let alert = NSAlert()
            alert.messageText = "Enable XYZ"
            alert.informativeText = "Click OK once you enabled XYZ in System Preferences - ..."
            alert.beginSheetModal(for: self.window, completionHandler: { response in
                if AXIsProcessTrustedWithOptions(options as CFDictionary) {
                    acquired()
                } else {
                    nope()
                }
            })
        }
    }
    
    func moveCursorPos() {
        let event = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: CGPoint(x: 100, y: 100), mouseButton: .left)
        event?.post(tap: .cghidEventTap)
    }
    

    open foo.app vs ./foo

    There's also one thing to be aware of - the difference between open foo.app, ./foo.app/Contents/MacOS/foo, ...

    open Foo.app:

    • Foo.app needs this accessibility permissions
    -+= 00001 root /sbin/launchd
     |--= 83677 zrzka /Users/zrzka/Desktop/Foo.app/Contents/MacOS/Foo
    

    Foo.app/Contents/MacOS/Foo:

    • Iterm.app needs this accessibility permissions
    -+= 00001 root /sbin/launchd
     |-+= 53984 zrzka /Applications/iTerm.app/Contents/MacOS/iTerm2
     | |-+= 53986 zrzka /Applications/iTerm.app/Contents/MacOS/iTerm2 --server /usr/bin/login -fpl zrzka /Applications/iTerm.app/Contents/MacOS/iTerm2 --launch_shell
     | | \-+= 53987 root /usr/bin/login -fpl zrzka /Applications/iTerm.app/Contents/MacOS/iTerm2 --launch_shell
     | |   \-+= 53988 zrzka -zsh
     | |     \--= 84461 zrzka Foo.app/Contents/MacOS/Foo
    

    open Foo.app/Contents/MacOS/Foo:

    • Terminal.app is launched and Foo in it
    • That's because I do use iTerm, but the default one is Terminal
    • Terminal.app needs this accessibility permissions
    -+= 00001 root /sbin/launchd
     |-+= 73320 zrzka /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
     | \-+= 75674 root login -pf zrzka
     |   \-+= 75679 zrzka -zsh
     |     \--= 75717 zrzka /Users/zrzka/Desktop/Foo.app/Contents/MacOS/Foo
    

    More tips

    It happens, from time to time, that my app no longer appears in the System Preferences - Security & Privacy - Accessibility. I don't know why, but it's somehow connected with my tccutil resets. Open Finder and drag & drop the Foo.app into the pane.