launchd.plist runs every 10 seconds instead of just once - macos

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>

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.

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.

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.

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

Related

MacOS Metal: Failing to capture GPU Frame from command line app

I'm trying to programmatically capture GPU frames using MTLCaptureManager in a command line application.
So far, the capture manager fails to support the MTLCaptureDestinationGPUTraceDocument destination.
I tried to create a very minimal repro case using XCode :
#import <Foundation/Foundation.h>
#import <Metal/Metal.h>
int main(int argc, const char * argv[]) {
#autoreleasepool {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
MTLCaptureManager* captureManager = [MTLCaptureManager sharedCaptureManager];
if (![captureManager supportsDestination:MTLCaptureDestinationGPUTraceDocument])
{
NSLog(#"********** captureManager does not support MTLCaptureDestinationGPUTraceDocument ************");
}
else
{
NSLog(#"captureManager support is fine");
}
}
return 0;
}
When run with XCode, it seems to be willing to work : the output is :
2020-09-02 16:25:59.712217+0200 testMetalCapture[20095:416447] Metal GPU Frame Capture Enabled
2020-09-02 16:25:59.712503+0200 testMetalCapture[20095:416447] Metal API Validation Enabled
2020-09-02 16:26:00.669092+0200 testMetalCapture[20095:416447] captureManager support is fine
Program ended with exit code: 0
But when I archive the build result, and run from a terminal, it fails :
2020-09-02 16:32:57.607 testMetalCapture[20126:419837] ********** captureManager does not support MTLCaptureDestinationGPUTraceDocument ************
Is there any runtime environment I could reproduce in terminal to get the MTLCaptureManager working ?
(Environment is XCode 11.6 + MacOS 10.15 Catalina)
From what I understood (I could not find any official documentation) :
MTLCaptureManager needs an authorization from the Info.plist: the MetalCaptureEnabled should set to YES.
The proper way is to bundle the command line application with such a plist
This is quite involved, see for example Building OSX App Bundle
I found by accident that MTLCaptureManager also works if there's an Info.plist in the same directory as the command line application.
This Info.plist can be almost empty, like this :
<?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>MetalCaptureEnabled</key>
<true/>
</dict>
</plist>
With this simple setup, I can run my test program (and my real one too)
% ./testMetalCapture
2020-10-02 15:53:08.507 testMetalCapture[28559:686864] Metal GPU Frame Capture Enabled
2020-10-02 15:53:08.523 testMetalCapture[28559:686864] captureManager support is fine
(I'm currently using MacOSX 10.15.6, it may break in the future)
The trick from #rotoglup was working for me, until it wasn't anymore: some buffers were full of zeros, and no image appeared in the 'screenshots' inside the metal debugger. I don't know if this was because of an update of the OS or of XCode or something else.
I still needed an Info.plist with this content, as #rotoglup suggested:
<?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>MetalCaptureEnabled</key>
<true/>
</dict>
</plist>
but I also needed to set the METAL_DEVICE_WRAPPER_TYPE env variable to 1 in the terminal that launches the command line app:
export METAL_DEVICE_WRAPPER_TYPE=1
With this, buffers are filled up correctly, and I get non-empty screenshots.

How to simulate keyboard and mouse events using CGEventPost in login window mac OS?

I have created a pre-login agent which uses CGEventPost for simulating keyboard. FYI I am developing a remote control app similar to teamviewer.
Keyboard
CGEventRef keyEvent = CGEventCreateKeyboardEvent( NULL, keyCode, down ) ;
CGEventPost( kCGHIDEventTap, keyEvent ) ;
CFRelease( keyEvent ) ;
Mouse
CGEventRef event = CGEventCreateMouseEvent(eventSource, eventType, mouseLocation, mouseButton );
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
Pre-login launch agent
<?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>my app label</string>
<key>LimitLoadToSessionType</key>
<string>LoginWindow</string>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>My app directory</string>
<key>ProgramArguments</key>
<array>
<string>app absolute path</string>
<string>service</string>
<string>myservice</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
CGEventPost is not working, I get the following in the Console logs after login
Untrusted apps are not allowed to connect to
Window Server before login.
I have searched o chromium's remote control (which has keyboard and mouse simulation working) source code. They use CGEventPost for keyboard, but it works in login window.
https://cs.chromium.org/chromium/src/remoting/host/input_injector_mac.cc?rcl=0&l=42
They seem to use a sh file in privileged helper tools directory and use to to load the service, I tried putting our service in privileged helpers tool, but still the event handling fails.
The Deprecated API CGPostMouseEvent, CGPostKeyBoardEvent work without problem , but would really like to know how non deprecated keyboard API works in chromium.
There is undocumented (classic apple , security through obscurity) stuff you should add to binary's sections, in order to make CGEventPost magically work when running in LoginWindow Context.
If you are using gcc, add the following to the compile flags
gcc <YOUR SOURCES AND FLAGS> -sectcreate CGPreLoginApp __CGPreLoginApp /dev/null
If you are using XCode, add the following to Other Linker flags in the Build settings of the project:
"-sectcreate"
__CGPreLoginApp
__cgpreloginapp
/dev/null
Refer https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-700/IOHIDFamily.xcodeproj/project.pbxproj project file for the LDFLAGS

iTunesTrack location is nil in sandbox mode?

I am sandboxing an osx app that uses scripting bridge to access iTunes.
for(iTunesFileTrack* track in fileTracks)
{
//url is nill in sandbox mode but good value in non sandbox mode
NSURL* url = [track location];
NSString* sourceFile = [[track location] path];
if(sourceFile == nil)
{
NSLog(#"Sourcefile for the track %# was nil", track);
continue;
}
}
nil is returned, I am using following entitlements
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.movies.read-write</key>
<true/>
<key>com.apple.security.assets.music.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.scripting-targets</key>
<dict>
<key>com.apple.iTunes</key>
<array>
<string>com.apple.iTunes.device</string>
<string>com.apple.iTunes.library.read</string>
<string>com.apple.iTunes.library.read-write</string>
<string>com.apple.iTunes.playback</string>
<string>com.apple.iTunes.podcast</string>
<string>com.apple.iTunes.user-interface</string>
</array>
</dict>
<key>com.apple.security.temporary-exception.apple-events:before:10.8</key>
<array>
<string>com.apple.itunes</string>
</array>
</dict>
</plist>
The console shows following violation
iTunes[1592]: AppleEvents/sandbox: Returning errAEPrivilegeError/-10004 and denying dispatch of event core/getd from process 'TestiTunesAccess'/0x0-0x4d04d, pid=1789, because it is not entitled to send an AppleEvent to this process.
it works fine in 10.7 and location is returned OK, but in 10.8 and 10.9 because the scripting-target entitlement is active, I can iterate the library but location of track is nil, why is that so ? if I just use temporary exception and remove the part:before10.8 then it works.
But since apple recommends we use scripting target in 10.8+ and not temporary exception entitlements, I am using the recommended ones. any help would be highly appreciated.
I fixed this by adding a temporary exception for iTunes, as shown in this answer.
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.iTunes</string>
</array>

Packaging C binary in Mac OS X Application Bundle

I'm trying to package my binary in a minimalistic app bundle. But I'm seeing some strange behavior with the result.
My bundle has this minimal structure:
$ ls -R HelloWorld.app
Contents
HelloWorld.app/Contents:
Info.plist MacOS PkgInfo
HelloWorld.app/Contents/MacOS:
helloworld
helloworld is a C binary compiled from:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv) {
while (1) {
printf("Hello world!\n");
sleep(2);
}
return 0;
}
Info.plist contains:
<?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>CFBundleExecutable</key>
<string>helloworld</string>
<key>CFBundleIdentifier</key>
<string>com.litl.helloworld</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>HelloWorld</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>20</string>
<key>LSMinimumSystemVersion</key>
<string>10.6</string>
<key>LSUIElement</key>
<true/>
<key>LSBackgroundOnly</key>
<true/>
</dict>
</plist>
Now for the strange behavior. When I run
open ./HelloWorld.app
The command hangs for about 30s. After that I can confirm that the helloworld binary is running. However its standard output does not show up in Console.app. If I launch this bundle programmatically (NSWorkspace sharedWorkspace] launchApplicationAtURL...) the call succeeds, but the binary exits immediately (I can see in the console it exited with error code 2).
This is on OS X 10.9.2.
What am I doing wrong?
You need to register with Cocoa to mark your application as responsive and 'ready'. If you would enable the dock icon, it means that it stops to bounce. In your case, if you hide the icon from the dock, you still need to register with Cocoa.
You can do that e.g. by creating a NSApplication class. See here for some low level deails.

Creating a simple Hello World tool that runs as a daemon

I built the Command Line tool (Foundation) template in Xcode. It just logs "Hello World" to the console.There is only one class in it main.m. Here's the code:
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
#autoreleasepool {
// insert code here...
NSLog(#"Hello, World!");
}
return 0;
}
Now I want to run it as a daemon and log "Hello World" to the console every 10 seconds. So I moved the product/binary to /tmp on my Mac. I created the following plist for launchd:
<?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>helloDaemon</string>
<key>ProgramArguments</key>
<array>
<string>/tmp/helloDaemon</string>
</array>
<key>StartInterval</key>
<integer>10</integer>
</dict>
</plist>
I loaded the plist using launchctl, but I do not see any "Hello World"s in the console. Instead, I get this:
11/03/2012 00:55:35.141 com.apple.launchd: (helloDaemon) Throttling respawn: Will start in 1 seconds
11/03/2012 00:55:45.141 com.apple.launchd: (helloDaemon) Throttling respawn: Will start in 2 seconds
11/03/2012 00:55:55.140 com.apple.launchd: (helloDaemon) Throttling respawn: Will start in 3 seconds
So what's going wrong?
Just add to your launchd plist
<key>StandardOutPath</key>
<string>/yourpath/sample.log</string>
You can tail -f it afterwards.
More info is here:
http://developer.apple.com/library/mac/technotes/tn2083/_index.html#//apple_ref/doc/uid/DTS10003794-CH1-SUBSECTION39
NSLog is not working because when you launch a demon process it does not have any standard io sockets or file handles attached to it. They have to be specifically allocated. A good resource for how to create a proper daemon and how to write the console and syslog is provided in the book "Advanced Mac OS X Programming (chapter 20) by Dalrymple & Hillegass.
They have define a skeleton program that addresses the io issues you highlight. I remembered reading it a while ago and thought that maybe someday I'd need it. The authors show a sample using the syslog.h lib using openlog() and syslog() for simple communications. They also show some other lower level methods for communication to files and even sockets (for servers, etc).
I always appreciate when someone can tell me how to do something rather than reference something, but in this case, that is the best I can do. good luck.
With the following keys in the plist file I get most of the logs. But sometimes I am confused if I get all the NSLog output in the log file. To me it seems sometime some logs line are missing.
I am not sure if all the NSLog output of a LaunchDaemon is logged or some are skipped due to system priority.
<key>StandardOutPath</key>
<string>/var/log/mydaemon.log</string>
<key>StandardErrorPath</key>
<string>/var/log/mydaemon.log</string>

Resources