Sometimes, we need to get things started when our os is booting. In windows, we just create a service and set him to start when the session is open, or we can also move a file in a specific folder. With linux you create a daemon. How can we do the same on macOs ?
That's what we're going to explore here !
LaunchD
LaunchD is a tool installed by default on macOs. It is made to handle daemons and agents. LaunchD rely on config files placed in specific folders.
Also, it can manage cron-like task management.
Daemon or Agent ?
Before going anywhere we need to get the difference between Agents and Daemons, at least the difference made by Launchd.
It's pretty straight-forward : it depends which user is running the process.
If the process is running as the current logged user, then you will use an Agent, if it's running as root, then you will use a Daemon.
LaunchD gives you the possibility to create three types of Agents and two types of Daemon. The creation of any of these types depends on where you create your config file like shown in the table below :
Type | Location | Run on behalf of |
---|---|---|
User Agents | ~/Library/LaunchAgents | Currently logged in user |
Global Agents | /Library/LaunchAgents | Currently logged in user |
Global Daemons | /Library/LaunchDaemons | root or the user specified with the key UserName |
System Agents | /System/Library/LaunchAgents | Currently logged in user |
System Daemons | /System/Library/LaunchDaemons | root or the user specified with the key UserName |
Disclaimer : this crystal-clear array is the work of LaunchD info (mentionned in the source part)
Plist file
In order to run your process, LaunchD need a plist config file placed in a specified folder as we saw previously.
A plist file is basically a simple xml file.
The minimal template of a plist file looks 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>
</dict>
</plist>
So, here we just have the xml Schema, a plist tag with used version of plist and a dict.
Dict here is a Dictionary
type, so it works witk Key and Values.
First thing first, we need to give a name to our process, it's required by launchd and it needs to be unique as it will be used to identify our job :
<?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.mjehanno.myScript</string>
</dict>
</plist>
We can then define the program we want to run. There is two way of doing this :
You can either have a script defined in a file :
<?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.mjehanno.myScript</string>
<key>Program</key>
<string>~/Scripts/myScript.sh</string>
</dict>
</plist>
Or you can pass a array of argument which seems to be the prefered way when dealing with a node script (at least if you don't want to have to handle shebangs and many environnement variable).
<?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.mjehanno.myScript</string>
<key>ProgramArguments</key>
<array>
<string>~/.nvm/versions/node/v14.18.2/bin/node</string>
<string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
<string>arg1</string>
<string>arg2</string>
</array>
</dict>
</plist>
Speaking about environnement variables, you can pass some to your job.
Let's imagine we need something in our PATH.
We just need to add a dictionary with the right Key :
<?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.mjehanno.myScript</string>
<key>ProgramArguments</key>
<array>
<string>~/.nvm/versions/node/v14.18.2/bin/node</string>
<string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
<string>arg1</string>
<string>arg2</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string> /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
</dict>
</dict>
</plist>
And that's it ! We have a plist file, defining our Job with a unique label, the script we need to run and we even gave him some context with our environnement variable.
Nb : plist files don't really like wildcard like * so you should avoid them in path
Enable the agent
Load and Run
Before launching our Agent, we need to load our job definition file in LaunchD.
LaunchD come with an handy cli called launchctl.
So now if we want to load our job we can run the following :
launchtl bootstrap gui/502 ./com.mjehanno.myScript.plist
launchctl bootstrap
takes a domain target ( gui/502
where 502 is my UserId) and a path to our plist file.
Now we can start it with :
launchctl kickstart gui/502/com.mjehanno.myScript
If you want your job to run directly when loaded there is also an option you can pass in the plist file :
<?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.mjehanno.myScript</string>
<key>ProgramArguments</key>
<array>
<string>~/.nvm/versions/node/v14.18.2/bin/node</string>
<string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
<string>arg1</string>
<string>arg2</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string> /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
</dict>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Error Handling
We just launched our agent but we have nothing, no return, no error, we don't know if it's running correctly or not.
Launchctl gives us the possibility to list our jobs :
launchctl list
(you might want to grep on the label you defined in your plist file)
This command will just display a list of all jobs loaded with their PID (if they are running), their label and a code representing their current status. Yet we don't know what does status code means.
No problem here, launchctl at the rescue again :
launchctl error [errorCode]
will give you a human readable description of the problem.
Also, you can unload your job at any time :
launchctl bootout /gui/502/com.mjehanno.myScript
Or stop it with :
launchctl kill [sigTerm] /gui/502/com.mjehanno.myScript
Finally, in your plist file you can also redirect stdout and stderr of your job to files.
<?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.mjehanno.myScript</string>
<key>ProgramArguments</key>
<array>
<string>~/.nvm/versions/node/v14.18.2/bin/node</string>
<string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
<string>arg1</string>
<string>arg2</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string> /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/com.mjehanno.myScript.out</string>
<key>StandardErrorPath</key>
<string>/tmp/com.mjehanno.myScript.err</string>
</dict>
</plist>
Warning : In case of an Agent, your user needs to have write access to the path you provided for StandardOutPath
or StandardErrorPath
.
Tips
I bumped into a command supposed to verify the integrity of your plist file :
plutil [pathToPlistFile]
GUI
If you don't want to deal with this stuff yourself, you can use a GUI app to manage your LaunchD configuration. At the moment I'm writing this I found two available options.
Both are paid-app but you can still use some part freely (only saving configuration will not work on the free-tier).
- LaunchControl
- Lingon
[EDIT]
TUI
If you're not afraid of using a terminal, you can use launch-tui to manage your agents and this one is completly free.
[EDIT]
Top comments (0)