August 12, 2009

iTunes syncing

Syncing an iTunes library between two computers isn't a straightforward task, normally for this I would simply drag the necessary files between two computers, but for the carputer project, this wasn't desirable. I need some way to keep the two systems in sync with a minimum of fuss and effort. Fortunately, there is a terminal command called rsync that is ideal for handling this task. Rsync works over an ssh connection between the two systems, and since this is running via a script, the login for this connection needs to run without asking for a password, so the use of ssh keys is required. There are a number of good tutorials on setting this up on the web, so I won't document that process here.

The script I need also needs to address a few other issues, including the system sleeping, which presented a few additional wrinkles. As things stand now, the carputer will be a laptop, and will run via the power adapter most of the time. When the car shuts off, the power adapter will be off, and the laptop will be on battery power. The Energy Saver settings are set to sleep the laptop after 1 minute. Obviously, if I'm in the middle of syncing data, I don't want the system to sleep, so the script will override this.

Finally, the script needs to check if the system we're syncing data from is reachable, and abort if it isn't. So, that being said, here is the current script I'm using:

#!/bin/ksh

# this command will keep the system from going to sleep, spawns a new processes
`pmset noidle` &
# get pid of the pmset process so we can kill it later
pid=`ps | grep [p]mset | awk '{print $1}'`

# set the log file to be placed in user's home directory
theLog=~/sync.log

# need to kill pmset we spawned earlier so system will sleep
kill -9 $pid

# Now tell system to go to sleep since we're done
pmset sleepnow

One additional thing I added some was simply logging of the script's output, I set up a variable called theLog with the file name. Any lines that get echoed to the log will include the date, except for the output of the rsync command.

The server and user variables would be for the machine you're syncing data from, setting up this master machine with a static IP address would obviously be a good idea. Since I'll only be performing this sync while the car is parked in the driveway, I know I'll be connected to my home wireless network, but it's possible that if the script ran while connected to another network that a machine with the specified IP might be found, but the rsync would then fail. Additional programming could be put in place to check for that scenario, but for me isn't necessary at this point.

One tricky bit that this script needs to handle is to prevent the Mac from going to sleep while we're doing our sync. Our Energy Saver settings are set to sleep after 1 minute on battery power, it is possible that our script could run longer than that depending on the amount of changed data that needs to sync. There are some 3rd party tools that can prevent system sleep, but nothing that was ideal for what I wanted here. After much Googling and experimenting, a relatively elegant solution presented itself.

The pmset command, used for interfacing with the Power Manager, in addition to being able to read and set various power settings (system sleep, hd spin down, display sleep, etc), also has a relatively unknown option to prevent the system from sleeping: pmset noidle. Now, the tricky part is that if you enter this command at the terminal prompt, the command runs, preventing the system from sleeping, and just sits there until you quit the process by typing Ctrl-C. Not useful for a script.

What I've done is to tell my script to fork this process by appending an ampersand at the end, so that it will run independently. in the background, letting the rest of this script continue on. In order to kill the process later, we need the process ID of this command, which is what that grep/awk line does. There is actually an easier way to get the PID of a spawned process, but this command creates a new shell and runs the command in that shell, and simply quitting that shell wouldn't quit the pmset that is running there, so it was necessary to get the actual PID of that command in order to terminate it.

So, the system sets the log location, uses the pmset to tell the system to never sleep, , and next, we set the IP address and username for the machine we're connecting to, and do a test ping to see if that IP is reachable. As my intention is to run this script while parked in my driveway, connected to my home wireless network, it wasn't necessary to perform any additional checks, but it is possible that if the system connects to a different wireless network, another system with that IP might be found, which would mean that the rsync will fail later, but for my use this wasn't worth the effort of coding around.

So, if we find out server is down that we want to be copying iTunes data from, then we log that fact, otherwise, we log that the server was found, and use rsync to connect to the home system, and copy files from the specified user's Music folder in their home directory, the ssh connection established will default us to that user's home directory, since we aren't specifying any path, the Music/iTunes/ folder from that location is what we're copying. We specify the destination as the current user's home directory/Music/iTunes folder. The -avz parameters will cause only changed data to be synced, new files will be copied, removed files will be deleted.

When the first rsync is done, everything will need to be copied, so obviously it's preferred to do this while running on AC power when this is first setup. After that, changes should be minimal, and will only take a few moments or minutes, depending on the amount of changed data.

After the rsync completed, we kill the pmset process we forked earlier, echo to the log that we're done and about to sleep, then finally use pmset again to tell the system to sleep now, and we're done!