How can I have launchd execute a script when a certain kind of usb device is connected?

I need launchd to execute a script when a certain kind of usb device is connected. Specifically, an iOS device.


is this possible? If so, what should the launchd plist look like?

Accepted Reply

Any idea on what I am doing wrong?

Failing to understand the

launchd
executable model )-:

I like to say that

launchd
is level triggered? It will keep the job running as long as the conditions expressed in the job’s property list are true. Consider the classic networking scenario:
  1. The job declares that it wants to listen on a specific port.

  2. Starting with the job stopped…

  3. Someone connects to the port.

  4. launchd
    starts the job.
  5. The job accepts the connection from the socket and runs it.

  6. The job keeps doing this until it exits for some reason (typically an idle exit or a memory pressure exit).

Imagine, however, if the job crashes before accepting the connection (this it, at the start of step 5). The connection is still pending on the socket, and thus

launchd
starts the job again. And it will keep doing this until the job handles all the connections.

This is a good thing in general.

In your case the conditions for starting the job are the presence of a particular USB peripheral. That peripheral doesn’t go away when you finish talking to it, and thus

launchd
keeps you running.

What you’re supposed to do here is as follows:

  1. When you’re launched, use

    xpc_set_event_stream_handler
    to listen for match events coming in from the system.
  2. When you get one, use the

    IOMatchLaunchServiceID
    property to find the corresponding I/O Registry entry.
  3. Run that entry.

    It seems like in your case you don’t actually want to do anything with the entry, so this step boils down to:

    • Log the entry.
    • Ignore it.

    On the other hand, if you also want to log disconnects running the entry is a little more complex.

  4. Either opt in to memory pressure exits (

    EnablePressuredExit
    ), or implement an idle exit, or just let your job run continuously.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

Which version(s) macOS does your app support?

You should look to the

LaunchEvents
>
com.apple.iokit.matching
property. You can use that tell
launchd
to start your job when a specific I/O Kit service is discovered. See the
launchd.plist
and
xpc_set_event_stream_handler
man pages for details.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I tried the following plist, but it is not working as expected.


After I bootstrap the job, I can connect a device and my test_connect.py script will be executed and connect_log.log will be appended to.


However, Launchd keeps spawning the job every 10 seconds. Even if I disconnect the device, launchd will spawn it every 10 seconds and connect_log.log will be appended to.


Any idea on what I am doing wrong?


idProduct: 4776 = 0x12a8

idVendor: 1452 = 0x05ac



<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>Label</key>

<string>org.myorg.device_connect</string>

<key>RunAtLoad</key>

<false/>

<key>KeepAlive</key>

<false/>

<key>WorkingDirectory</key>

<string>/Users/ericg/Desktop/test_connect</string>

<key>ProgramArguments</key>

<array>

<string>/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Resources/Python.app/Contents/MacOS/Python</string>

<string>/Users/ericg/Desktop/test_connect/test_connect.py</string>

</array>

<key>LaunchEvents</key>

<dict>

<key>com.apple.iokit.matching</key>

<dict>

<key>com.apple.device-attach</key>

<dict>

<key>idProduct</key>

<integer>4776</integer>

<key>idVendor</key>

<integer>1452</integer>

<key>IOProviderClass</key>

<string>IOUSBDevice</string>

<key>IOMatchLaunchStream</key>

<true/>

</dict>

</dict>

</dict>

</dict>

</plist>


The test_connect.py script looks like:


import os

import datetime


SCRIPT_PATH = os.path.dirname( __file__ )

logfile = os.path.join( SCRIPT_PATH, "connect_log.log" )

currentTime = datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")


with open( logfile, "a" ) as fp:

fp.write( currentTime + "\n" )

Any idea on what I am doing wrong?

Failing to understand the

launchd
executable model )-:

I like to say that

launchd
is level triggered? It will keep the job running as long as the conditions expressed in the job’s property list are true. Consider the classic networking scenario:
  1. The job declares that it wants to listen on a specific port.

  2. Starting with the job stopped…

  3. Someone connects to the port.

  4. launchd
    starts the job.
  5. The job accepts the connection from the socket and runs it.

  6. The job keeps doing this until it exits for some reason (typically an idle exit or a memory pressure exit).

Imagine, however, if the job crashes before accepting the connection (this it, at the start of step 5). The connection is still pending on the socket, and thus

launchd
starts the job again. And it will keep doing this until the job handles all the connections.

This is a good thing in general.

In your case the conditions for starting the job are the presence of a particular USB peripheral. That peripheral doesn’t go away when you finish talking to it, and thus

launchd
keeps you running.

What you’re supposed to do here is as follows:

  1. When you’re launched, use

    xpc_set_event_stream_handler
    to listen for match events coming in from the system.
  2. When you get one, use the

    IOMatchLaunchServiceID
    property to find the corresponding I/O Registry entry.
  3. Run that entry.

    It seems like in your case you don’t actually want to do anything with the entry, so this step boils down to:

    • Log the entry.
    • Ignore it.

    On the other hand, if you also want to log disconnects running the entry is a little more complex.

  4. Either opt in to memory pressure exits (

    EnablePressuredExit
    ), or implement an idle exit, or just let your job run continuously.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks. With that understanding, I believe I have the problem solved.


I found this handy little python package:



https://github.com/prezesp/py-iokit-matching-handler


which consumes the com.apple.iokit.matching event and ignores it.


With this package available, I can simply add


from PyIOKitMatchingHandler import IOKitMatchingHandler


# Consume an event

handler = IOKitMatchingHandler()

handler.handle()


and my script is called only once as it should be.


I don't care about detecting disconnects, only connects.

You may consider Stecker also. It is a free utility that triggers shortcuts on device attachment and detachment.