Script Trigger in Mac OS X – podcast
Hello and welcome to this technical podcast from Amsys. My name is David Acland and I’ll be taking you through triggering scripts in Mac OS X. One of my roles here is to develop technical solutions for our customers. As many of you will be aware, Mac OS X has a rich set of UNIX tools under the hood that can be leveraged to make the end users life a little easier.
Although scripting is an art form in itself, when you start writing scripts for OS X, working out how to get the script triggered at the right time and by the right user account can be a challenge.
So today I will be covering some of the options for triggering scripts (and other actions) in Mac OS X.
During this podcast, we will take a look at running scripts at scheduled intervals by using CRON & LaunchD. We’ll take a look at how you can let your users initiate the scripts, using AppleScript applications and payload free package installers. We’ll also cover event based triggers such as login, logout or sleep, and finally, how you can leverage third party programs such as Casper or Munki to provide a trigger mechanism.
Now before we get started, it would probably be a good idea to mention a few prerequisites. This podcast assumes that you have a reasonable level of understanding around scripting and Mac OS X.
If you are unsure, I would recommend starting by looking at Darren Wallace’s blog post “Bash Scripting – Getting Started.
Step one – get your script working
The first step in this process is to get a functional script. As you can imagine, if your script doesn’t work, the trigger isn’t going to help. That being said, there are a few cases where your script runs fine manually in the Terminal, but using an automated trigger it fails. There are a few reasons why this can happen…
First, consider which users will be running it. Depending on how you trigger the script will have an effect on which user it will run as. Root obviously has full access to the file system but a script triggered by an Active Directory user will only have write access to their home area so depending on what you want your script to do, you may need to take this into account.
Secondly, you will need to ensure the environment is suitable for the script. For example, if a script that is reading data from a network directory service is running at start-up, you will need to make sure the commands you have included will still function correctly when the machine is not fully booted.
And finally, make sure you test your script heavily before worrying about the trigger. If your script has syntax errors or other general mistakes preventing it from running, it will make troubleshooting much more complicated.
My test script
I’m starting off with a short script that creates a receipt file named by the current date & time. A very simple script is always a good place to start so you can make sure your trigger is working. Once the script runs as you intended, you can add more content, or replace it with another script.
I’m using these commands because they are simple, will run under almost any user account, and with the receipt file I will easily be able to tell whether the script has run or not.
If you are following along during the podcast you can of course have as long or as short a script as you would like but I would recommend starting with something fairly basic.
Decide when you want your script to run
Next you need to decide how or when you want your script to run. We are going to take a look through a few user initiated options, including AppleScript applications, package installers, Platypus apps and automator.
and then we’ll look at the automated options like running a script on a timed interval, using an event such as login or wake, or third party triggers from Casper or Munki.
User Initiated Scripts
I’ve come across lots of scenarios where fully automating the running of a script isn’t appropriate. A good example is the mounting of network drives. For desktop machines, you can use an automated trigger like login, but if they are laptop users who are in and out of the office, this would require them to login while they are on the LAN to get their drives. It would be much better if you can provide them with a dock icon that can mount the drives when it is convenient for them.
AppleScript is a very neat way to let your users run scripts. You can use it to build self-contained applications, with the script buried inside.
The main command to use is “do shell script”. You follow the “do shell script” command with the path to the script you want to run. You can add “with administrator privileges” if the commands in your script require admin access.
The only catch with this method, is that we are using an absolute path to the actual script. If you have dropped your script file into the AppleScript app bundle, moving the app will break the path and your script won’t run.
A way around this is to use the “path to resource” command. In this case, the script file is in a location relative to the root of the AppleScript app, allowing you to move the app around without breaking the trigger.
Once you have your AppleScript running correctly, just save it as an application and distribute to your user’s. And by distribute to your users I mean, distribute to a small handful of users and make sure there are no unexpected bugs (but I’m sure you all knew that).
A second, manual script trigger option is to use a package installer file. Package installers have the ability to run scripts before or after they have delivered their payload. If you only want to run a script, you create a “payload free” package, which is simply a package that does not have any files to deploy.
I normally create these packages and provide them to junior IT staff to simplify routine tasks. An example would be binding to a directory service. There’s a large selection of options when you bind a Mac to a directory. Asking a junior technician to bind 500 machines to Active Directory is likely to result in a selection of machines that are configured just slightly differently. So when I ask a junior tech to bind these 500 machines to Active Directory, I give them an installer package that configures the options exactly as I need them.
When it comes to creating the package installer, I normally use Iceberg. This is a free tool that I find particularly effective at this task. You can of course use another product, such as Composer which will do the job. As far as Package Maker goes, I would avoid it unless you like making your life difficult.
Another tool you can use is Platypus. This is very similar to Applescript in its behaviour, but instead of the script remaining visible inside the AppleScript bundle, Platypus creates a cocoa app that has the script embedded in the code. This is a great option if you would like to protect the contents of your script.
The last option I wanted to mention is Automator. This is an extremely easy to use program that allows you to build a workflow by drag and drop. Of course if you just dropped in a shell script and saved it as an application it would be no different to the previous two methods. What is unique here is the ability to save the workflow as either a service or a folder action.
A service will allow the user to run a script from a contextual menu and a folder action will run when a specified folder has items added to it.
For the next section of the podcast, I want to switch focus to automated scripts.
The first method of script automation is to use a timed schedule. The two key tools you would use to set a scheduled script are the UNIX scheduler, CRON, or the more recent LaunchD.
Now some of you may be wondering why I am including CRON in this podcast, considering that it has been superceeded by LaunchD. There is one very good reason for choosing CRON over LaunchD and that is flexibility. As you will see in a moment, LaunchD has fairly basic scheduling capabilities. CRON has been built with a load of advanced scheduling features which we will take a quick look at.
The basic syntax for cron is simple. You specify the minute, hour, day of the month, month and day of the week in a row, followed by the script that you want to run.
Getting more advanced, you can start to see the advantages in using Cron to do your scheduling. For example, the ability to specify multiple times or a time range for execution. With a little creativity, you can set a very specific schedule with this tool.
The second scheduling tool is LaunchD. Its primary purpose is to start and stop system and application daemons and has been used by Apple since 10.4, replacing the more traditional UNIX startup items and rc scripts. You can use LaunchD to run a script at startup, login, logout or when specific file system items are modified. Of course, as you have just heard, LaunchD does not favor well against CRON when it comes to setting time schedules, but its main strength is the other triggers that are included.
For more information about LaunchD, take a look at the LaunchD Podcast by Richard Mallion.
LaunchD uses XML plist files as the basic trigger. These hostile looking files hold information regarding the item to run and what event should trigger it.
As far as time scheduling goes, there is not a great deal you can do, apart from setting an every x mins or at a specific time or date. If I am using LaunchD as my trigger but want greater control over exactly when it will run, I will build the login into the script itself.
Once you get used to the syntax theyre not too bad, but if you would like a little help creating the LaunchD files, you can use Lingon. Lingon is basically a GUI tool that can create the XML file for you.
To ensure the LaunchD will trigger at the correct time, you need to place it in a specific location. The top two paths in this table are the System LaunchD items. Unless you really know what you are doing, I wouldn’t add your custom LaunchD files here.
The next path, /Library/LaunchDaemons, is generally used to run things on startup, before any user is logged in.
The path beneath it, /Library/LaunchAgents, can be used to trigger when any user of the Mac logs in.
The final path is the LaunchAgents folder in the user’s home directory. As you can imagine, this will only run if that particular user logs in.
So the XML files are essentially the same, but the location is important as it determines when it will try and load the daemon.
Another option for triggering a script is to use the built-in LoginWindow Hooks. This is quite a basic feature that allows you to run a script either at login or at logout. It uses the loginwindow preference file so you can use either MCX or the defalts write command to set the necessary key(s). Example syntax is below, you just need to replace the path with the actual path to your script.
I will point out at this stage that the keys can only hold a single value. This is usually not a problem, unless you use a third party program that wants to use the LoginWindow Hooks as its trigger. You may find that after installing the program, a previously set login hook stops running.
One other thing to point out here. Login & Logout Hooks run as root. Depending on the user that you need to run the script, this may have a bearing. The second point is the timing of the LoginHook. I have attempted to mount a network share with a script configured to use Kerberos authentication and this method failed. Essentially, it was trying to mount the share BEFORE the user had obtained a Kerberos ticket.
This is just a passing mention. We stumbled across this binary a few years ago and have found it to be very useful. It has the ability to trigger a script when a machine is put to sleep or woken from sleep. If you have a script that doesn’t like being interrupted by inpatient users, this will help you build in some resilience.
The final triggers I wanted to mention are Casper and Munki. There are obviously lots of others but these are the ones I use most often. If you have a Casper Suite solution in your organisation, you can very easily add a script to any of the Casper triggers that are available. Munki requires a bit more work as it is a command-line tool but then it is free!
I won’t say anything more about these programs as if you are using them, you are probably already familiar with a lot of the concepts in this podcast.
To wrap up, I wanted to mention a few troubleshooting tips that may help you if you get stuck.
First and foremost, make sure your script works. If you have a broken script and you try an implement one of these triggers, you won’t know where the problem lies. If you are going to be running your script when the user is logged in, make sure that it performs correctly as that user. It’s no good testing the script as an admin if the intended user does not have admin privileges.
If you are having trouble with your script, try replacing it with a basic placeholder script like the one at the start of this podcast. That way, you can at least confirm whether your trigger is functioning or not.
Secondly, make sure that your script isn’t quarantined by OS X. If you transferred it vie safari or mail, the script may have the quarantine flag set on it and may not run. To remove the flag, run xattr -d com.apple.quaranite /path/to/script in the terminal.
If you are using LaunchD, check the permissions on the xml plist file. LaunchD is very particular about the permissions on these files. As with any file in OS X, if the permissions are too restrictive, they will not run. I have seen technicians “open up” the permissions to see if this is the problem. LaunchD see’s that the permissions are “too open” and insecure and will not run launch the process.
If you are unsure if it is loading or not, try using launchctl load /path/to/xml in the terminal. If there is something wrong with the permissions, launchctl will tell you.
Next, I would recommend adding logging to your scripts. Although this is more for troubleshooting the script itself, if your script is designed to run silently with no output, it is difficult to see if it has run at all. Adding just a little basic logging can confirm if the trigger is working or not.
Finally, again looking at script troubleshooting, use each in your scripts to read back the contents of variables, and optionally add to a logfile so you can tell what is going on when the script is fully automated.
Thanks for watching this podcast and I hope you have found it useful.