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>
<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
$PATH
defined - no user environment variables defined
- usually
Label
contains inversed FQDN, akaua.org.mac-blog.sample
, and file name is namedua.org.mac-blog.sample.plist
- carefully read
man launchd.plist
to 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.plist
Once file is in place you can enable/disable it like so:
launchctl load ~/Library/LaunchAgents/sample.plist
launchctl unload ~/Library/LaunchAgents/sample.plist
aka 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.plist
To list loaded agents use:
launchctl list
launchctl list ua.org.mac-blog.sample
This 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
;;
esac
There is an GUI application - LaunchControl
And as you can guess it is quite easy to create some small web ui to manage the thing