Skip to content

Commit

Permalink
Run event tap on background thread.
Browse files Browse the repository at this point in the history
  • Loading branch information
niw committed Mar 22, 2020
1 parent 325ac48 commit 77fd1fc
Showing 1 changed file with 174 additions and 55 deletions.
229 changes: 174 additions & 55 deletions HapticKey/Classes/HTKEventTap.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,132 @@

NS_ASSUME_NONNULL_BEGIN

static CGEventRef EventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventRef, void * _Nullable userInfo)
@class HTKEventTapLoop;

@protocol HTKEventTapLoopDelegate <NSObject>

@optional
- (void)eventTapLoopDidDisableEventTap:(HTKEventTapLoop *)eventTapLoop;
- (void)eventTapLoop:(HTKEventTapLoop *)eventTapLoop didTapCGEvent:(CGEventRef)eventRef;

@end

@interface HTKEventTapLoop : NSObject

@property (nonatomic, weak, nullable) id<HTKEventTapLoopDelegate> delegate;
@property (nonatomic, nullable) NSThread *thread;

@end

@implementation HTKEventTapLoop

static CGEventRef EventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventRef, void * _Nullable userInfo)
{
@autoreleasepool {
HTKEventTap * const eventTap = (__bridge HTKEventTap *)userInfo;
// Called on the background thread.

switch (type) {
case kCGEventTapDisabledByTimeout:
case kCGEventTapDisabledByUserInput: {
eventTap.enabled = NO;
HTKEventTapLoop * const eventTapLoop = (__bridge HTKEventTapLoop *)userInfo;

os_log_error(OS_LOG_DEFAULT, "Event tap disabled by type: %d", type);
switch (type) {
case kCGEventTapDisabledByTimeout:
case kCGEventTapDisabledByUserInput: {
os_log_error(OS_LOG_DEFAULT, "Event tap disabled by type: %d", type);

id<HTKEventTapDelegate> const delegate = eventTap.delegate;
if ([delegate respondsToSelector:@selector(eventTapDidDisable:)]) {
[delegate eventTapDidDisable:eventTap];
}
break;
id<HTKEventTapLoopDelegate> const delegate = eventTapLoop.delegate;
if ([delegate respondsToSelector:@selector(eventTapLoopDidDisableEventTap:)]) {
[delegate eventTapLoopDidDisableEventTap:eventTapLoop];
}
default: {
// `eventWithCGEvent:` returns an autoreleased `NSEvent` that retains given `CGEvent`.
// without `@autoreleasepool`, this will may leak and also `CGEvent` as well.
NSEvent * const event = [NSEvent eventWithCGEvent:eventRef];

id<HTKEventTapDelegate> const delegate = eventTap.delegate;
if ([delegate respondsToSelector:@selector(eventTap:didTapEvent:)]) {
[delegate eventTap:eventTap didTapEvent:event];
}
break;
break;
}
default: {
id<HTKEventTapLoopDelegate> const delegate = eventTapLoop.delegate;
if ([delegate respondsToSelector:@selector(eventTapLoop:didTapCGEvent:)]) {
[delegate eventTapLoop:eventTapLoop didTapCGEvent:eventRef];
}
break;
}
}

return eventRef;
return eventRef;
}

- (void)startWithEventMask:(CGEventMask)eventMask
{
if (self.thread) {
return;
}

__weak typeof (self) weakSelf = self;
NSThread * const thread = [[NSThread alloc] initWithBlock:^{
// Called on the background thread.

typeof (self) const strongSelf = weakSelf;
if (!strongSelf) {
return;
}

CFMachPortRef const eventTap = CGEventTapCreate(kCGSessionEventTap, kCGTailAppendEventTap, kCGEventTapOptionListenOnly, eventMask, EventTapCallback, (__bridge void *)strongSelf);
if (!eventTap) {
return;
}

CFRunLoopSourceRef const runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
if (!runLoopSource) {
CFRelease(eventTap);
return;
}

CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

CGEventTapEnable(eventTap, true);
os_log_info(OS_LOG_DEFAULT, "Event tap enabled: %p", eventTap);

// This run loop is ended by `_htk_thread_stop` call.
CFRunLoopRun();

CGEventTapEnable(eventTap, false);
os_log_info(OS_LOG_DEFAULT, "Event tap disabled: %p", eventTap);

CFRunLoopRemoveSource(CFRunLoopGetMain(), runLoopSource, kCFRunLoopCommonModes);
CFRelease(runLoopSource);

CFMachPortInvalidate(eventTap);
CFRelease(eventTap);
}];
thread.name = @"at.niw.HapticKey.HTKEventTapLoop.thread";
[thread start];

self.thread = thread;
}

@implementation HTKEventTap
- (void)stop
{
CFMachPortRef _eventTap;
CFRunLoopSourceRef _runLoopSource;
if (!self.thread) {
return;
}

// `_htk_thread_stop` is called on the background thread while its run loop runs.
[self performSelector:@selector(_htk_thread_stop) onThread:self.thread withObject:nil waitUntilDone:YES];

self.thread = nil;
}

- (void)_htk_thread_stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
}

@end

// MARK: -

@interface HTKEventTap () <HTKEventTapLoopDelegate>

@property (nonatomic, nullable) HTKEventTapLoop *loop;

@end

@implementation HTKEventTap

- (instancetype)init
{
return [self initWithEventMask:kCGEventMaskForAllEvents];
Expand All @@ -71,9 +156,14 @@ - (void)dealloc
[self _htk_main_disable];
}

- (BOOL)isEnabled
{
return self.loop != nil;
}

- (void)setEnabled:(BOOL)enabled
{
if (_enabled != enabled) {
if (self.enabled != enabled) {
if (enabled) {
[self _htk_main_enable];
} else {
Expand All @@ -84,47 +174,76 @@ - (void)setEnabled:(BOOL)enabled

- (void)_htk_main_enable
{
if (_eventTap) {
if (self.loop) {
return;
}
if (_runLoopSource) {

HTKEventTapLoop * const loop = [[HTKEventTapLoop alloc] init];
loop.delegate = self;
[loop startWithEventMask:self.eventMask];

self.loop = loop;
}

- (void)_htk_main_disable
{
if (!self.loop) {
return;
}

_eventTap = CGEventTapCreate(kCGSessionEventTap, kCGTailAppendEventTap, kCGEventTapOptionListenOnly, self.eventMask, EventTapCallback, (__bridge void *)self);
if (_eventTap) {
_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, _eventTap, 0);
if (_runLoopSource) {
CFRunLoopAddSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes);
CGEventTapEnable(_eventTap, true);
[self.loop stop];

os_log_info(OS_LOG_DEFAULT, "Event tap enabled: %p", _eventTap);
self.loop = nil;
}

_enabled = YES;
} else {
CFRelease(_eventTap);
_eventTap = NULL;
// MARK: - HTKEventTapLoopDelegate

- (void)eventTapLoopDidDisableEventTap:(HTKEventTapLoop *)eventTapLoop
{
// Called on the background thread.

__weak typeof (self) const weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
typeof (self) const strongSelf = weakSelf;
if (!strongSelf) {
return;
}
}

strongSelf.enabled = NO;

id<HTKEventTapDelegate> delegate = strongSelf.delegate;
if ([delegate respondsToSelector:@selector(eventTapDidDisable:)]) {
[delegate eventTapDidDisable:strongSelf];
}
});
}

- (void)_htk_main_disable
- (void)eventTapLoop:(HTKEventTapLoop *)eventTapLoop didTapCGEvent:(CGEventRef)eventRef
{
if (_runLoopSource) {
CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes);
CFRelease(_runLoopSource);
_runLoopSource = NULL;
}
if (_eventTap) {
CGEventTapEnable(_eventTap, false);
// Called on the background thread.

os_log_info(OS_LOG_DEFAULT, "Event tap disabled: %p", _eventTap);
CFRetain(eventRef);

CFRelease(_eventTap);
_eventTap = NULL;
}
__weak typeof (self) const weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
typeof (self) const strongSelf = weakSelf;
if (!strongSelf) {
return;
}

id<HTKEventTapDelegate> delegate = strongSelf.delegate;
if ([delegate respondsToSelector:@selector(eventTap:didTapEvent:)]) {
// `eventWithCGEvent:` returns an autoreleased `NSEvent` that retains given `CGEvent`.
// without `@autoreleasepool`, this will may leak and also `CGEvent` as well.
@autoreleasepool {
// `NSEvent` must be instantiate on main thread, or some properties may not be prepared such as `allTouches`.
NSEvent * const event = [NSEvent eventWithCGEvent:eventRef];
[delegate eventTap:strongSelf didTapEvent:event];
}
}

_enabled = NO;
CFRelease(eventRef);
});
}

@end
Expand Down

0 comments on commit 77fd1fc

Please sign in to comment.