Daemons on macOS with Rust
Sylvan Franklin
Posted on November 29, 2024
Overview and background
Creating a background service / daemon on macOS can be quite confusing. The native service that macOS ships with is called launchCTL
, and it is not very intuitive at all to figure out. Each service has several different states that it can be in (enabled, disabled, not enabled, bootstrapped). I was writing a Rust daemon called srhd, and I found the process nightmarish. I created a Rust crate to make the process much simpler for anyone who faces this problem in the future.
This guide
- Will walk through creating an example daemon with
launchctl
. - Walk through how macOS handles daemons.
- Bamboozle you into staring my project on github.
Example Project
Create a new cargo binary project with
cargo init --bin daemon_example
cd daemon_example
Add launchctl crate
cargo add launchctl
Open the project in your default editor, I recommend nvim. Go to the main file
and create a new Service instance, this will allow us to use a binary as a
service. I'll assume that if you're reading this tutorial you already have a
binary in mind.
fn main() {
let service = Service::new("com.<owner>.<binary>", PathBuf::from("/bin/ls"));
}
Now we can use any of the following commands on this object
fn main() {
let service = Service::new("com.<owner>.<binary>", PathBuf::from("/bin/ls"));
service.start().unwrap();
service.stop().unwrap();
service.restart().unwrap();
}
Keep in mind that launchctl will not create a plist file for you. This file can be created with another crate that some rando wrote a while ago, but I would just use a format string since a crate is overkill in my opinion. Visit the github repo for more information about the properties that these plist files can have.
fn install(ctl: &launchctl::Service) -> Result<(), Error> {
let plist = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>Label</key>
<string>{}</string>
<key>ProgramArguments</key>
<array>
<string>{}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
<key>StandardOutPath</key>
<string>/tmp/srhd_sylvanfranklin.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/srhd_sylvanfranklin.err.log</string>
<key>ProcessType</key>
<string>Interactive</string>
<key>Nice</key>
<integer>-20</integer>
</dict>
</plist>",
ctl.name,
ctl.bin_path.to_str().unwrap(),
);
Ok(fs::write(ctl.plist_path.clone(), plist)?)
}
fn main() {
let service = Service::new("com.<owner>.<binary>", PathBuf::from("/bin/ls"));
install(&service);
service.start().unwrap();
}
Run the project, and you should have a thing attached the OS. You should see a notification telling you that a new login item has been added. If you wish to remove it, simply delete the plist file and use the .stop()
command.
cargo run
I'm lowkey not gonna explain launchctl
I mean the macOS native one. It's just super confusing, and I'm tired of writing this guide. Just look at the docs, or reach out to me on github and I'll help you out. Cheers and what not.
Posted on November 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.