Forge Code

Interfacing With Disk Utility Using Ruby Scripting and RubyCocoa

Automated backups are a wonderful thing. Ideally, every computer user should have at least one automated backup system in place for their data. Apple’s Time Machine is a fantastic solution for most users. Despite its flaws, it is extremely easy to setup, and it works unobtrusively in the background. I use it to backup all of my Macs as my main backup solution. Power users, particularly programmers, however, like to have a little bit more control over these sorts of things. Even though I already have Time Machine backing up my Mac Pro, I want to be able to write custom backup scripts to backup the most important data onto a second internal HDD. That’s not too hard to do using rsync or something similar, but I don’t want to have my backup volume visible on the Desktop when it’s not in use. Having the backup volume easily accessible increases the risk that a backup will “accidentally” go missing, or get borked somehow. In essence, I want my backup scripts to do the following:

  • Mount the backup volume
  • Do the backup
  • Unmount the backup volume

Easy enough, right?

The Problem - Mounting a Volume using its Name

Apple provides a command line utility named ‘diskutil’, that gives all of the functionality of Disk Utility. To unmount a volume with the name “MyVolume”, you can do the following:

diskutil unmount /Volumes/MyVolume

Relatively straightforward. Unfortunately, to mount the same volume, you cannot do this:

diskutil mount /Volumes/MyVolume

since there is no /Volumes/MyVolume unless that volume is mounted. According to the diskutil man page, the four ways of identifying a ‘Device’ are:

  • The device node entry. Any entry of the form of /dev/disk*, e.g. /dev/disk2.
  • The disk identifier. Any entry of the form of disk*, e.g. disk1s9.
  • The volume mount point. Any entry of the form of /Volumes/*, e.g. /Volumes/Untitled.
  • The Universally Unique Identifier or UUID. Any entry of the form of e.g. 11111111-2222-3333-4444-555555555555.

In other words, the only way to mount a volume using diskutil is to identify it using an identifier like ‘/dev/disk2’ or ‘disk2’. Sure, you can go and look up these identifiers for a specific disk or volume using Disk Utility (or diskutil), but since they are dependent on your hardware configuration (and could change when you add or remove any physical disk), it’s not a good idea to identify a disk or volume this way in a script.

Since I’ve been wanting to learn more about Ruby anyway, this seemed like a perfect task for a simple Ruby script.

Interfacing with diskutil using RubyCocoa

After skimming through the diskutil man page, I noticed that there was an option to format the output of certain commands (such as list and info) as a plist (property list). Parsing a plist from a file is trivial in Cocoa, so with the magic of RubyCocoa, it becomes trivial in Ruby as well:

require 'osx/cocoa'
dict = OSX::NSDictionary.dictionaryWithContentsOfFile(plistPath)

Since the actual plist data we have is not in a file, but comes from the output of a command line program, I used the following code to capture it. Note that ‘shell_command’ is a shell command that will dump a plist to stdout.

temp_file_path = "/tmp/temp.plist"
`#{shell_command} | cat > #{temp_file_path}`
dict = OSX::NSDictionary.dictionaryWithContentsOfFile(temp_file_path)

This isn’t ideal, but I couldn’t work out a better way to get the plist output from stdout into a dictionary (or hash as they are called in Ruby). If you know a better way, let me know!

By calling diskutil list -plist, you will get something similar to the following:

> diskutil list -plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>AllDisks</key>
  <array>
      <string>disk0</string>
      <string>disk0s1</string>
      <string>disk0s2</string>
      <string>disk1</string>
      <string>disk1s1</string>
      <string>disk1s2</string>
      <string>disk2</string>
  </array>
  <key>VolumesFromDisks</key>
  <array>
      <string>Giskard 750GB</string>
      <string>DVD_VIDEO</string>
  </array>
  <key>WholeDisks</key>
  <array>
      <string>disk0</string>
      <string>disk1</string>
      <string>disk2</string>
  </array>
</dict>
</plist>

If you look closely, you will notice that the WholeDisks key lists 3 disks, whereas the VolumesFromDisks key only lists 2 volumes. That’s because the third disk is my backup drive, which was unmounted when I ran the command. To get more information on a volume (using its diskXsX identifier) from diskutil, you can do the following:

> diskutil info -plist disk0s2

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>BayName</key>
  <string>"Bay 3"</string>
  <key>Bootable</key>
  <true/>
  <key>BusProtocol</key>
  <string>SATA</string>
  <key>CanBeMadeBootable</key>
  <false/>
  <key>CanBeMadeBootableRequiresDestroy</key>
  <false/>
  <key>Content</key>
  <string>Apple_HFS</string>
  <key>DeviceIdentifier</key>
  <string>disk0s2</string>
  <key>DeviceNode</key>
etc...

This plist contains key named VolumeName, which surprisingly enough contains the volume name of the disk in question. In order to achieve our original aim of mounting a disk using its volume name, we need to get the disk identifier (in the form diskXsX) from the volume name. To do that, all we have to do is grab the list of disks using diskutil list -plist, then we can iterate through each disk in the AllDisks array, and call diskutil info -plist diskXsX. If the volume name of that disk matches the volume we are looking for, we know we have a match.

The DiskUtil Ruby class

I created a Ruby class named DiskUtil that handles the interfacing with diskutil. It has the following core methods:

  • nsdictionary_from_shell_command(shell_command)
  • refresh_volume_infos
  • volume_infos
class DiskUtil
  
require 'osx/cocoa'
  
@@volume_infos = nil

# Returns a RubyCocoa dictionary/hash containing the data of an executed shell command
def self.nsdictionary_from_shell_command(shell_command)
  temp_file_path = "/tmp/temp.plist"
  `#{shell_command} | cat > #{temp_file_path}`
  dict = OSX::NSDictionary.dictionaryWithContentsOfFile(temp_file_path)

  if dict == nil: puts "Error: nsdictionary_from_shell_command(\"#{shell_command}\") failed" end    
          
  return dict
end

# Read the latest volume_infos from diskutil
def self.refresh_volume_infos
  all_disks = nsdictionary_from_shell_command("diskutil list -plist").objectForKey("AllDisks")
  @@volume_infos = all_disks.collect do | device_id |
      nsdictionary_from_shell_command("diskutil info -plist #{device_id}")
  end
end

def self.volume_infos
  if @@volume_infos == nil
      self.refresh_volume_infos
  end
  return @@volume_infos
end

end

nsdictionary_from_shell_command contains the code listed earlier in this post with some error checking tacked on for good measure. refresh_volume_infos has the job of iterating through the diskutil output and putting it into Ruby data structures. The result is kept in the class variable @@volume_infos. The volume_infos method is a simple cached accessor method for @@volume_infos. It checks to see if @@volume_infos is nil, and if it is, refresh_volume_infos is called. This provides a simple caching mechanism for reading @@volume_infos.

Every call to diskutil info -plist diskXsX incurs a slight delay - on my Mac Pro, it’s probably about 0.3 seconds. If we called diskutil every time we wanted to access this data, we wouldn’t be able to liberally use Ruby iterators and retain object-oriented modularity without suffering a significant performance penalty. By reading all necessary data from diskutil once, then caching that data in a native Ruby data structure, we can iterate in whatever way we want without any (extra) performance overhead.

There are more methods in the DiskUtil class that provide higher-level behaviour, but if you have followed this article so far, they should be quite self explanatory.

Handling Command Line Invocation

Apart from the DiskUtil class, the script also contains code to handle invocation from the command line. This basically means handling command line arguments and translating them into DiskUtil calls. Again, nothing too complicated here, but it is probably a useful starting point if you are writing your own Ruby utility scripts.

# Only gets called if called from the command line (not in irb)
if $0 == __FILE__
  $supported_argv_verbs = ['mount', 'unmount']


  # first check to see if the ARGV is "acceptable"
  if ARGV.length != 2 or !$supported_argv_verbs.include?(ARGV[0])
      puts "disk_assistant #{$disk_assistant_version} written by Nick Forge.\n\nUsage:\n\t#{$0} verb volumeName\n\n\tverb = {mount, unmount}\n\n"
      exit
  end
  

  if ARGV[0] == "mount"
      if DiskUtil::mount_volume(ARGV[1]) : Process.exit!(0) end
  elsif ARGV[0] == "unmount"
      if DiskUtil::unmount_volume(ARGV[1]) : Process.exit!(0) end
  end
  
  Process.exit!(1)
end

Using disk_assistant

All of this code is contained in one file, which is named disk_assistant. Usage is very simple. The following pretty much says all there is to say:

disk_assistant unmount MyVolume

disk_assistant mount MyVolume

Caveats

I wrote this script as a learning exercise, and at the time I had no real Ruby experience, so I’m sure there are conventions and design patterns that I’m breaking in this code. No warranty, implied or otherwise, is given with this code. Don’t use it in a nuclear reactor, or your grandma’s life support machine (which I’m sure runs OS X 10.5). Please let me know if there’s any obvious problems or typos.

Download

To download the full disk_assistant script, head over to the disk_assistant project page.

Addendum

The same functionality that disk_assistant can be achieved as a one-liner using diskutil, grep and awk like so:

diskutil mount `diskutil list | grep "My Backup Volume" | awk '{print $NF}'`

The point of this exercise, however, was not to solve this particular problem in the most efficient way. The point was to show how Ruby/RubyCocoa is a worthy replacement for shell scripting from the perspective of a developer. The advantages to using Ruby/RubyCocoa for scripting if you are a developer are two-fold:

  • When you are used to using modern languages, shell scripts full of awk and sed are not easily parsed by our “spoilt” developer brains. I find if a < b much more natural than if [ $a -lt $b ], and I suspect most developers who don’t live in the command line/shell scripting world would too. I don’t mind having a slightly longer script if I can mentally parse it more quickly.

  • Ruby/RubyCocoa allows you to do things like deal directly with the entire Cocoa API, as well as the Ruby library that ships with OS X. The syntactical richness of Ruby allows you to do very complex things with arrays and lists very easily - often in a single statement.

I have started using Ruby/RubyCocoa scripts exclusively when I know that a particular script is going to be used on OS X 10.5+ exclusively. Developers: what do you use for your scripting needs?

Comments