Daemons on macOS with Rust

sylvanfranklin

Sylvan Franklin

Posted on November 29, 2024

Daemons on macOS with Rust

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

  1. Will walk through creating an example daemon with launchctl.
  2. Walk through how macOS handles daemons.
  3. 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
Enter fullscreen mode Exit fullscreen mode

Add launchctl crate

cargo add launchctl
Enter fullscreen mode Exit fullscreen mode

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"));
}
Enter fullscreen mode Exit fullscreen mode

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(); 
}
Enter fullscreen mode Exit fullscreen mode

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(); 
}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
sylvanfranklin
Sylvan Franklin

Posted on November 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related