LaunchDaemons

Resources for my JNUC presentation, Intro to LaunchDaemons

launchd.info – This is the holy grail for launchd information and where I did a ton of my research for this topic. There are a lot of references to an older tool known as LaunchControl, which I do not use, but the information presented is very useful.

launchctl man page – If you want to learn more about launchctl (the tool that is mainly used for interfacing with launchd), you can’t get much better than from the man page itself.

Creating Launch Daemons and Agents – This is Apple’s documentation page on launchd. It’s a little dated in appearance and navigation, but there is a lot of good guidance here, as well as some examples of things like scheduled jobs.

Launched – The launchd plist generator shown in my slides and video demonstration. I highly recommend using a generator like this one when you are getting started with LaunchDaemon and LaunchAgent creation, since the syntax can be a little bit tricky.

launchd Package Creator – This is a really neat little utility created by jamf-nation user Ryan Ball that generates a plist for you and can also package it with a script if you choose. If you are deploying LaunchDaemons with Jamf, this is a good way to do it.

Video Breakdown

My session is available on-demand here. It will be available on YouTube in November.

1 – The Script

The script that I am using is a very simple one. You can find the contents here:

#!/bin/sh

filename=/usr/local/newFile
date=$(date +"%Y-%m-%d %T")
if [ ! -f $filename ]
	then
    	touch $filename
    	echo "$date $filename was created."
else
	echo "$date $filename already exists."
fi

(If you want to follow along, you can paste the code above into BBedit or another text editor.) This script creates a file if it doesn’t already exist. I mostly used this script for the purposes of demonstrating something that would work very quickly in the video, but this script could also be useful if you wanted to use this with an extension attribute. One use case I can think of would be using this with a LaunchDaemon that triggers on a certain date.

Quick example: I have a lab of computers that needs to be used for graphic arts, but not until the second semester. I would create a LaunchDaemon that runs this script on the date that the first semester ends. Then in Jamf, I would use an extension attribute to check for /usr/local/newFile and make a smart group for computers that have it. Then I could add that smart group to the scope of all of my graphic arts profiles and policies. This automation would save us bandwidth from installing all graphic arts programs and keeping them updated throughout the first semester when they wouldn’t be in use.

For more information on the extension attribute and smart group pieces of the process, the Jamf Pro Administrator Guide is a good resource.

2 – Preparing the Script

After creating the script, I name is createFile.sh and save it to the /usr/local/ folder. You don’t need to put the script in /usr/local/ – you can put it anywhere you want, but generally speaking, /usr/local is a good place to store scripts that you want to be accessible by all users, including the root user. macOS has many folders that are protected by SIP (System Integrity Protection) but /usr/local is not one of them. You can read more about SIP here.

Once the script is saved and in the correct location, I use this command to change the permissions on it to make it executable:

chmod +x /usr/local/createFile.sh

chmod is a command that changes the permissions of a file. You can use it to give custom read, write, and execute permissions for the owner, group, and everyone. In this case, I am only concerned with making the file executable, so I am using the +x argument. This makes it so that the file can be executed by anyone. A good in-depth description of all the things chmod can do is here (this is written for Linux, but it works the same way on macOS).

I check to make sure this worked, just to be on the safe side. You could skip this step, but I don’t recommend it. I do this first by navigating to the directory where the file is located with the cd command:

cd /usr/local/

cd stands for change directory and it is putting us into the folder where the script is located. Then I run another command to list the files and their permissions:

ls -al

ls is the list command and it gives a list of everything that is contained in the folder. The -a argument means I will get all results and the -l argument uses a long listing which gives some extra information about the permissions of the files, written in this style:

-rwxr-xr-x

Read the chmod article I listed above for in-depth information about what exactly all of these letters mean, but for now you can rest assured that the three x’s here mean that the file is executable by the owner, group, and everyone. Excellent! The script is ready!

3 – Creating the LaunchDaemon

Now for the fun part! I go to the Launched plist generator site to create the LaunchDaemon that will run this script on a schedule. I name the LaunchDaemon createFile, and fill in the command section with the full path to my script.

For purposes of demonstration, I set the start interval to 10 seconds. You probably do not want to do this in the real world, because it will run your script every ten seconds forever until the LaunchDaemon is unloaded. (If you are following along with this guide, don’t worry. I will include instructions at the end to unload it.)

I set the standard out path to /var/log/newFile.log and the standard error path to /var/log/newFile_err.log. Again, you can set these to be anything you want, but /var/log is generally where log files are kept. You can create these files beforehand or not – the LaunchDaemon will create them if they don’t exist.

After I am done with all of these settings, I generate the plist and copy the code to BBedit. There are instructions on the page to run a curl command to get the plist directly from the page and load it. DO NOT DO THIS. I can’t stress enough how bad of an idea it is to run a curl command and then load the resulting file directly into your computer and allow it to have root access to your machine. The contents of the plist I created are here:

<?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>launched.createfile</string>
	<key>ProgramArguments</key>
	<array>
		<string>sh</string>
		<string>-c</string>
		<string>/usr/local/createFile.sh</string>
	</array>
	<key>StandardErrorPath</key>
	<string>/var/log/newFile_err.log</string>
	<key>StandardOutPath</key>
	<string>/var/log/newFile.log</string>
	<key>StartInterval</key>
	<integer>10</integer>
</dict>
</plist>

Looking at the output from the generator, I see that it successfully created a plist with all of our desired keys. I save the file to my desktop with the name launched.createfile.plist and now it is ready to load!

4 – Bringing it All Together

First, I need to put the LaunchDaemon in the correct folder. Remember that there is usually only one location that is appropriate for user-created LaunchDaemons and that is /Library/LaunchDaemons. I copy the file there with this command:

sudo cp [source file] /Library/LaunchDaemons

cp is the copy command. We need to use sudo to copy to the /Library folder because it is owned by root. If you are following along, you can replace [source file] with wherever you saved the plist. Since I saved it to my desktop, the source file is /Users/rebecca.latimer/Desktop/launched.createfile.plist. Don’t forget that you can drag files into Terminal to make sure their paths are correct!

Now the LaunchDaemon is in the right location. If you want to confirm this, as I did in the video, you can do the following commands:

cd /Library/LaunchDaemons

ls -al

These commands should look familiar – they are the same ones we used to check permissions on the script earlier. Doing this, we confirm that the file was copied properly and is now owned by root.

Almost there! Even though the LaunchDaemon is in the correct location, we still have to load it. I run this command to load it:

sudo launchctl load -w /Library/LaunchDaemons/launched.createfile.plist

Again, sudo is required because we are working with a file owned by root. The launchctl command is used for interfacing with the launchd process and launchctl load adds our LaunchDaemon to launchd, making it start immediately if RunAtLoad is specified or begin working toward the interval that was set, and the -w makes sure that it loads even if it has been disabled for some reason (not that it should have been, but we include the -w to be on the safe side). Note that we are manually loading the LaunchDaemon here, but we could accomplish the same thing by just restarting the computer – launchd will load anything in the /Library/LaunchDaemons folder when it starts up.

I want to check to make sure that the LaunchDaemon is running, so I use launchctl again to list all of the LaunchDaemons that are currently running and search for the one that was just created with this command:

sudo launchctl list | grep launched.createfile.plist

grep is a command that finds files that match the criteria you list. If we ran launchctl list by itself, we would get a huge list of currently-running LaunchDaemons. Our file would be in that list, but it would be difficult to find. So we use grep to search for this specific one. It appears in the list, which means that it is running! Success!

At this point, we can be confident that the LaunchDaemon is running, but we can check very easily with this script to make sure that is true. Since it is creating a file, we can simply check to see if the file was created or not by navigating to the folder (either with cd in Terminal or using Finder) /usr/local and seeing if newFile is there. It does appear, which is great, but even if it didn’t, we can always look in our log files for more information about what went wrong. Remember that we put standard out and standard error log files in the LaunchDaemon! You can go to /var/log/ and check newFile.log and newFile_err.log. We put some echoes in our script that would normally go to standard out, so those would go to newFile.log. We can see that they are there, and the file is being written to every 10 seconds! It works! Just like magic!

Addendum – Cleaning Up

I did not include this section in the video, but if you are following along, it is a good idea to clean up what you have done by removing the LaunchDaemon that was created. You can unload the LaunchDaemon by running the unload command the same way we ran the load command earlier:

sudo launchctl unload -w /Library/LaunchDaemons/launched.createfile.plist

This makes the LaunchDaemon stop running, but you will also want to remove the file from the /Library/LaunchDaemons folder with this command:

sudo rm -rf /Library/LaunchDaemons/launched.createfile.plist

Slides:

Thank you for watching my session and being interested in LaunchDaemons! If you have any questions, found it helpful (or unhelpful), or just want to chat you can leave a comment here, contact me on MacAdmins Slack (rebelati) or send me a message on Twitter.

Leave a Comment

Your email address will not be published. Required fields are marked *