macOS application hangs if accessibility changed while using CGEventTap

Hi all:

I have a macOS application which capture mouse events:

    CGEventMask eventMask = CGEventMaskBit(kCGEventMouseMoved) |
                            CGEventMaskBit(kCGEventLeftMouseUp) |
                            CGEventMaskBit(kCGEventLeftMouseDown) |
                            CGEventMaskBit(kCGEventRightMouseUp) |
                            CGEventMaskBit(kCGEventRightMouseDown) |
                            CGEventMaskBit(kCGEventOtherMouseUp) |
                            CGEventMaskBit(kCGEventOtherMouseDown) |
                            CGEventMaskBit(kCGEventScrollWheel) |
                            CGEventMaskBit(kCGEventLeftMouseDragged) |
                            CGEventMaskBit(kCGEventRightMouseDragged) |
                            CGEventMaskBit(kCGEventOtherMouseDragged);
    _eventTap = CGEventTapCreate(kCGHIDEventTap,
                                 kCGHeadInsertEventTap,
                                 kCGEventTapOptionDefault,
                                 eventMask,
                                 &MouseCallback,
                                 nil);   
    _runLoopRef = CFRunLoopGetMain();
    _runLoopSourceRef = CFMachPortCreateRunLoopSource(NULL, _eventTap, 0);
    CFRunLoopAddSource(_runLoopRef, _runLoopSourceRef, kCFRunLoopCommonModes);
    CGEventTapEnable(_eventTap, true);
CGEventRef MouseCallback(CGEventTapProxy proxy,
                         CGEventType type,
                         CGEventRef event,
                         void *refcon) {
    NSLog(@"Mouse event: %d", type);
    return event;
}

This mouse logger need accessibility privilege granted in Privacy & Security. But I found that if accessibility turned off while CGEventTap is running, left & right click are blocked, unless restart macOS.

Although replace kCGEventTapOptionDefault to kCGEventTapOptionListenOnly can fix this issue, but I have other feature which require kCGEventTapOptionDefault.

So I try to detect accessibility is disabled and remove CGEventTap:

[[NSDistributedNotificationCenter defaultCenter] addObserver:self
  selector:@selector(didToggleAccessStatus:)
  name:@"com.apple.accessibility.api"
  object:nil
  suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
}

However, the notification won't be sent if user didn't turn off accessibility but removed it from list. Worse, AXIsProcessTrusted() continues to return true.

Is there a way to fix mouse blocked, or detect accessibility removed?

Thanks!

Accepted Reply

The only reliable way I've found to detect accessibility access is to try creating a new event tap with kCGEventTapOptionDefault. If the permission is missing, CGEventTapCreate will return NULL. In my case I was dealing with keyboard events, not mouse events, but I expect it will work the same. Here's the code I use:

bool	EventListener::CanFilterEvents()
{
	CFMachPortRef thePort = CGEventTapCreate(
		kCGSessionEventTap,
		kCGTailAppendEventTap,
		kCGEventTapOptionDefault,	// active filter, not passive listener
		CGEventMaskBit(kCGEventKeyDown),
		CTapListener::MyTapCallback,
		NULL );
	bool madeTap = (thePort != NULL);
	if (madeTap)
	{
		CFMachPortInvalidate( thePort );
		CFRelease( thePort );
	}
	
	return madeTap;
}
  • Is there any side-effect if keep creating new CGEventTap? If not, maybe I'll try to write a timer to check tap null every 10 seconds.

  • @benson3816 As far as I know, there is no side-effect to repeatedly creating event taps, so long as you clean up afterward as shown in my code.

Add a Comment

Replies

Thanks for taking the time to write about this issue you are having. I want to make sure your report gets to the right team to take a look at this, would you mind filing a bug report using Feedback Assistant? This will get your issue in our tracking system, and uploading code snippets like this and even a sysdiagnose from your Mac can help us understand the issue more, https://developer.apple.com/bug-reporting/

Once you create a bug report, feel free to reply here with your Feedback ID!

  • I have written a feedback, ID: FB12889998.

Add a Comment

The only reliable way I've found to detect accessibility access is to try creating a new event tap with kCGEventTapOptionDefault. If the permission is missing, CGEventTapCreate will return NULL. In my case I was dealing with keyboard events, not mouse events, but I expect it will work the same. Here's the code I use:

bool	EventListener::CanFilterEvents()
{
	CFMachPortRef thePort = CGEventTapCreate(
		kCGSessionEventTap,
		kCGTailAppendEventTap,
		kCGEventTapOptionDefault,	// active filter, not passive listener
		CGEventMaskBit(kCGEventKeyDown),
		CTapListener::MyTapCallback,
		NULL );
	bool madeTap = (thePort != NULL);
	if (madeTap)
	{
		CFMachPortInvalidate( thePort );
		CFRelease( thePort );
	}
	
	return madeTap;
}
  • Is there any side-effect if keep creating new CGEventTap? If not, maybe I'll try to write a timer to check tap null every 10 seconds.

  • @benson3816 As far as I know, there is no side-effect to repeatedly creating event taps, so long as you clean up afterward as shown in my code.

Add a Comment

The only reliable way I've found to detect accessibility access

You don’t need ‘full’ accessibility access to listen for events. There’s a specific TCC service for that, namely ListenEvent. You can check where you have that privilege using CGPreflightListenEventAccess.

Is there some reason that doesn’t work for you?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

You don’t need ‘full’ accessibility access to listen for events. There’s a specific TCC service for that, namely ListenEvent. You can check where you have that privilege using CGPreflightListenEventAccess. Is there some reason that doesn’t work for you?

In my case, I do need to post events as well as listen, and the original poster of this thread also indicates a need to do more than just listen.

I'm aware that there is also a function CGPreflightPostEventAccess, but in my experience that does not update while my process is running.

You don’t need ‘full’ accessibility access to listen for events. There’s a specific TCC service for that, namely ListenEvent. You can check where you have that privilege using CGPreflightListenEventAccess.

@eskimo As written in my post, I have other feature which require kCGEventTapOptionDefault, so ‘full’ accessibility access is needed.

I'm not sure whether access tcc.db is a good idea, it might stop working in the future. Is there an approach which use public API?

It’s true that PostEvent and ListenEvent are different services, but:

  • They both have CG helpers, CGRequest{Post,Listen}EventAccess and CGRequest{Post,Listen}EventAccess.

  • They both work in sandboxed apps.

  • Neither requires the full Accessibility privilege.

benson3816 wrote:

I'm not sure whether access tcc.db is a good idea

It’s definitely not, and I certainly didn’t mean to suggest that. I’m using the term TCC because it’s really helping to understand that when it shows up in logs and so on. There’s also the tccutil command-line tool. And I’m using specific TCC service names because that’s what you pass in to tccutil. Annoyingly, there isn’t a one-to-one relationship between those and the items listed in System Settings > Privacy & Security.

JWWalker wrote:

in my experience that does not update while my process is running.

Indeed. Honestly, I think the best option there is to accept the quit and relaunch.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo:

Neither requires the full Accessibility privilege.

That may be true under the hood, but while Listen access is (I think) reflected in System Settings > Privacy & Security > Input Monitoring, PostEvent access is shown as System Settings > Privacy & Security > Accessibility.

Honestly, I think the best option there is to accept the quit and relaunch.

It might be OK to repeatedly quit and relaunch while waiting for accessibility permission to be granted, but I wouldn't want to constantly be quitting and relaunching to detect the rare case of permission being revoked. Which was the OP's concern.

PostEvent access is shown as System Settings > Privacy & Security > Accessibility.

Yep. That is super confusing.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The mouse logger is designed as long-running process, so relaunch repeatedly is not an option.

I prefer JWWalker's CGEventTapCreate == nil solution, tested it with a timer and worked.

Althrough I was hoping there is a notification about this, but polling is fine. I will implement this method into my app.

Thank you both for the response!