hohle
Automate iSync with launchd
by Jon on Friday, March 2, 2007 file under: Technology
iSync

Back in July, I wrote about scripting iSync so it would automatically sync my phone every night via a cron job. The solution wasn't perfect, but it worked pretty well.

Since then I've become fond of Launchd. I've written LaunchDaemons to keep run my nightly backups and keep Darwin Ports in sync. While reading the Launchd entry in Wikipedia yesterday, I came across this section on LaunchAgents:

The LaunchAgents folders contain jobs, called agent applications, that will run as a user or in the context of userland. These may be scripts or other foreground items, and they can even include a user interface.

Let me back up a little bit. While my AppleScript + cron works "pretty well," it is by no means perfect. The biggest flaw is that if I'm not the active user, the script fails. That means I can't automatically sync Kortney's phone every night, because one of us is going to be the active user, meaning the other person can't sync. The underlying issue is that programs launched from cron can't connect to anyone's display but the active user's, and if the active user is different from the user cron is running as, the program requiring use of display dies a horrible death.

That's where LaunchAgents come in. LaunchAgents have the ability to run a task at a given time or given event, can be run as a specific user, and can run tasks which require a user interface. Bingo!

I've modified the script slightly since the last time I posted it, so here it is again. The major change is now the script uses iSync's return status as an exit code instead of just returning the value.

-- This script will tell iSync to synchronize.  if there's
-- more then one device attached, I don't know what that
-- means.
--
-- hints from
-- http://growl.info/documentation/applescript-support.php
-- http://www.macosxhints.com/article.php?story=20031201172150673
--
-- Author: Jonathan Hohle

tell application "System Events"
	set growlIsRunning to (count of (every
	  process whose name is "GrowlHelperApp")) > 0
	
	set iSyncIsRunning to (count of (every
	  process whose name is "iSync")) > 0
end tell

if growlIsRunning then
	tell application "GrowlHelperApp"
		
		-- Make a list of all the notification types 
		-- that this script will ever send:
		set the allNotificationsList to {"Result Notification"}
		
		-- Make a list of the notifications 
		-- that will be enabled by default.      
		-- Those not enabled by default can be enabled later 
		-- in the 'Applications' tab of the growl prefpane.
		set the enabledNotificationsList to {"Result Notification"}
		
		register as application "iSyncScript"
		  all notifications allNotificationsList default
		  notifications enabledNotificationsList
		  icon of application "Script Editor"
	end tell
end if

tell application "iSync"
	activate
	synchronize
	-- wait until sync status != 1 (synchronizing)
	repeat while (syncing is true)
	end repeat
	
	set syncStatus to sync status
	set lastSync to last sync
end tell

set syncStatusText to ""

-- syncStatus = 2 -> successfully completed sync
if syncStatus = 2 then
	set syncStatusText to "Successfully Synced"
	set syncStatus to 0
else
	if syncStatus = 3 then
		set syncStatusText to "Completed with Warnings"
	else if syncStatus = 4 then
		set syncStatusText to "Completed with Errors"
	else if syncStatus = 5 then
		set syncStatusText to "Last Sync Cancelled"
	else if syncStatus = 6 then
		set syncStatusText to "Last Sync Failed to Complete"
	else if syncStatus = 7 then
		set syncStatusText to "Never Synced"
	end if
	
end if


if syncStatus = 0 and not iSyncIsRunning then
	tell application "iSync" to quit
end if

set displayText to "Status: " & syncStatusText & " (" &
  syncStatus & ").  Synced on " & lastSync

if growlIsRunning then
	tell application "GrowlHelperApp"
		notify with name "Result Notification"
		  title "iSyncScript" description displayText
		  application name "iSyncScript" icon
		  of application "iSync"
	end tell
else
	display dialog "syncStatus: " & syncStatus
end if

do shell script "exit " & syncStatus

Again, I saved this to ~/Library/Scripts/AutoSync.scpt directory, and can call it from the the command line with `osascript Library/Scripts/AutoSync.scpt`.

Instead of scheduling this with cron, lets create a LaunchAgent for launchd to run. This is probably one of the more simple uses for launchd; its just going to run this script once a day at 4:15am. Here's the plist:





        Label
        local.isync.sync

        LowPriorityIO
        

        Nice
        1

        ProgramArguments
        
                osascript
                Library/Scripts/AutoSync.scpt
        

        ServiceDescription
        Nightly iSync Sync

        StartCalendarInterval
        
                Hour
                4
                Minute
                15
        

        StandardOutPath
        /dev/null

        StandardErrorPath
        /dev/null


I'll quickly highlight the various fields in the plist. Label is an arbitrary string assigned to the job (it should be unique from other jobs); its just a nice human readable identifier. LowPriorityIO and Nice are just letting launchd know that this job isn't very important, if something important is going on when this job runs, let that other thing take precedence. ProgramArguments are what we're going to run: the osascript program with our script as the only argument. ServiceDescription is a human readable description of the job. StartCalendarInterval is when the job should run. This is similar to cron, put in the constraints you want, and leave out the ones you would have *'d in cron (Hour = 4 and Minute = 15 means this will run every year, month, and day at 4:15am).

Take that plist, modify it to your liking (or use Lingon to create one for you) and save it to ~/Library/LaunchAgents/local.isync.sync.plist. Now launchctl can be used to load and run the LaunchAgent. Fire up a terminal and run the following to load the LaunchAgent:

launchctl load ~/Library/LaunchAgents/local.isync.sync.plist

Now run it to make sure it works:

launchctl run local.isync.sync

If everything worked correctly, iSync should pop open, sync whatever it is you want to sync, and quit iSync. Now that the job is loaded, it should run at the interval you assigned. Not only that, but you don't have to be the active user for it to run, meaning multiple users can load the same (or similar) job sometime in the middle of the night. The only drawback I've found is that you must be logged in for the LaunchAgent to work (you don't have to be the current user, but you have to be logged in). UPDATE: I don't know what I was thinking, but you still have to be the active user (or console owner as Apple puts it). So this doesn't really solve any of the shortcomings of cron. Looking at Apple's List of daemonable frameworks, it appears neither AppleScript or SyncServices are daemon safe. Safe to say, I still like launchd, and will continue to use it to automate things in place of cron.

That's just the tip of the launchd iceberg! Happy hacking!

Update: I've received a few emails mentioning whitespace issues when copying and pasting the above AppleScript, so I've uploaded a binary here.

Permanent link to Automate iSync with <code>launchd</code>

hohle.net | hohle.org | hohle.name | hohle.co.uk | hohle.de | hohle.info