Time Machine: Mount backup disk on demand

Jul 21, '10 07:30:00AM

Contributed by: syzygies

It is both inconvenient and less safe to keep backup volumes mounted when not in use. A mounted volume is more prone to corruption, and open file dialogs needlessly spin up mounted volumes, contributing to a user experience latency that is most apparent if the primary drive is solid state.

By creating an /etc/periodic/hourly directory modeled after the existing /etc/periodic/daily directory, and adapting com.apple.periodic-daily.plist, there is a place to put a script that handles mounting and unmounting the Time Machine backup disk, and launching Time Machine.

Scripts that implement nightly tasks can be placed in the /etc/periodic/daily directory, which is run at 3:15 AM by /System/Library/LaunchDaemons/com.apple.periodic-daily.plist. Using sudo in Terminal, create the parallel directory /etc/periodic/hourly and the parallel file /System/Library/LaunchDaemons/com.apple.periodic-hourly.plist.

Then clone the file com.apple.periodic-hourly.plist from com.apple.periodic-daily.plist, replacing each instance of daily by hourly, and replacing the StartCalendarInterval by a StartInterval of 3600 seconds. Specifically, replace:

<key>StartCalendarInterval</key>
<dict>
 <key>Hour</key>
 <integer>3</integer>
 <key>Minute</key>
 <integer>15</integer>
</dict>
with
<key>StartInterval</key>
<integer>3600</integer>
The syntax for this file is documented by the man page launchd.plist(5). Now, after restart, any script placed in /etc/periodic/hourly will be run every hour as super user.

Turn off Time Machine in System Preferences, but leave the correct backup disk selected. One can still launch Time Machine from the command line or a script, by calling backupd-helper. Our script will mount and unmount the backup disk as necessary, and call Time Machine by this mechanism.

One can create a similar script to automate SuperDuper! backups. One can reverse engineer a replacement for backupd-helper by watching SuperDuper! as it runs using the command line command ps -ax | grep SDCopy. Calling SDCopy is unsupported, but it has worked flawlessly for me for years, and opens up the possibility of updating folders as easily as volumes.

You can suggest that Apple implement this functionality for Time Machine, by providing feedback at Mac OS X Feedback. SuperDuper! requests that I not document this functionality for their SDCopy tool, so you should ask them to support mounting and unmounting backup volumes on demand.

Together, these scripts implement the software portion of silencing my external drives. To complete this task, one can either use passively cooled external enclosures, or replace the fan on an actively cooled enclosure. See Modding the WiebeTech RTX220-QJ for an example of how one might do this.

Here is the Time Machine script, written in Python. I have hard-wired my backup disk and log file directory, so the script needs to be adapted for use on a different system. (In general, one should never run any script as super user that one doesn't completely understand.)

#!/usr/bin/env python

# place in /etc/periodic/daily to run nightly (or hourly if set up):
#
# sudo cp -f timemachine.py /etc/periodic/daily/667.timemachine
# sudo cp -f timemachine.py /etc/periodic/hourly/667.timemachine

import re
import datetime
import time
from subprocess import Popen, PIPE

# do executes a shell command, returning stdout and stderr

def do(cmd):
 p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
 (fi, fo, fe) = (p.stdin, p.stdout, p.stderr)
 fi.close()
 msg = fo.read()
 fo.close()
 fe.close()
 return msg

# mounted checks if vol is mounted

def mounted(vol):
 pat = r'/Volumes/(\S+)\s+[(](.+)[)]'
 msg = do('/sbin/mount')
 vols = dict(re.compile(pat).findall(msg))
 return vol in vols.keys()

# disks returns a dictionary associating e.g. 'Flash' to 'disk0s2'

def disks():
 pat = r'Apple_HFS\s+(\S+)\s+.+(disk\d+s\d+)'
 msg = do('/usr/sbin/diskutil list')
 vols = re.compile(pat).findall(msg)
 return dict(vols)

# sleep waits a bit

def sleep():
 time.sleep(30)

# mount_volume returns pair (now mounted?, was mounted?),
# after attempting to mount volume for "name" as needed

def mount_volume(vol, logfile):
 if mounted(vol):
 (now, was) = (True, True)
 else:
 was = False
 diskID = disks()
 mountable = diskID.keys()
 if vol in mountable:
  do("/usr/sbin/diskutil mount %s >>'%s' 2>&1" % (diskID[vol], logfile))
  sleep()
 now = mounted(vol)
 return (now, was)

# unmount_volume unmounts volume

def unmount_volume(vol, logfile):
 do("/usr/sbin/diskutil unmount '/Volumes/%s' >>'%s' 2>&1" % (vol, logfile))

# run Time Machine
# Time Machine has finished when 'backupd' is no longer running

logdir = '/Local/Backup Logs'
backupd = "/System/Library/CoreServices/backupd.bundle/Contents/Resources/backupd-helper >>'%s' 2>&1"

def start_backupd(logfile):
 do("echo 'starting Time Machine' >>'%s'" % logfile)
 do(backupd % logfile)

def backupd_running(logfile):
 return do("ps -ax | grep [b]ackupd >>'%s' 2>&1" % logfile)

def timemachine(vol):
 now = datetime.datetime.now().strftime("%F %H-%M-%S")
 logfile = "%s/%s (Time Machine)" % (logdir, now)
 (tnow, twas) = mount_volume(vol, logfile)
 if tnow:
 start_backupd(logfile)
 sleep()
 while backupd_running(logfile):
  sleep()
 if not twas:
  sleep()
  for i in range(1,8):
  sleep()
  unmount_volume(vol, logfile)
  if not mounted(vol):
   break

# execute (replace 'Time' by name of Time Machine volume)

timemachine('Time')


[crarko adds: I haven't tested this one. The python script is mirrored here. As advised, be sure to gain an understanding of what the script does before attempting to use it.]

Comments (22)


Mac OS X Hints
http://hints.macworld.com/article.php?story=20100712085231232