Skip to content

Instantly share code, notes, and snippets.

@johndturn
Last active December 25, 2024 10:27
Show Gist options
  • Save johndturn/09a5c055e6a56ab61212204607940fa0 to your computer and use it in GitHub Desktop.
Save johndturn/09a5c055e6a56ab61212204607940fa0 to your computer and use it in GitHub Desktop.
Overview of using launchd to set up services on a macOS machine.

launchd - Script Management in macOS

What is it?

  • Used on macOS for managing agents and daemons and can be used to run scripts at specified intervals
    • macOS's competitor to cron, along with other things
  • Runs Daemons and Agents

What is a Daemon?

A daemon is a program running in the background without requiring user input.

  • Used to perform daily maintenance tasks or do something when an event occurs at the OS level, like when a device is connected.
  • Runs on behalf of the root user of the machine

What is an Agent?

  • Basically the same thing as a daemon, but runs on behalf of the logged-in user, not the root user

How to use it?

  • Important Distinction: You don't interact with launchd directly
    • Use launchctl to load or unload daemons and agents instead
  • Steps to set up an agent or daemon:
    1. Create a program that you want to run in the background
    2. Create a .plist file describing the job to run (See below for how to author one)
    3. Store it in the relevant spot based on whether or not you're creating a daemon or an agent, and why type of agent or daemon that you want to include (see screenshot below)
    4. Use launchctl to load the job and set it to run: launchctl load PATH/TO-PLIST
      • Note that you only have to run this once when you first author / install the service. Upon reboot / login all agents & daemons will be loaded and run according to the .plist running commands
  • If you want to run a job regardless of its run conditions listed in the .plist file, you can run the following: launchctl start LABEL.OF.JOB

plist_Locations

How to write a .plist file

The majority of the learning that I needed in order to run my background job came from the first link in the Further Reading section below

  • A .plist file is valid for loading into launchd with just the Label and Program or ProgramArguments attributes, but the background job won't run, since it doesn't know when to invoke it
  • It's an xml document
  • Some key attributes to include:
    • Label (required): The name of your job, should be unique and follow "reverse domain" naming convention
    • Program (required if ProgramArguments isn't present): The path to the executable on your system
    • ProgramArguments (required if Program isn't present): Array of string arguments including the path to your executable and any other arguments
      • All strings are concatenated together with spaces between them to make up the full command
    • ServiceDescription: Human-readable description of your service
    • Working Directory: Can set the working directory when your program runs
    • RunAtLoad: Should we run the job at boot time (for daemons) / login time (agents)?
    • StartCalendarInterval: Dictionary used to specify cron-like running intervals, like "run this every day at 3:00 AM"
      • Available keys:
        • Month
        • Day
        • Weekday
        • Hour
        • Minute
      • Important Note: omitted keys are interpreted as *
    • StartInterval: Used to run the background job every n seconds
    • StandardErrorPath: Useful for indicating a specific log file location for when errors arise in your service
    • EnvironmentVariables: Allows you to set different environment variables that can be accessed as part of your program
      • Common entries here include setting your PATH when running different scripts.
        • Given that your service is running outside of the context of loading a terminal / shell session, your shims and other enhancements usually found in a .bashrc, .bash_profile, or .zshrc file won't be loaded; therefore you have to load those up here or your service might not run as expected

Example .plist file

This is an example .plist file that I use to run a background job to sync my Obsidian-based Second Brain to the git-based version of the repo, which then runs some formatting and other processes.

<?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>dev.johnturner.ObsidianFoamReconciler</string>

        <key>ServiceDescription</key>
        <string>Regular sync between Obsidian Second Brain vault and my git-based Second Brain</string>

        <key>Program</key>
        <string>/Users/johnturner/second-brain/sync-from-obsidian.sh</string>

        <key>WorkingDirectory</key>
        <string>/Users/johnturner/second-brain</string>

        <key>StandardErrorPath</key>
        <string>/Users/johnturner/Desktop/reconciler-error.log</string>

        <key>EnvironmentVariables</key>
        <dict>
            <key>PATH</key>
            <string>
                /usr/local/opt/gettext/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
            </string>
        </dict>

        <key>StartCalendarInterval</key>
        <dict>
            <key>Hour</key>
            <integer>11</integer>
            <key>Minute</key>
            <integer>0</integer>
        </dict>

        <key>RunAtLoad</key>
        <false />
    </dict>
</plist>

Further Reading

@johndturn
Copy link
Author

@nonrice Great question. Most of the time these plist files will follow the reverse domain name notation naming convention. I actually don't know if it's necessary or not, but it's a good practice to follow as it will help to avoid plist naming conflicts usually.

@cmendez20
Copy link

Hi, is StartCalendarInterval a required attribute? I'm trying to run a .plist file every 2 minutes and noticed that it doesn't have that specific attribute, only the StartInterval....

@johndturn
Copy link
Author

@cmendez20 I believe that it is NOT a required attribute. If you check this documentation, and click on the "Configuration" tab at the top, you'll see at the bottom of the page where it describes the difference use cases for StartCalendarInterval vs. StartInterval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment