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.
|