MacOS Launchd
Launchd in MacOS is kind of combination of systemctl and crontab
There are system and user services, we are interested in user services only
Each service configuration file lives here: ~/Library/LaunchAgents/*.plist
"plist" - stands for "properties list" and is good old XML file (yes, not YAML)
Here is an example of such file to run powershell script on schedule
<?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>sample</string>
<!-- survive restarts -->
<key>RunAtLoad</key>
<true/>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/pwsh</string>
<string>/Users/mini/Desktop/sample/sample.ps1</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/mini/Desktop/sample</string>
<key>EnvironmentVariables</key>
<dict>
<key>FOO</key>
<string>bar</string>
</dict>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Weekday</key>
<integer>1</integer> <!-- Monday -->
<key>Hour</key>
<integer>14</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Weekday</key>
<integer>5</integer> <!-- Friday -->
<key>Hour</key>
<integer>14</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<key>StandardOutPath</key>
<string>/Users/mini/Desktop/sample/sample.log</string>
<key>StandardErrorPath</key>
<string>/Users/mini/Desktop/sample/sample.log</string>
</dict>
</plist>Notes:
- no
$PATHdefined - no user environment variables defined
- usually
Labelcontains inversed FQDN, akaua.org.mac-blog.sample, and file name is namedua.org.mac-blog.sample.plist - carefully read
man launchd.plistto see what you can configure in plist, it is not so big but quite interesting and has all the answers
If you decide to store files somewhere else you can always symlink them:
ln -s ~/Desktop/sample/sample.plist ~/Library/LaunchAgents/sample.plistOnce file is in place you can enable/disable it like so:
launchctl load ~/Library/LaunchAgents/sample.plist
launchctl unload ~/Library/LaunchAgents/sample.plistaka it is the same as systemctl enable sample
Note: there are also start and stop commands, start is usefull for cronjobs, like this:
launchctl start ~/Library/LaunchAgents/sample.plistTo list loaded agents use:
launchctl list
launchctl list ua.org.mac-blog.sampleThis one may be usefull to see if agent is running or not and what was exit code.
Here is one more example for dotnet service
<?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>
<!-- Basic service information -->
<key>Label</key>
<string>cflog</string>
<key>LimitLoadToSessionType</key>
<array>
<string>Aqua</string>
<string>Background</string>
<string>LoginWindow</string>
<string>StandardIO</string>
<string>System</string>
</array>
<!-- Command to execute and its arguments -->
<key>ProgramArguments</key>
<array>
<string>/Users/mini/.dotnet/dotnet</string>
<string>run</string>
</array>
<!-- Environment variables if needed -->
<key>EnvironmentVariables</key>
<dict>
<key>ASPNETCORE_URLS</key>
<string>http://+:5000</string>
</dict>
<!-- Working directory for the web server -->
<key>WorkingDirectory</key>
<string>/Users/mini/Desktop/cflog</string>
<!-- Run configuration -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<!-- Log files -->
<key>StandardOutPath</key>
<string>/Users/mini/Desktop/cflog/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/mini/Desktop/cflog/stderr.log</string>
</dict>
</plist>Note: brew services list actually doing something similar, and copying files from /opt/homebrew/Cellar/traefik/3.3.4/homebrew.mxcl.traefik.plist to ~/Library/LaunchAgents/homebrew.mxcl.traefik.plist
For convenience here is small script to make life little bit easier:
#!/usr/bin/env bash
list() {
printf "%-8s %-8s %s\n" "PID" "STATUS" "LABEL"
for f in ~/Library/LaunchAgents/*.plist
do
label=$(plutil -convert json -o - $f | jq -r '.Label')
if [ "$label" == "null" ]
then
continue
fi
pid=$(launchctl list | grep $label | cut -f1)
# if empty set to -
if [ -z "$pid" ]
then
pid="-"
fi
status=$(launchctl list | grep $label | cut -f2)
if [ -z "$status" ]
then
status="-"
fi
printf "%-8s %-8s %s\n" $pid $status $label
done
}
enable() {
if [ -f ~/Library/LaunchAgents/$1.plist ]
then
launchctl load ~/Library/LaunchAgents/$1.plist
else
echo "File '~/Library/LaunchAgents/$1.plist' does not exist"
exit 1
fi
}
disable() {
if [ -f ~/Library/LaunchAgents/$1.plist ]
then
launchctl unload ~/Library/LaunchAgents/$1.plist
else
echo "File '~/Library/LaunchAgents/$1.plist' does not exist"
exit 1
fi
}
start() {
if [ -f ~/Library/LaunchAgents/$1.plist ]
then
launchctl start ~/Library/LaunchAgents/$1.plist
else
echo "File '~/Library/LaunchAgents/$1.plist' does not exist"
exit 1
fi
}
case $1 in
list)
list
;;
enable)
enable $2
;;
disable)
disable $2
;;
start)
start $2
;;
*)
echo "Usage:"
echo "services list"
echo "services enable nginx"
echo "services disable nginx"
exit 1
;;
esacThere is an GUI application - LaunchControl
And as you can guess it is quite easy to create some small web ui to manage the thing
To manually run cronjob you may:
launchctl kickstart -k gui/$(id -u)/mavis