launchd.plist runs every 10 seconds instead of just once

℡╲_俬逩灬. 提交于 2021-02-18 11:30:10

问题


I have been setting up a launchd.plist XML that is run every time a specific USB device is mounted. I followed the instructions on the xpc_events(3) man page and it is running the application whenever the device is mounted.

The problem I'm having is that the application is run again and again every 10 seconds as long as the device is still mounted. How can I set it up so it only runs once when the device is inserted in the USB port?

<?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>com.myapp.agent</string>
    <key>Program</key>
    <string>/Applications/MyApp.app/Contents/MacOS/MyAgent</string>
    <key>LaunchEvents</key>
    <dict>
        <key>com.apple.iokit.matching</key>
        <dict>
            <key>com.apple.device-attach</key>
            <dict>
                <key>idVendor</key>
                <integer>2316</integer>
                <key>idProduct</key>
                <integer>4096</integer>
                <key>IOProviderClass</key>
                <string>IOUSBDevice</string>
                <key>IOMatchLaunchStream</key>
                <true/>
            </dict>
        </dict>
        <key>com.apple.notifyd.matching</key>
        <dict>
            <key>com.apple.interesting-notification</key>
            <dict>
                <key>Notification</key>
                <string>com.apple.interesting-notification</string>
            </dict>
        </dict>
    </dict>
</dict>
</plist>

回答1:


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.




回答2:


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.

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.




回答3:


I am trying to use something like this:

#include <xpc/xpc.h>
#include <unistd.h>
#include <asl.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        return 1;
    }

    asl_log(NULL, NULL, ASL_LEVEL_DEBUG, "event_stream_handler: starting");

    xpc_set_event_stream_handler("com.apple.iokit.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t event) {
        const char *name = xpc_dictionary_get_string(event, XPC_EVENT_KEY_NAME);
        uint64_t id = xpc_dictionary_get_uint64(event, "IOMatchLaunchServiceID");
        asl_log(NULL, NULL, ASL_LEVEL_DEBUG, "event_stream_handler: received event: %s: %llu", name, id);

        execv(argv[1], argv + 1);
    });

    dispatch_main();

    return 0;
}

So a script which consumes the event and runs the script passed as an argument.




回答4:


This works for me:

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);
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if(argc >= 2) {
            execv(argv[1], (char **)argv+1);
        }
    }
}

full source code here



来源:https://stackoverflow.com/questions/13987671/launchd-plist-runs-every-10-seconds-instead-of-just-once

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!