launchd.plist runs every 10 seconds instead of just once

I wrote a tutorial on this with detailed instructions and example files for triggering an arbitrary executable or shell script by the connection of an external device (usb/thunderbolt) to a Mac computer, without the respawning problem.

Like the authors approach, it relies on Apple's IOKit library for device detection and a daemon for running the desired executable. For the daemon to not be triggered repeatedly after connecting the device, a special stream handler (xpc_set_event_stream_handler) is used to "consume" the com.apple.iokit.matching event, as explained in the post by @ford and in his github repo.

In particular, the tutorial describes how to compile the xpc stream handler and how to reference it together with the executable in the daemon plist file and where to place all the relevant files with correct permissions.

For the files, please go here. For completeness, I have also pasted their content below.

Run shell script or executable triggered by device detection on a mac

Here I use the example of spoofing the MAC address of an ethernet adapter when it is connected to the Mac. This can be generalized to arbitrary executables and devices.

Put your shell script or executable into place

Adapt the shell script spoof-mac.sh

#!/bin/bash
ifconfig en12 ether 12:34:56:78:9A:BC

to your needs and make it executable:

sudo chmod 755 spoof-mac.sh

Then move it into /usr/local/bin, or some other directory:

cp spoof-mac.sh /usr/local/bin/

Building the stream handler

The stream handler xpc_set_event_stream_handler.m

//  Created by Ford Parsons on 10/23/17.
//  Copyright © 2017 Ford Parsons. All rights reserved.
//

#import <Foundation/Foundation.h>
#include <xpc/xpc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        xpc_set_event_stream_handler("com.apple.iokit.matching", NULL, ^(xpc_object_t _Nonnull object) {
            const char *event = xpc_dictionary_get_string(object, XPC_EVENT_KEY_NAME);
            NSLog(@"%s", event);
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if(argc >= 2) {
            execv(argv[1], (char **)argv+1);
        }
    }
}

is universal (no need to adapt) and can be built on a mac command line (with xcode installed):

gcc -framework Foundation -o xpc_set_event_stream_handler xpc_set_event_stream_handler.m

Let's place it into /usr/local/bin, like the main executable for the daemon.

cp xpc_set_event_stream_handler /usr/local/bin/

Setup the daemon

The plist file com.spoofmac.plist contains the properties of the daemon that will run the executable on device connect trigger.

<?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>UserName</key>
    <string>root</string>
    <key>StandardErrorPath</key>
    <string>/tmp/spoofmac.stderr</string>
    <key>StandardOutPath</key>
    <string>/tmp/spoofmac.stdout</string>
    <key>Label</key>
    <string>com.spoofmac.program</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/xpc_set_event_stream_handler</string>
        <string>/usr/local/bin/spoofmac.sh</string>
    </array>
    <key>LaunchEvents</key>
    <dict>
        <key>com.apple.iokit.matching</key>
        <dict>
            <key>com.apple.device-attach</key>
            <dict>
                <key>idVendor</key>
                <integer>32902</integer>
                <key>idProduct</key>
                <integer>5427</integer>
                <key>IOProviderClass</key>
                <string>IOPCIDevice</string>
                <key>IOMatchLaunchStream</key>
                <true/>
                <key>IOMatchStream</key>
                <true/>
            </dict>
        </dict>
    </dict>
</dict>
</plist>

It contains information for identifying the device you want to base your trigger on, like idVendor, idProduct, IOProviderClass. These can be figured out in the System Information App on your mac.

Screenshot System Information

Convert the hex identifiers to integers before inserting into the plist file (for example using int(0x8086) in python).

IOProviderClass should be either IOPCIDevice (Thunderbolt) or IOUSBDevice (USB).

The other relevant entry in the plist file is the location of xpc_set_event_stream_handler and the executable.

Other entries include the location of standard output (log) files and the executing user.

Since MAC spoofing requires root privileges, we put com.spoofmac.plist into /Library/LaunchDaemons:

cp com.spoofmac.plist /Library/LaunchDaemons/

not into a LaunchAgents folder. Launch agents ignore the UserName argument.

Insure that the owner of the file is root:

sudo chown root:wheel /Library/LaunchDaemons/com.spoofmac.plist

Launch the daemon

Activate the daemon:

launchctl load /Library/LaunchDaemons/com.spoofmac.plist

and you are good to go.

Unloading is done using launchctl unload.


AIUI your application must call xpc_set_event_stream_handler to remove the event from the queue. You might also have to add <key>KeepAlive</key><false/> to the .plist, but I'm not sure about that.

Tags:

Macos

Usb

Launchd