4

Have a small swift script called skey.swift which sending a keypress into processes even when they're not active, (e.g. they're in the background) by their PID (process ID).

import Foundation
if CommandLine.argc < 2 {
        print("Error", CommandLine.arguments[0], " No arguments are passed.")
        exit(1)
}

let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)

let key_d = CGEvent(keyboardEventSource: src, virtualKey: 0x12, keyDown: true)  // key "1" press
let key_u = CGEvent(keyboardEventSource: src, virtualKey: 0x12, keyDown: false) // key "1" release

for i in 1 ..< Int(CommandLine.argc) {
        if let pid = pid_t(CommandLine.arguments[i]) {
                print("arg:", pid)
                key_d?.postToPid( pid )
                key_u?.postToPid( pid )
        }
}

Test case:

  • Let say run the TextEdit.app and the Notes.app
  • write down their PIDs from the ps axu | grep -E 'MacOS/(TextEdit|Notes)' - example
me              87756   3,3  0,3  4716112  97228   ??  S    11:54     0:28.01 /Applications/Notes.app/Contents/MacOS/Notes
me              83077   0,0  0,1  4609916  49312   ??  S     8:00     0:04.58 /Applications/TextEdit.app/Contents/MacOS/TextEdit
  • remember the PID's: 87756 83077
  • Run from the Terminal the swift skey.swift 87756 83077
  • in the BOTH app-windows (TextEdit and Notes) appears the "1", even if they're in the background (the Terminal is in the active app).
  • so, the script works as expected !!

The problem

  • compile the script with swiftc skey.swift
  • run the compiled binary with the same arguments, e.g. ./skey 87756 83077
  • ONLY the first PID gets the "1" (in this case, only in the Notes)

Why such a difference between running a script:

  • swift skey.swift 87756 83077
  • or ./skey 87756 83077

EDIT

Just found in this source-code a comment

// Maybe this is a dumb way to solve this, but let's sleep for just a moment so they events don’t arrive out of order.

so, started experimenting with the usleep myself. 50µs won't help, but using 2x1000µs HELPED and finally the compiled and interpreted versions work exactly in the same way.

So, this works.

for i in 1 ..< Int(CommandLine.argc) {
        if let pid = pid_t(CommandLine.arguments[i]) {
                print("arg:", pid)
                key_d?.postToPid( pid )
                usleep(1000)
                key_u?.postToPid( pid )
                usleep(1000)
        }
}

But, as the comment talking about the "dumb way" - would be nice to know the CORRECT WAY because the code should work in any macOS...

EDIT 2

Based on @willeke's comment I tried the following

for i in 1 ..< Int(CommandLine.argc) {
        if let pid = pid_t(CommandLine.arguments[i]) {
                print("arg:", pid)
                key_d?.postToPid( pid )
                key_u?.postToPid( pid )
        }
}
sleep(1) // give enough time to send events before exit

and it works as expected. Looks like the event sending process must exist, otherwise, all his events are discarded.

Ashwath Shetty
  • 106
  • 1
  • 10
clt60
  • 62,119
  • 17
  • 107
  • 194
  • Cannot reproduce. I cannot observe a different behavior between running the script and the compiled program. Both insert the “1” in both applications (assuming that you allowed Terminal to control your computer in the Privacy/Accessibility settings). – Martin R Sep 17 '20 at 13:46
  • @MartinR Strange. Just tried again - only the first PID gets the keypress in case of compiled script. The accessibility setting is ok - otherwise it would block even the first (not compiled) version too. My system: Hight Sierra (10.13.6). – clt60 Sep 17 '20 at 17:34
  • @jm666 I did run it with both of your ways and worked correctly. I even tried with another way: added `#!/usr/bin/swift` as the first line of the script, added executable rights to the swift file and I run it directly like this `./skey.swift 87756 83077`. Again everything seems to work as expected. – gcharita Sep 17 '20 at 21:17
  • @gcharita using the shebang `#!/usr/bin/swift` is exactly the same as `swift skey.swift`, so no surprise. Would be nice to know your macos version. On the my 10.13.6 it is definitely NOT working... – clt60 Sep 17 '20 at 21:49
  • 1
    @jm666 mine is 10.15.6 – gcharita Sep 17 '20 at 21:50
  • @jm666 although I didn't mention that I had hard time getting it to work when I run it directly from Xcode as a command line tool. – gcharita Sep 17 '20 at 21:59
  • 1
    @gcharita Thank you for trying. Tomorrow will try the script again myself in another computer with another macos version. I using it directly from terminal - without Xcode. – clt60 Sep 18 '20 at 07:30
  • What is the question, "Difference between execution - swift script and compiled swiftc binary" or "how to correcty send keypresses"? – Willeke Sep 18 '20 at 13:18
  • 1
    @Willeke Usually the goal is having a correctly working software - in this case have an correct script which sending keypresses to processes in background defined by PIDs. However - the above script is working _somewhat_ so would be nice to know why it working as interpreted and why not as compiled - even more (see the edit) why works with `usleep` and what is the correct method. So, yes - the both questions are valid and IMHO they're undividable from each other. Honestly, I do not understand the point of your comment. – clt60 Sep 18 '20 at 22:13
  • The title "Difference between execution - swift script and compiled swiftc binary" doesn't reflect the question "how to correcty send keypresses". Please edit the title. See [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask). – Willeke Sep 19 '20 at 07:56
  • Events might be not be processed when sent too fast and/or to an inactive app. Compiled Swift runs faster than interpreted Swift. Does this answer your question? – Willeke Sep 19 '20 at 08:05
  • 1
    Weird, it works when run from Xcode but not from Terminal. I'll investigate. – Willeke Sep 19 '20 at 08:56
  • 2
    A command line tool in Xcode also doesn't work. Only the first event appears in an event tap in another app. It looks like the events aren't sent if the posting process isn't running anymore. As far as I know there isn't a better way to wait. – Willeke Sep 21 '20 at 15:12
  • 1
    @Willeke Wau, the _"It looks like the events aren't sent if the posting process isn't running anymore."_ sounds logical and reasonable and it is probably the correct answer. Just tried - enough to "sleep" **after** sending the events (just before exit) and it works as expected... – clt60 Sep 22 '20 at 23:34
  • @Willeke please add your comment as an answer - will accept it. – clt60 Sep 22 '20 at 23:40

0 Answers0