Solipsism Gradient

Rainer Brockerhoff’s blog

Browsing Posts in Software

Re: Cocoa musings pt.1

No comments

In my post on event taps, I mentioned the following code to get a global event tap:

   CFMachPortRef tapg = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

This taps the event stream at the “annotated session” point (hence the kCGAnnotatedSessionEventTap parameter). Basically, this means that the event has already been analyzed as to destination application, and you can ask the passed event directly for that application’s process ID. This is was I was using in Klicko.

Unfortunately the docs don’t mention another step that takes place while “annotating” a mouse-down event: apparently, if the click was on a window’s title bar (not elsewhere in the window), the owning process is brought to the front before the tap sees the event! Klicko’s processing varied depending on the clicked-on window’s and the owning process’ state, so I was seeing different results depending on the click location – title bar or inside the window. Worse, a workaround I needed to do to bring non-main windows to the front for background processes wouldn’t be applied at all if the user clicked on a title bar!

A more subtle consequence was that I was intercepting (and discarding) mouse clicks, while the system was expecting the click that brought the process to the front to actually arrive at the process… the result was that, while the menu bar changed to the process, its windows would remain in the back.

After beating my head against this wall for several days, I was rereading the CGEvent docs again for ideas and noticed that I hadn’t tried applying a global tap before event annotation:

   CFMachPortRef tapg = CGEventTapCreate(kCGSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

Note the kCGSessionEventTap parameter. This meant that I had to discover the owning application’s process ID myself (instead of asking the mouse-down event directly). Premature optimization strikes again; I’d selected the annotated tap just to avoid doing these steps.

At any rate, I immediately discovered that I now could properly intercept clicks anywhere, discard them, and do process and window activation as I wanted to. Well, almost; there was still some redundant window activation to do, as Carbon and Cocoa apps seem to still have some residual differences in that regard. I’ll be filing bugs at Apple about some of these things.

This latest build of Klicko fixes all known bugs and edge cases, and some memory leaks. Hopefully this will be the last build for a long time.

In my next post I’ll explain some technical details.

Just saw Rob Keniger’s Wrapster plug-in for Coda, an interesting use of event taps; it’s a plug-in that intercepts key events for its master application.

The version I looked at (1.2) uses a global tap and checks if its master application is active:

   if(![NSApp isActive]) {
      return event;
   }

Of course, it would be conceptually more reliable to obtain the event’s destination process PSN and test against that; but the best way would be to tap the current process like this:

   ProcessSerialNumber psn = {kNoProcess, kCurrentProcess};
   CFMachPortRef tapg = CGEventTapCreateForPSN(&psn, kCGTailAppendEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), ProcessEvent, NULL);

Global event taps are a great tool, but they’re also easy to abuse, and I suppose that in a year or two we might have dozens of utilities or plug-ins tapping events; the actual results might be dependent on the order they run or load, for instance, and overall responsiveness might suffer. Therefore, please make absolutely sure that you need a global tap to do whatever you want to do, that it takes as little time as possible, and that it does careful parameter checking.

One great application to test event taps (and to check their presence) is Event Tap Testbench. Its author, Bill Cheeseman, has helped me a lot with subtle details of both event taps and the Accessibility APIs; thanks Bill! Do also check out their other products.

Rob tests if Accessibility is enabled by trying to create a tap and seeing if it succeeds. While this works, I think it’s easier to do it like this:

   if (!AXAPIEnabled()&&!AXIsProcessTrusted()) {
      // error dialog here
   }

The first call checks if Accessibility is enabled in System Preferences; the second checks if your process is allowed to set a key tap even with Accessibility disabled. To achieve this, you must (from a process running as root) call AXMakeProcessTrusted() on the tapping application’s executable – this will take effect on its next run.

In the case of Klicko, it taps only mouse button events, for which Accessibility doesn’t need to be on; however, it uses the Accessibility APIs to check out details about the clicked-on windows. Some people dislike turning Accessibility on in System Preferences, so a future version of Klicko might use AXMakeProcessTrusted() to avoid this; this means asking the user for an Administrator password and running a separate tool, like Quay does. I’m still evaluating the trade-offs for that: it precludes running Klicko from ~/Applications or from the disk image, adds to the application size, and the user has to navigate an extra dialog on first run.

Update:Klicko now uses AXMakeProcessTrusted().

Found an error in the code signature for both Klicko and Quay, induced by the presence of a newer signing certificate for iPhone apps.

So I pushed out new builds for Quay 1.1.1 (285) and Klicko 1.0.1 (103). Klicko also has other small improvements; see the release notes.

Klicko 1.0.1 (97)

No comments

I decided to rev the version number, not just the build number, for this release.

The main reason is that now we have a slight difference (and a UI difference) on first run. Klicko needs “access for assistive devices” turned on in the System Preference, and complains if that’s not the case. However, now you have two options:

1- turn “access for assistive devices” on, and click “Continue”

2- click “Authorize” and enter your administrator password.

In the latter case – and this will work only if you installed Klicko inside the main /Applications folder – Klicko will set itself to run in a privileged group that can use the Accessibility API without that option.

Beyond small internal optimizations, the other notable feature in 1.0.1 is that you can change the icon, if you don’t like it. Just make or copy your own icon, paste onto Klicko’s icon in the Finder’s “Get Info” panel, and there it is – you can do this even while Klicko is running! If your icon has a 16×16-pixel version, that one will be used in the menu bar.

Of course, if you have an icon for Klicko that’s better than the default one, send it in! It shouldn’t be too hard to do… icon_wink.gif

And, yet another small bug fix release (87). This one allows clicking on windows belonging to background or GUI enhancement apps like Growl, Default Folder X and others.

Cocoa musings pt.1

No comments

One of the reasons for my taking a week or two off to work mostly on the just-released Klicko was that I like to rework, and group together, code snippets that worked well for me in earlier applications, and see if I can update them to conform to my slowly growing experience. I’m also prone to digress; one such digression (several months!) resulted in the release of the unexpectedly popular RBSplitView.

Both Klicko and Quay use code that, like RBSplitView, were destined for XRay II, the supposed successor of XRay, which sadly is not reliable under Leopard. Alas, at this writing, it sounds like XRay II will remain in the freezer, its mummy mined for code snippets and general philosophical experience… but the basic idea persists, and something quite equivalent (but also quite different in detail) is already being conceived.

Both Quay and Klicko do part of their seeming magic with a technology called “Quarz Event Taps” (PDF file). This was introduced in Tiger, and perfected in Leopard. Briefly, an event tap is a C callback routine that is called to filter low-level user input events at some points in the system’s event processing, which is actually quite complex. Events can be generated, examined, modified or even suppressed before they’re delivered to an application. Since user input events are usually routed to the foreground window (that is, to the foreground application, even if it has no window), this makes event taps quite powerful.

You can make a global event tap, or a per-process tap. Quay sets up a tap on the Dock process to intercept clicks on Dock icons. Klicko uses a global tap to check for clicks on background windows.

Tapping one application is, in principle, easy: you locate the application to be tapped by its PSN (Process Serial Number), set up the tap, tie it to your application’s main run loop, and that’s it. Here’s what a bare-bones implementation would look like:

// This is the callback routine, called for every tapped event.

CGEventRef ProcessEvent(CGEventTapProxy tapProxy, CGEventType type, CGEventRef event, void *refcon) {
   switch (type) {
      case kCGEventLeftMouseDown:
         // process a mouse down
         break;
      case kCGEventLeftMouseUp:
         // process a mouse down
         break;
   }
   return event;   // return the tapped event (might have been modified, or set to NULL)
               // returning NULL means the event isn't passed forward
}

// Here's how you set up the tap: we're catching mouse-down and mouse-up

...
   ProcessSerialNumber psn;
   // get the PSN for the app to be tapped; usually with the routines in <Processes.h>
...
   CFMachPortRef tapg = CGEventTapCreateForPSN(&psn, kCGTailAppendEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);
   if (!tapg) {   // bail out if the tap couldn't be created
      NSLog(@"application tap failed");
      [NSApp terminate:nil];
   }
   CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tapg, 0);
   if (!source) {   // bail out if the run loop source couldn't be created
      NSLog(@"runloop source failed");
      [NSApp terminate:nil];
   }
   CFRelease(tapg);   // can release the tap here as the source will retain it; see below, however
   CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
   CFRelease(source);  // can release the source here as the run loop will retain it

After that, all should work – in principle. The devil is in the details. Here’s how you locate a running application by its application ID and return its PSN:

BOOL GetPSNForApplicationID(NSString* appid, ProcessSerialNumber* outPSN) {
   outPSN.highLongOfPSN = outPSN.lowLongOfPSN = kNoProcess;
   while (GetNextProcess(outPSN)==noErr) {
      NSDictionary* pdict = [(NSDictionary*)ProcessInformationCopyDictionary(&psn,
         kProcessDictionaryIncludeAllInformationMask) autorelease];
      if ([[pdict stringForKey:(id)kCFBundleIdentifierKey] isEqualToString:appid]) {
         return YES;
      }
   }
   return NO;
}

To make a global tap, you don’t need a PSN. Just use the following tap creation call instead:

   CFMachPortRef tapg = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

More details. If you’re tapping an application, it may not be running; CGEventTapCreateForPSN will return NULL in that case. Or it may quit while you have the tap set up. You probably want to monitor that process and either quit, or rerun the application, or wait for it to come back up. In the latter cases, you’ll have to back out of the now-dead tap carefully:

   CFMachPortInvalidate(tapg);
   CFRunLoopRemoveSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
   CFRelease(tapg);   // this CFRelease has been moved from before the CFRunLoopAddSource

supposing, of course, that you have held on to those two variables. Note how the CFRelease(tapg) should, in such a case, happen only after the source has been removed from the run loop; otherwise invalidating the tap will cause the run loop to crash. You can use the same technique to close a global event tap, though usually there’s no need; if your app crashes or quits, the tap will be closed automatically.

However, there’s a serious problem while debugging an event tap. If you’re tapping a single application, and set a breakpoint inside yours (or break into the debugger anywhere because of a crash or exception), both applications will stop. If the same happens while a global tap is active, the entire system stops accepting user input! The only way to recover is to ssh/telnet in from another machine, and kill Xcode. So even if you prefer NSLog/printf calls to breakpoints, this will be very inconvenient for all but the simplest callback code.

The solution I found was to always use an application tap while debugging. An easy way is to define, as I always do, a special macro inside the main project build configuration panel (but for the debug configuration only): inside the “Preprocessor Macros Not Used In Precompiled Headers” (aka GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS) write “DEBUG”, and then, instead of the global tap, compile in an application tap on some always-present application (like the Finder) by using #ifdef DEBUG/#else/#endif statements.

Even that isn’t always sufficient, as Xcode 3 notoriously may invoke the debugger (even on your release build!) if your app crashes. You must either get used to never clicking on “Build & Go” for your release build, or you must make a runtime check for the debugger. The latter will prevent inadvertent freezes, but if you forget to take it out before deployment, your application will behave oddly if a curious user runs it under a debugger.

This post is already too long, so I’ll talk only briefly about what you can do inside the event tap callback itself. Every possible execution path should be short and contain no long loops or wait points. If you’re just watching events, always return the same event passed in as parameter. Return NULL if you want to suppress an event; however, be careful to suppress entire event groups. For instance, if you decide to suppress a mouse-down, store the event number and also suppress subsequent mouse-dragged and mouse-up events with the same number; otherwise the destination application may behave oddly. Some apps may behave oddly when tapped, by the way.

Update: I previously said here that to intercept or generate keyboard events, your application must run with setgid 0 (the “wheel” group). I was mistaken; my apologies. Your application must run setuid root to make an event tap at the third tap point (which I didn’t mention here), which is where the events enter the window server (kCGHIDEventTap).

Just pushed out Klicko version 1.0 (79) and Quay 1.1.1 (283). Both are just small bug fix releases. Hopefully this will make Quay useable again for everybody while I work on the upcoming 1.2, which should be a huge step forward in functionality. There are still a few outstanding Klicko bugs, so be sure to check for updates over the next days, too.

Photos licensed by Creative Commons license. Unless otherwise noted, content © 2002-2024 by Rainer Brockerhoff. Iravan child theme by Rainer Brockerhoff, based on Arjuna-X, a WordPress Theme by SRS Solutions. jQuery UI based on Aristo.