Using launchd with AppleScript to Access a Flash Drive Automatically

This discussion assumes a very basic familiarity with launchd. I found launchd in Depth’s coverage to be most helpful, in concert with the launchd section in Matisse Enzer’s book: Unix for Mac OS X 10.4 Tiger.

Updating an iPod is as simple as hooking it up; the machine recognizes it is there, synchronizes the contents with the iTunes library, and it is all over. Wouldn’t it be nice to be able to do that with your JumpDrive Let’s say that you have a file that needs to be processed every time you plug your little flash drive into the machine. Even if you have written an AppleScript that will do the job, you would still need to run the script by hand every time you plug in the drive.

Not anymore. Although launchd, Apple’s fledgling UNIX utility for triggering actions automatically, cannot just sit there and watch for a file on a flash drive, it is only necessary to put together a single .plist file and an AppleScript to do all the work for you… Automatically.

For this tutorial article, we will consider 5 items:
*1) The name of the external drive (let’s call it CODEBOY)
*2) The name of the file on CODEBOY that needs to be processed (membership.csv)
*3) The name of the AppleScript already present on your machine that processes membership.csv (MacScript.scpt)
*4) The name of the AppleScript that checks for the presence of CODEBOY (AutoFlash.scpt)
*5) The launchd agent that continuously runs AutoFlash.scpt (WatchCB.plist)
I know it looks a little complicated, but if you hang on for a bit, you will understand before it is over, and then you can go off and set your machine up to do all sorts of things just by plugging in your flash drive.

Here is the outline of what is happening, then we will get into the details (Assume for this outline that CODEBOY is NOT plugged into your machine):

  1. launchd is running WatchCB.plist in the background * It is waiting for a change to /Volumes/ *- AutoFlash.scpt then checks to see if CODEBOY with the file membership.csv exists.

  2. You plug in CODEBOY *- AutoFlash.scpt now sees that membership.csv on CODEBOY is present * AutoFlash.scpt executes MacScript.scpt (thereby processing membership.csv) * AutoFlash.scpt sets a property to indicate that CODEBOY is present * MacScript.scpt will not run again while CODEBOY remains plugged in

  3. You eject CODEBOY *- AutoFlash.scpt now sees that membership.csv on CODEBOY is no longer present * AutoFlash.scpt resets a property to indicate that CODEBOY is no longer present * Next time CODEBOY is plugged in, MacScript.scpt will run once more.

I did this entire project because I am lazy. I like to keep my Palm Pilot synchronized with a database of members of my church (name, address, telephone, etc.). This list comes from a Windows-based machine, and is in .csv format. It changes every few weeks, so I wrote a nifty AppleScript to do all the work synchronizing the .csv file with my Palm Desktop. I eventually tired of the intensely physical challenge of dragging and dropping the file from my flash drive to the droplet on my desktop every time I wanted to run the update script. In addition, I had been wanting to try to learn launchd, so this was a perfect opportunity to work on both issues.

I will state up front that I am not any kind of AppleScript or UNIX professional; I am an amateur, and am willing to accept any and all comments on my work here, especially when it improves the process, or clarifies the functionality. If you know something that I don’t, and can enhance this system, please say so, with boldness.

I was pretty excited when I first read about launchd, since it has the capacity to be told to watch a file for changes, and then do whatever needs to be done. Unfortunately, it cannot watch a file that is not present all the time to, ummm, watch. If you tell it to keep an eye on a file on your flash drive, and subsequently unplug the drive, forget about it; your launchd agent is history. Thanks to some gentle suggestions by Gary Kerbaugh, I learned that the WatchPaths function of launchd does not necessarily need to watch a file; it can simply watch a path for changes. Every time you plug in a flash drive or external hard drive, or mount a CD or DVD, you are changing your system’s root Volumes path (/Volumes), which will call whatever script that agent is directed to call.

Okay, I thought, but perhaps I can just whip up an AppleScript to watch for the flash drive, and call it from the launch agent. Well, that almost works. It turns out that a launchd agent can really only call shell scripts, but since every Mac comes with the osascript shell script built in, I just used that to call the AutoFlash.scpt regularly.

Here is the launchd agent .plist file that I came up with. It is embedded as text in an AppleScript for easy copying to a text editor (it will not compile in your Script Editor).


"<xml version="1.0" encoding="UTF-8">
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http:// 
[url=http://www.apple.com/DTDs/PropertyList-1.0.dtd"]]www.apple.com/DTDs/PropertyList-1.0.dtd"][/url]
<plist version="1.0"]
<dict>
    <key>Disabled</key>
    <true/>
    <key>Label</key>
    <string>WatchingVolumesPath</string>
    <key>LowPriorityIO</key>
    <true/>
    <key>Program</key>
    <string>/usr/bin/osascript</string>
    <key>ProgramArguments</key>
    <array>
        <string>osascript</string>
        <string>/Users/casdvm/Desktop/AutoFlash.scpt</string>
    </array>
    <key>ServiceDescription</key>
    <string>Runs Applescript directly when Volumes Path changes</string>
    <key>StandardOutPath</key>
    <string>/Users/casdvm/Desktop/AutoFlashLog.txt</string>
    <key>WatchPaths</key>
    <array>
    <string>/Volumes</string>
    </array>
</dict>
</plist>"

***These .plist files can be edited in any text editor. I use TextEdit (I am cheap, as well as lazy) and simply copy one of the .plist files located in Macintosh HD:System:Library:LaunchDaemons and alter it to fit my needs. (All of those launchd .plist files start with com.apple.xxxxx.) You can certainly copy this one for your own use, or find another example out there in cyberspace somewhere. For more specific details on these type of XML files, see the web site posted above, or go to your Terminal and look up the man page for launchd.plist. The command is (man launchd.plist). You can also use the Property List Editor, which is located in the Developer:Applications:Utilities folder, assuming you have already installed your Developer Tools. (The Developer Tools are located on your installation disk that came with your Mac, or you can download them free from Apple.) But wait, there’s more!! You can also download a couple of utilities to make these .plist files for you, one is called Lingon and the other is Launchd Editor***

The vital parts of my .plist file example above are in just two keys: Program and ProgramArguments, since they tell the agent what shell script to run, and which arguments to pass onto our shell script. The Program key says that we will be accessing the osascript (please note that the full path to the script must be provided here), and the ProgramArguments key is used to ‘build’ the command line to run our AppleScript, again, with the full path to AutoFlash.scpt provided. I should point out that the two-line format shown in the plist where the strings “osascript” and the string “/Users/casdvm/Desktop/AutoFlash.scpt” appear one above the other separately is required.

You cannot just put spaces as you would in the Terminal (even using single quotes) to produce a single string like: “osascript /Users/casdvm/Desktop/AutoFlash.scpt” with a space in it. You must put each command, option, or argument on a separate “string” line, as shown in the example.

Remember that whenever the /Volumes folder changes, this launch agent will fire, running the AutoFlash.scpt AppleScript, regardless of whether or not you are attaching CODEBOY, or popping in a DVD. Therefore, the primary duty of AutoFlash.scpt is to determine if the change that called it is the change that we want. In essence, launchd asks the question, “Is membership.csv now present on CODEBOY”

This is the AutoFlash AppleScript that I developed:


property flashState : 2
set triggerFile to "CODEBOY:membership.csv"
set script2Run to (path to desktop as Unicode text) & "MacScript.scpt"
tell application "Finder"
	if file triggerFile exists then --The file is found, therefore, the flash drive is present
		if flashState = 2 then --This indicates that MacScript.scpt needs to be called
			run script file script2Run
			set flashState to 0 --After the script has been run, change the flashState property
		end if
	else --This is ONLY accessed when the file no longer exists, therefore, the flash drive is NOT present
		if flashState = 0 then --Since the drive is no longer present, the value of flashState must be re-set to 2 in order to prepare for the next hookup
			set flashState to 2
		end if
	end if
end tell

This is the script that our launchd agent calls every time a change is noted in the /Volumes folder. If CODEBOY:membership.csv exists, it next looks at the property flashState to see if the file still needs to be processed by MacScript.scpt. The combination of the file being present and flashState being equal to 2 is the trigger to run MacScript.scpt and process membership.csv. Once the script has run, the flashState property is changed to 0. That way, you can leave CODEBOY hooked up as long as you want without fear that MacScript.scpt will continue to be called over and over again, while you are mounting and unmounting CDs or other external drives. Once CODEBOY has been removed, the script then changes the flashState property back to 2, and everything is set up for the next time it is plugged in.

When you have both your .plist and AppleScript files ready to go, you must put them both in their final resting places so you can get the paths correct for them. The .plist file needs to go in a certain folder so it can be automatically loaded at computer startup. (You can name this file anything that you want. Even the .plist file extension is supposedly optional, but I prefer not to tempt fate, so I saved mine as WatchCB.plist.) The folder in which to save the .plist file must be titled LaunchAgents and must be located in your user’s Library folder (yourHD:Users:YOURUSERNAME:Library:). This folder is not normally installed with the system, so you will need to create it. (There is a script to do it for you at the end of the article as well.) Once the .plist file is in its folder, it needs to be loaded (from the Terminal) so that it will run forever until you unload it. (See the man page for launchctl {man launchctl} for details on loading and unloading launchd agents.)

I have included the Disabled key in my example .plist above, and set it to true. When you load the agent from the Terminal, you need to use the syntax, launchctl load -w FULLPATHTOTHE.PLISTFILE to set the Disabled key to false so that it will run. As long as the .plist file is in your user’s LaunchAgents folder, and the Disabled key is false, it will run every time you start up your machine. The Disabled key is NOT required, so you can delete it if you wish, and the agent will still load every time you power up the machine.

After the agent is loaded you are ready to plug in your flash drive and watch things happen. You can see from my examples that I have both of my AppleScripts on my desktop (AutoFlash.scpt pointed to by the .plist, and MacScript.scpt which is run by the AutoFlash.scpt). That is going to change soon, but it was easy for testing purposes to keep everything visible. Just remember to get all your paths corrected in the AppleScript(s) and the .plist file BEFORE you load the agent.

Another interesting thing to note is the StandardOutPath key in the .plist file. Whenever the property flashState in AutoFlash.scpt is changed (either from 2 to 0, or from 0 to 2), the new digit is printed out on the command line in the Terminal, if it is running. It is not terribly bothersome, but it seems unclean. By identifying a different output path (in this case, a .txt file on my Desktop entitled AutoFlashLog.txt), all those digits become part of that file, instead of littering up the command line. My experiments indicate that this output is appended to the file log, instead of erasing and re-writing it. Once again, the Desktop is probably not the best final resting place, but it was convenient for testing purposes.

I have tried placing the file in the /tmp and /var/tmp folders, but although the agent actually writes the AutoFlashLog.txt file in those locations, it never calls the AutoFlash.scpt. I suppose it is probably a UNIX permissions issue, so I will most likely just tuck the file somewhere within my Documents folder.

Remember that you cannot use this method to access an AppleScript which requires any kind of user input, or a user interface of any kind. Your scripts must be ‘silent’ in that they operate without any fanfare. It is a function of the whole background daemon or agent system that we are dealing with here.

Just in case you are interested in how to access a file on a flash drive within the MacScript.scpt AppleScript itself, here is the code that I use:

tell application "Finder" to set raw_CSV to file "Macintosh HD:Volumes:CODEBOY:membership.csv" 
set rf to open for access (raw_CSV as alias) --This 'as alias' is the trick to getting this to work when setting the variable to a file.
set the_Data_raw to read rf using delimiter return 
-- This reads the file as a list of it's paragraphs. 
-- To refer to paragraphs of the document in your script, use item of the list.
close access rf

I hope this helps someone out there who is as lazy as I am and looking for more ways to automate boring, regular tasks. As I stated earlier, if you know of any way to improve on this, please don’t be shy about saying so; I would love to learn more details on how all this works and the documentation available for launchd and launchctl is a bit sparse.

Addenda

Given the amount of work involved in working this out and writing it up for this article, “lazy” doesn’t quite tally with my description of Craig Smith, the author. – Adam Bell, Editor of MacScripter Tutorials

--Scriptlet to create a LaunchAgents folder in the User's Library -- ACB
tell application "Finder"
	set user_Library_Folder to path to library folder from user domain
	if not (exists (folder user_Library_Folder as text) & "LaunchAgents") then
		make new folder at user_Library_Folder with properties {name:"LaunchAgents"}
	end if
end tell

Hi Craig,

1st of all awesome post!!!

I’m having a little issue with implementing a plist file.

I have created a plist file:


"<xml version="1.0" encoding="UTF-8">¨<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http:// ¨[url=http://www.apple.com/DTDs/PropertyList-1.0.dtd"]]www.apple.com/DTDs/PropertyList-1.0.dtd"][/url]¨<plist version="1.0"]¨<dict>
¨<key>Disabled</key>
¨<True/>
<key>Label</key>
¨<string>WatchingVolumesPath</string>
¨<key>LowPriorityIO</key>
¨<true/>
¨<key>Program</key>
¨<string>/usr/bin/osascript</string>
¨<key>ProgramArguments</key>
¨<array>
¨<string>osascript</string>
¨<string>/Users/jbird/Desktop/DriveEraser.scpt</string>
¨</array>¨
<key>ServiceDescription</key>
¨<string>Runs Applescript directly when Volumes Path changes</string>
¨<key>WatchPaths</key>
¨<array>¨
<string>/Volumes</string>¨
</array>
¨</dict
>¨</plist>"

I am trying to implement it with:

launchctl load /Users/test/desktop/Edit.plist - Via Terminal

But it will not go

An ideas?

Hi,

please read Craig’s article carefully

Hi Stefan,

Still no luck:

Terminal Response:

the label key in the plist file must match the file name, in your case

/Users/jbird/Library/LaunchAgents/WatchingVolumesPath.plist

Hi Stefan,

Still not getting it to work, starting to fell very dumb :frowning:

Ive taken some of what im doing here to implement the plist :

http://www.codepoetry.net/products/launchdeditor

“¨<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http:// ¨[url=http://www.apple.com/DTDs/PropertyList-1.0.dtd"]]www.apple.com/DTDs/PropertyList-1.0.dtd"][/url]¨<plist version="1.0"]¨
¨Disabled
¨
¨Label
¨Hello *** - Changed this to be name of script
¨LowPriorityIO¨
¨
Program
¨/usr/bin/osascript ** I dont have this folder setup could this be the issue?
¨ProgramArguments
¨
¨osascript
¨/Users/Desktop/hello.scpt ** - Changed this to launch my scpt
¨
¨ServiceDescription
¨Runs Applescript directly when Volumes Path changes¨
WatchPaths
¨
¨/Volumes
¨¨

¨”

/Users/Desktop is no valid path, you mean probably /Users/jbird/Desktop

Hi Stefan,

Updated that now,

stil when i run : jbird$ launchctl load ~/Library/LaunchAgents/Hello.plist

it is not found

the Disabled key must be false otherwise the agent is apparently already loaded

Hi Stefan,

Rather than push the loading via terminal ive pushed it via Do Shell Script:

do shell script "launchctl load -w '/Users/jbird/Library/LaunchAgents/Test.plist'"

and i have placed my apple script in :

/Users/jbird/Library/Scripts/launchd/Test Script.scpt’"

This all seemed to go through fine, but when i plug my USB stick in, it still does not launch …

I have even tried speciifying the volume with no luck:

<key>ServiceDescription</key>
<string>AppleScript directly when Volumes changes</string>
<key>WatchPaths</key>
<array>
	<string>/Volumes/JAYUSB</string>
</array>

I’m a bit confused now because you have changed the name of the files a couple of times.
The name of the plist file must match the key label (without the file extension) and the path to the script must be the full path, without tilde abbreviation and without quoted or escaped characters

Just try it exactly with Craig’s example, call the plist file WatchingVolumesPath.plist and the script AutoFlash.scpt.
The only thing you have to adjust is the short user name in both path lines.
Note that the plist file must be saved as plain text UTF-8 encoded

Hi Stefan,

Totally all my fault somewhere down the line, thanks for your help and support.

I have 2 Questions:

Can it only be run from the desktop - as i have tried every known way to man to get it to work from my documents folder and it will not work!

Here is my Work

iDriveLauncher.plist

iDrive Kicker

iDriveAutoLaunch.scpt

i am now calling an App i made, when its called it does not show at the front of the screen, i have to click in the doc to get it to show up (Defeats the purpose of why i am doing this)

an ideas on how i can get the focus to the front of the screen rather than have to click in doc?



set launchiDrive to (path to home folder as text) & "Documents:Mac Apps:Created Mac Apps:Programs:iDrive.app"

tell application launchiDrive to activate



unlike the property list file, the script file can be stored everywhere.

applications created with AppleScript have an generic bundle identifier.
Try to change the bundle identifier and write

activate application file id [bundle identifier]

or retrieve its process in System Events and set the frontmost property to true

Cheers Stefan,

Will play with that when i get home tonight.

I still don’t get why it works on the desktop but not when i put it into a folder in documents?

Thank you for the great tutorial. I have two questions:

man launchd.plist says this about the Program key:

I have successfully tested a plist that excludes the Program key and uses this for ProgramArguments:

<key>ProgramArguments</key>
	<array>
		<string>osascript</string>
		<string>/Users/John/Library/Scripts/beep.scpt</string>
	</array>

Is there a reason to include the program key in the example above?

Secondly, will someone elaborate on the use of the Disabled key ? I can load and unload the script with the -w flag. However, when I open the plist file after the it has been loaded successfully, the key has not changed from false to true. The launchctl manual says this about -w :

Why and how do you properly use the Disabled key ?

No, both forms work

the behavior of the disabled key changed in Snow Leopard.
I don’t use the disabled key at all. If you want to disable the job, unload it

For a simpler approach, you can do the same thing by assigning folder actions to the /Volumes folder.