swapfile.pl

Copyright © 2004-2006 Dave Bayer. Subject to the terms and conditions of the MIT License.

#!/usr/bin/perl
use warnings;
use strict;

# swapfile.pl  --  Perl script for moving swapfile in Mac OS X 10.4
#
# Copyright (c) 2004-2006 Dave Bayer
# Subject to the terms and conditions of the MIT License.

We assign a template for the shell script rc.swapfile to $rc_swapfile, using Perl's here-document syntax. Each line starting with a colon : is part of the quoted string. The leading colons serve to visually distinguish the quoted text from the rest of the Perl code, and are stripped en passant as the text is assigned to $rc_swapfile.

(my $rc_swapfile = <<'EOF') =~ s/^: ?//mg;
: #!/bin/sh
: #
: # rc.swapfile  --  set up a separate swapfile partition in Mac OS X 10.4
: #
: # Copyright (c) 2004-2006 Dave Bayer
: # Subject to the terms and conditions of the MIT License.
: #
: # http://www.math.columbia.edu/~bayer/OSX/swapfile/
: #
: # Permission is hereby granted, free of charge, to any person obtaining a
: # copy of this software and associated documentation files (the "Software"),
: # to deal in the Software without restriction, including without limitation
: # the rights to use, copy, modify, merge, publish, distribute, sublicense,
: # and/or sell copies of the Software, and to permit persons to whom the
: # Software is furnished to do so, subject to the following conditions:
: # 
: # The above copyright notice and this permission notice shall be included in
: # all copies or substantial portions of the Software.
: # 
: # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
: # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
: # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
: # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
: # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
: # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
: # DEALINGS IN THE SOFTWARE.
:

swapsearch gives the disk numbers to inspect, and the order in which to inspect them. swapfile.pl will modify this value, so the most likely disk number is listed first.

: swapsearch='0 1 2 3 4 5 6 7 8 9'

swapmap is the partition number of the swap volume. swapfile.pl will modify this value.

: swapmap='10'

swapvolume is the name of the swap volume. swapfile.pl will modify this value.

: swapvolume='Swap'

swaptype is the format of the swap volume, either ufs or hfs. swapfile.pl will modify this value.

: swaptype='ufs'

swapprint is the md5 checksum of the swap disk, as described by pdisk. swapfile.pl will modify this value.

: swapprint='f7904e44f9d7b5043d90126fb74a0bca'

swaplog is the location of the log file to be used by rc.swapfile.

: swaplog='/var/log/rc.swapfile.log'
:
: echo >>${swaplog}
: echo `date` >>${swaplog}
:

swskip is the md5 checksum of a nonexistent disk, as described by pdisk. swapfile.pl will modify this value.

: swskip='d41d8cd98f00b204e9800998ecf8427e'

The first while loop of rc.swapfile examines each available disk using pdisk, and compares their md5 checksums to the saved values swapdisk and swskip. We wait between iterations, to give slow drives a chance to spin up and be recognized. If a candidate disk for the swap volume is found, we leave this section of code with swfound=1.

: swcount=1
: swfound=0
: while [ $swcount -le 5 ] && [ $swfound -eq 0 ]
: do
: 	if [ $swcount -ge 2 ]; then echo '--' >>${swaplog}; fi
: 	for swapdisk in ${swapsearch}
:	do
: 		swaphash=`pdisk /dev/disk${swapdisk} -dump 2>/dev/null | grep -v '/dev/disk' | md5 -q`
: 		if echo ${swaphash} | grep -qF ${swskip}; then continue; fi
: 		echo  /dev/disk${swapdisk} ${swaphash} >>${swaplog}
:
: 		if echo ${swaphash} | grep -qF ${swapprint}
:		then
: 			swfound=1
:			break
:		fi
:	done
: 	sleep 2
: 	swcount=`expr $swcount + 1`
: done
:

The next until loop of rc.swapfile attempts to mount the candidate disk for the swap volume, if it is not already mounted. If the candidate disk for the swap volume is successfully mounted, we leave this section of code with swmounted=1.

: swmounted=0
: if [ $swfound -eq 1 ]
: then
: 	swapdevice=/dev/disk${swapdisk}s${swapmap}
:	swcount=0
:	swmounted=1
: 	until /sbin/mount | grep -qF ${swapdevice}
:	do
:		if [ $swcount -ge 5 ]
:		then
:			echo "Unable to mount swap volume ${swapvolume}; using default configuration" >>${swaplog}
:			swmounted=0
:			break
:		fi
:		sleep $swcount
:		if [ $swcount -eq 2 ]
:		then
: 			echo "/sbin/fsck -y ${swapdevice}" >>${swaplog}
: 			/sbin/fsck -y ${swapdevice} >/dev/null 2>&1
:			sleep 1
:		fi
: 		echo "/sbin/mount -vt ${swaptype} ${swapdevice} /Volumes/${swapvolume}" >>${swaplog}
:		/sbin/mount -vt ${swaptype} ${swapdevice} "/Volumes/${swapvolume}" >>${swaplog} 2>&1
:		sleep $swcount
: 		swcount=`expr $swcount + 1`
:	done
: else
:	echo "Swap volume ${swapvolume} not found; using default configuration" >>${swaplog}
: fi
:

Finally, rc.swapfile checks for the existence of the file .enablevm. If it is found, then we clean up the old swap directory, and set swapdir to the new desired location. swapdir is used by Apple's /etc/rc script to set up virtual memory.

: if [ $swmounted -eq 1 ]
: then
: 	if [ -f "/Volumes/${swapvolume}/.enablevm" ]
:	then
: 		echo "Using ${swapdevice} for swapfile" >>${swaplog}
: 		if [ -f ${swapdir}/swapfile0 ]; then rm -rf ${swapdir}/swap*; fi
: 		swapdir="/Volumes/${swapvolume}/.vm"
: 	else
: 		echo "/Volumes/${swapvolume}/.enablevm not found; using default configuration" >>${swaplog}
: 		echo "/sbin/umount -v /Volumes/${swapvolume}" >>${swaplog}
: 		/sbin/umount -v "/Volumes/${swapvolume}" >>${swaplog} 2>&1
: 	fi
: fi
:

We unset all variables used by rc.swapfile, and return to Apple's /etc/rc script.

: unset swapsearch swapmap swapvolume swaptype swapprint swskip swaplog swcount swfound swapdisk swaphash swapdevice swmounted
EOF

$help is another Perl here-document, used to provide help when swapfile.pl is called with incorrect arguments.

(my $help = <<'EOF') =~ s/^: ?//mg;
:
: Usage:
:
: 	swapfile.pl install volume
: 	swapfile.pl uninstall
: 	swapfile.pl inspect
: 	swapfile.pl automount bool
: 	swapfile.pl cleanup volume
:	swapfile.pl volumes
:
: Examples:
:
: 	sudo swapfile.pl install Swap
: 	Moves swapfile to partition mounted at /Volumes/Swap, on next restart
:
: 	sudo swapfile.pl uninstall
: 	Restores location of swapfile to system partition, on next restart
:
: 	swapfile.pl inspect
: 	View diagnostic information about current status of swap partition
:
: 	sudo swapfile.pl automount true
: 	Sets system preference to mount external drives during startup
:
: 	[sudo] swapfile.pl cleanup volume
: 	Remove swap files from volume no longer used for swapping
:
: 	[sudo] swapfile.pl volumes
: 	View disk fingerprints and associated volumes
:
EOF

We now begin writing Perl code for swapfile.pl itself:

my ($rc_key, $rc_before, $rc_insert, $rc_remove, %rc_md5, $rc_swapfile_md5, $hr, %run, $do, $arg);

$rc_key = ".enablevm";
$rc_before = "swapdir=/private/var/vm";
$rc_insert = "if [ -f /etc/rc.swapfile ]; then . /etc/rc.swapfile; fi # inserted locally";

These are the md5 checksums for Apple's /etc/rc script, unmodified and as modified by swapfile.pl, for OS X 10.3 and 10.4:

%rc_md5 = (
	# OS X 10.3 /etc/rc
	'79c0a57e77161ed614dfc72e3bf833f5' => 0,
	'cdb80266e5ee95f478ced5e32142dc46' => 1,

	# OS X 10.4 /etc/rc
	'0406f688163230496ee55f85fdd413da' => 0,
	'64e03196440f0cd5c4bbbe563d269d55' => 1,
);

cmd executes a shell command, echoing the command and its result to standard output, and returns the result.

sub cmd {
	my ($do) = @_;
	print "\n    % $do\n";
	my $res = `$do`;
	(my $show = $res) =~ s/^/    /mg;
	print $show;
	$res;
}

rc_modded checks the status of Apple's /etc/rc script, to see if we have modified it, and to see if it is one of the recognized versions recorded in $rc_md5. It returns 1 if /etc/rc has been modified to call rc.swapfile, and 0 otherwise.

sub rc_modded {
	my $fingerprint = `md5 -q /etc/rc`;
	chop($fingerprint);
	my $mod = $rc_md5{$fingerprint};
	if (! defined($mod)) {
		printf "Warning: unrecognized version of /etc/rc\n%s\n", $fingerprint;
		$mod = `grep -F "$rc_insert" "/etc/rc"` ? 1 : 0;
	}
	if ($mod) {
		print "/etc/rc calls /etc/rc.swapfile\n";
	} else {
		print "/etc/rc does not call /etc/rc.swapfile\n";
	}
	$mod;
}

edit_rc modifies the Apple script /etc/rc. For install it adds the line $rc_insert to /etc/rc, which calls our script rc.swapfile before virtual memory is set up. For uninstall it removes this same line.

sub edit_rc {
	local $/;

	open(FILE,"</etc/rc") or die "unable to read from file /etc/rc\n\n";
	$_ = <FILE>;
	close(FILE);

	if ($do eq "install") {
		s:($rc_before\n):$1\n$rc_insert\n\n:;
	}
	elsif ($do eq "uninstall") {
		(my $rc_delete = $rc_insert) =~ s:\[:\\[:;
		s:\n\s*$rc_delete\s*:\n:;
	}

	open(FILE,">/etc/rc") or die "unable to write to file /etc/rc\n\n";
	print FILE;
	close(FILE);
}

automount_status checks the status of the system preference for mounting external drives at startup time.

sub automount_status {
	my $status = "false";
	if ( -f "/Library/Preferences/SystemConfiguration/autodiskmount.plist") {
		$status = `defaults read /Library/Preferences/SystemConfiguration/autodiskmount AutomountDisksWithoutUserLogin`;
		$status = $status == 1 ? "true" : "false";
		printf "AutomountDisksWithoutUserLogin is %s\n\n", $status;
	}
	else {
		printf "autodiskmount.plist has not yet been created\n\n";
	}
	$status;
}

install_cmd examines the swap volume $vol, edits /etc/rc to call rc.swapfile, and writes a version of rc.swapfile customized to look for $vol.

sub install_cmd {
	my ($vol) = @_;
	local $/;

	die "/etc/rc does not support whitespace in swap volume names\n\n" if $vol =~ /\s/;
	(my $df = cmd("df | grep '/Volumes/$vol\$'")) or die "/Volumes/$vol not found\n\n";
	(my $disk = $df) =~ s:/dev/disk(\d*)s\d*.*\n:$1:;
	(my $map = $df) =~ s:/dev/disk\d*s(\d*).*\n:$1:;

	my $entry = cmd("pdisk /dev/disk$disk -partitionEntry $map");
	my $type = "hfs";
	if (1 == `echo '$entry' | grep -c ' Apple_HFS '`) { $type = "hfs"; }
	elsif (1 == `echo '$entry' | grep -c ' Apple_UFS '`) { $type = "ufs"; }
	else { print "\nWarning: unable to determine type of partition format; defaulting to hfs\n"; }

	my $fingerprint = cmd("pdisk /dev/disk${disk} -dump 2>/dev/null | grep -v '/dev/disk' | md5 -q");
	chop($fingerprint);
	my $skip = `echo -n | md5 -q`;
	chop($skip);

	my $search = '0 1 2 3 4 5 6 7 8 9';
	$search =~ s:$disk ?::;
	$search = "$disk " . $search;

	if (-f "/etc/rc.swapfile") {
		print "\n/etc/rc.swapfile already exists, and will be updated\n"
	}
	else {
		print "\ncreating /etc/rc.swapfile\n"
	}

	$_ = $rc_swapfile;

	s:(swapsearch=)'[ \d]*':$1'$search':;
	s:(swapmap=)'\d*':$1'$map':;
	s:(swapvolume=)'\w*':$1'$vol':;
	s:(swaptype=)'\w*':$1'$type':;
	s:(swapprint=)'\w*':$1'$fingerprint':;
	s:(swskip=)'\w*':$1'$skip':;

	open(FILE,">/etc/rc.swapfile") or die "unable to write to file /etc/rc.swapfile\n\n";
	print FILE;
	close(FILE);

	cmd("grep '^swap.*=\'.*\'\$' /etc/rc.swapfile");
	print "\n";

	automount_status();
	if (! rc_modded()) {
		print "editing /etc/rc to insert call to rc.swapfile\n";
		edit_rc();
	}
	print "\n";

	if (-f "/Volumes/$vol/$rc_key") {
		print "/Volumes/$vol/$rc_key already exists\n";
	}
	else {
		print "writing /Volumes/$vol/$rc_key\n";
		`echo > '/Volumes/$vol/$rc_key'`;
	}
	print "\n";
}

uninstall_cmd reverses the change to /etc/rc and deletes rc.swapfile.

sub uninstall_cmd {
	print "\n";

	automount_status();
	if (rc_modded()) {
		print "editing /etc/rc to delete call to rc.swapfile\n\n";
		edit_rc();
	}
	if (-f "/etc/rc.swapfile") {
		print "deleting /etc/rc.swapfile\n";
		`rm /etc/rc.swapfile`;
	}
	else {
		print "/etc/rc.swapfile not found\n";
	}
	print "\n";
}

inspect_cmd examines the log file and virtual memory status, to see what happened on the last startup.

sub inspect_cmd {
	print "\n";
	automount_status();
	rc_modded();
	cmd("grep '^swap.*=\'.*\'\$' /etc/rc.swapfile") if ( -f "/etc/rc.swapfile");
	cmd("tail -n 16 /var/log/rc.swapfile.log");
	my $path = cmd("ps -wwax | grep dynamic_pager | grep -v grep");
	$path =~ s/.* (\S+)\n/$1/;
	cmd("ls -l $path*");
	print "\n";
}

automount_cmd sets the system preference for mounting external drives at startup time.

sub automount_cmd {
	my ($bool) = @_;
	$bool eq "true" or $bool eq "false" or die "true or false required as parameter to automount\n\n";
	print "\n";
	if ($bool ne automount_status()) {
		print "setting AutomountDisksWithoutUserLogin to $arg\n";
		`defaults write /Library/Preferences/SystemConfiguration/autodiskmount AutomountDisksWithoutUserLogin -bool $arg`;
	}
}

cleanup_cmd removes .vm and .enablevm from $vol, if it is not in current use as the swap partition.

sub cleanup_cmd {
	my ($vol) = @_;

	print "\n";
	die "/Volumes/$vol not found\n\n" unless `df | grep '/Volumes/$vol\$'`;
	die "$vol is in use as the swap partition\n\n" if `ps -wwax | grep dynamic_pager -m1 | grep  '/Volumes/$vol'`;

	automount_status();
	if (-f "/Volumes/$vol/$rc_key") {
		print "deleting /Volumes/$vol/$rc_key\n";
		`rm '/Volumes/$vol/$rc_key'`;
	}
	else {
		print "/Volumes/$vol/$rc_key not found\n";
	}
	if (-d "/Volumes/$vol/.vm") {
		print "deleting /Volumes/$vol/.vm\n";
		`rm -r '/Volumes/$vol/.vm'`;
	}
	else {
		print "/Volumes/$vol/.vm not found\n";
	}
	print "\n";
}

volumes_cmd lists the md5 checksums for each disk, and the volumes on each disk.

sub volumes_cmd {
	my @mount;
	for my $line (split /\n/, `mount | grep /dev/disk | sort`)
	{
		$line =~ s:^/dev/disk(\d+)s\d+ on (.*) \([^()]*\)$:$1\n$2:;
		my ($disk, $vol) = split /\n/, $line;
		$vol =~ s:/Volumes/::;
		if ($vol =~ /[,\s]/) { $vol =~ s/.*/"$vol"/; }
		push @{$mount[$disk]}, $vol;
	}

	my $skip = `echo -n | md5 -q`;
	chop $skip;
	print "\n";
	for (my $disk=0; ; $disk++)
	{
		my $hash = `pdisk /dev/disk${disk} -dump 2>/dev/null | grep -v '/dev/disk' | md5 -q`;
		chop $hash;
		if ( $hash eq $skip )
		{
			if ($disk == 0) { next; }
			else { last; }
		}
		printf "%s /dev/disk%d ", $hash, $disk;
		for my $vol (@{$mount[$disk]}) { printf "%s ", $vol; }
		print "\n";
	}
	print "\n";
}

%run is a hash table pairing command line arguments with their associated Perl subroutines, and the number of required arguments. We execute the corresponding command if possible, and provide help otherwise.

%run = (
	'install' => [\&install_cmd, 1],
	'uninstall' => [\&uninstall_cmd, 0],
	'inspect' => [\&inspect_cmd, 0],
	'automount' => [\&automount_cmd, 1],
	'cleanup' => [\&cleanup_cmd, 1],
	'volumes' => [\&volumes_cmd, 0],
);

($do, $arg) = @ARGV;
if (not $do or not $run{$do} or $run{$do}[1] != $#ARGV) { 
		print $help;
		exit(1);
}

$run{$do}[0]($arg);
print "done\n\n";