1

I have the following code to send virtual keypresses to a process given its pid

    NSRunningApplication* app = [NSRunningApplication
                                 runningApplicationWithProcessIdentifier: pid];
    [app activateWithOptions: (NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];

    event1 = CGEventCreateKeyboardEvent (NULL, (CGKeyCode)cg_key_code, true);
    event2 = CGEventCreateKeyboardEvent (NULL, (CGKeyCode)cg_key_code, false);

    CGEventPost(kCGHIDEventTap, event1);
    CGEventPost(kCGHIDEventTap, event2);

The process that I wish to send keypresses to promptly comes to the front, as expected. But the problem is, the first keypress is going to the application which was at front before my application came front. When tested, [app isActive] returns false for the first time. After the first key, everything goes fine.

Why is this happening? Even though I am posting the key event after getting my process to the front.

Pavan Manjunath
  • 27,404
  • 12
  • 99
  • 125

3 Answers3

2

As the documentation doesn't say that activateWithOptions: waits, and it doesn't provide a completion block to callback when the application has come to the foreground we can assume that the method will return as soon as the validity of the switch have been checked and the activation message sent. There will inevitably be some latency between this happening and the application actually being ready to receive user input.

While we could hope that OS X would buffer the user input and send it to the application when it's ready there will always be a race condition in this case so it's prudent to code with the expectation that you need to wait.

Just waiting for a set time isn't a great idea, but you have the tools to determine what you should do and for how long - specifically using isActive. Also, it's prudent to check the response of activateWithOptions: to ensure we don't end up deadlocked.

Something like:

if ([app activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]) {

    while (![app isActive]) {
        app = [NSRunningApplication
                   runningApplicationWithProcessIdentifier: pid];
        [NSThread sleepForTimeInterval:0.05];
    }
}

// send key press events
Pavan Manjunath
  • 27,404
  • 12
  • 99
  • 125
Wain
  • 118,658
  • 15
  • 128
  • 151
  • Good idea. But there's a problem. `app` never comes to know that its active and the while loop never ends. To overcome this I tried getting `app` handle everytime inside the loop. But this, somehow, is significantly slowing my application from coming to the foreground. I am not sure why changes to my code prevents the OS from bringing my appln to the foreground – Pavan Manjunath Mar 01 '15 at 21:08
  • Try extending the time from 0.05 to 0.1, not that your process hogging a little CPU time should be a massive issue on a modern OS – Wain Mar 01 '15 at 21:11
  • I made it work with 0.05 itself, but as I said before, I need to get a new app handle everytime inside the loop. ( The delay I mentioned in prev comment is gone, somehow!) I shall edit your answer to include this fact and accept your answer. – Pavan Manjunath Mar 01 '15 at 21:15
  • You should really get the new `app` instance after the delay, though with a delay of 0.05 it won't make a perceptible difference. – Wain Mar 01 '15 at 21:17
  • Thanks. Even after taking app after delay, I see that my process takes a hell a lot of time ( almost 3 sec ) to come to FG first time. After that, for subsequent key presses, even if I put it to BG forcibly, it comes back quite quickly. Not sure whats happening the first time **EDIT**: The delay is not the first time only. Its intermittent. :( – Pavan Manjunath Mar 01 '15 at 21:22
  • And you didn't previously see that delay? And if you add a `sleepForTimeInterval:1` before checking `isActive`? – Wain Mar 01 '15 at 21:27
  • 1s works perfectly fine. ( But its unacceptable :) ) I saw the delay previously too. It was happening only at the first time. Upon further probing I eventually found that it was happening randomly – Pavan Manjunath Mar 01 '15 at 21:30
  • Interesting. Guesswork: It's possible that creating the new `app` is querying state which sets up a lock on some global state and thus slows activation, but that seems weird. Other API options for finding the current front app are based on the same underpinnings. You could run an `NSTask` to query the front application, but that's a lot more code... – Wain Mar 01 '15 at 21:36
  • 1
    It is clearly documented that `NSRunningApplication` will not update its properties unless you allow the run loop to run. Do not sleep and poll. If you're not going to use `CGEventPostToPSN()`, you should keep a strong reference to the `NSRunningApplication` instance, set up KVO observation of the `active` property, and then return to the main event loop. When the KVO change notification arrives, check the `active` property and, if it's true, post your events, clear the KVO observation, and clear your strong reference. – Ken Thomases Mar 01 '15 at 21:50
  • And implementation of the (second) approach detailed by @KenThomases above can be found at: https://stackoverflow.com/questions/57006309/inject-keyboard-event-into-nsrunningapplication-immediately-after-foregrounding – P i Jul 13 '19 at 21:35
1

Instead of using CGEventPost(), use CGEventPostToPSN(). That delivers the events to the specific process. You can use code this like to get the process serial number:

ProcessSerialNumber psn;
GetProcessForPID(app.processIdentifier, &psn);
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • I am totally new to Obj-C. But I think GetProcessForPID is a Carbon API and deprecated, right? Do you know any equivalent for Cocoa? – Pavan Manjunath Mar 01 '15 at 21:59
  • It's not exactly Carbon, but it is old and deprecated. However, deprecated doesn't mean "never use". It means "only use it when it's the only way to achieve what you need, and be prepared for it to eventually go away". Use it for this, anyway, because there's non-deprecated API which requires a PSN and no modern way to get the PSN. Also, file a bug with Apple requesting either a way to post events to a PID or a modern way to get the PSN. – Ken Thomases Mar 01 '15 at 22:34
  • I tested this approach and it fails. – P i Jul 13 '19 at 21:34
1

I have recently asked a similar question at: Inject keyboard event into NSRunningApplication immediately after foregrounding it

Please examine the answer by TheNextman.

It is an implementation of one of two approaches outlined by Ken Thomases.

I have just tested it and it works.

I tested the other approach outlined by Ken (in his answer), and it did not work. I imagine this may be because of the deprecated GetProcessForPID call.

P i
  • 29,020
  • 36
  • 159
  • 267