#!/usr/bin/perl

#----------------------------------------------------------------
# usage: xdramdisk [-v] [-f] mount
#        xdramdisk [-v] [-f] umount
# 
# 	-d       debug mode
# 	-v       verbose mode
# 	-f       ignore existing files in /mnt/xdphys_disk
# 
# This program is to be used in conjunction with xdphys to mount or
# unmount the RAM disk, if such is desired by xdphys.
# 
# This program will be called by xdphys if the "use_ramdisk" svar is 1.
#
# Assumptions:
#
#    *  RAM disk devices are called /dev/ram*
#    *  Filesystem info is in /etc/fstab
#    *  RAM disk mount point is /mnt/xdphys_disk
#    *  Default RAM disk size of 16MB
#    *  The user is allowed to use sudo to force umount the ramdisk
#       if it was last mounted by another user (see reference guide). 
#
# Return codes:
#
# 	0         The RAM disk was mounted, and is ready for use.
# 	1         Invalid command line arguments
# 	2         The RAM disk was mounted, but our file exists already
#             (we delete the file and return 0 for this if the -f
#              flag is given)
#   3         Not Linux!
#   4         Mountpoint problem
#   5         Problem in /etc/fstab
#   6         No valid RAM disk devices found
#   7         Couldn't find config.xdphys
#   8         Mount/umount related error
#   9         A temp file already exists in /mnt/xdphys_disk.
#   
#----------------------------------------------------------------

use Getopt::Std;

# Program control variables
$verbose=0;
$debug=0;
$force=0;
$mount=1;               # 1 if in mount mode, 0 if in umount mode

# Return Codes
$NOERR               =0;
$ERR_ARGS            =1;
$ERR_FILE_EXISTS     =2;
$ERR_OS              =3;
$ERR_MOUNTPOINT      =4;
$ERR_FSTAB           =5;
$ERR_DEVICES         =6;
$ERR_NO_CONFIG       =7;
$ERR_MOUNT           =8;
$ERR_FILE_EXISTS     =9;

# Locations of utility programs
$sudo="/usr/bin/sudo";

# Global variables
$xdphysrc="~/.xdphysrc/config.xdphys";
$mountpoint="/mnt/xdphys_disk";
$def_file="xdphys_file.tmp";
$ramdisk_size=16;                             # Megabytes
@ram_devices = ();                           
$working_device="";                           # The RAM disk device we're
                                              # actually going to use.
$mounted=0;                          
$mounted_by="";                          

#----------------------------------------------------------------
# usage: &usage;
#
# Print the usage message.
#
# Returns: Nothing.
# 
#----------------------------------------------------------------

sub usage () {

	print STDOUT "usage: xdramdisk [-v] [-f] mount\n";
	print STDOUT "       xdramdisk [-v] [-f] umount\n\n";
	print STDOUT " 	-d       debug mode\n";
	print STDOUT " 	-v       verbose mode\n";
	print STDOUT " 	-f       gnore existing files in /mnt/xdphys_disk\n";

}

#----------------------------------------------------------------
# usage: &parse_cmdline;
#
# Parse the command line args.
#
# See header for recognized args.
#
# Modifies the following global vars: $force, $verbose, $debug, $mount.
#
# Returns: Nothing.
# 
#----------------------------------------------------------------

sub parse_cmdline () {

	getopts("fvd",\%option);

	$option{d} && do {
		$debug && 
			print STDOUT "xdramdisk(parse_cmdline): DEBUG: -d switch acknowledged.\n";
		$debug = 1;
	};

	$option{f} && do {
		$debug && 
			print STDOUT "xdramdisk(parse_cmdline): DEBUG: -f switch acknowledged.\n";
		$force = 1;
	};

	$option{v} && do {
		$debug && 
			print STDOUT "xdramdisk(parse_cmdline): DEBUG: -v switch acknowledged.\n";
		$verbose = 1;
	};

	@ARGV || do {
		$verbose && print STDERR "xdramdisk: not enough arguments.\n";
		$verbose && &usage;
		exit($ERR_ARGS);
	};

	$ARGV[0] =~ /^mount$/ && do {
		$debug && 
			print STDOUT "xdramdisk(parse_cmdline): DEBUG: in \"mount\" mode.\n";
		$mount = 1;
		return;
	};

	$ARGV[0] =~ /^umount$/ && do {
		$debug && 
			print STDOUT "xdramdisk(parse_cmdline): DEBUG: in \"umount\" mode.\n";
		$mount = 0;
		return;
	};

	print STDERR "xdramdisk: invalid argument \"".$ARGV[0]."\"\n";
	&usage;
	exit($ERR_ARGS);
}

#----------------------------------------------------------------
# usage: $filename=&expand_tilde($filename);
#
# Expand the tilde in a filename to the user's home directory.
#
# Returns: expanded filename
#----------------------------------------------------------------
sub expand_tilde ($) {

	my $filename = shift(@_);

	# Lovely, isn't it?  From the _Perl Cookbook_, p 231
	# Notes -- $> == effective uid of this process
	$filename =~ s{ ^ ~ ( [^/]* ) }
	              { $1
				       ? (getpwnam($1))[7]
                       : ( $ENV{HOME} || $ENV{LOGDIR}
					        || (getpwuid($>))[7]
						 )
	}ex;

	return ($filename);
}

#----------------------------------------------------------------
# usage: &get_ramdisk_size($filename);
#
# Parse the xdphys config file for the "ramdisk_size" parameter.
#
# Sets the global variable $ramdisk_size; this has a default value
# (see Global Variables section, above).
#
# We check for existence of $filename in &sanity, so this should
# always succeed.
#
# Returns: 1 if found, 0 if not found.
#----------------------------------------------------------------
sub get_ramdisk_size ($) {

	my $filename = shift(@_);
	my @fields;
	my $found;

	$found=0;
	open(CONFIG, "< $filename");
	while (<CONFIG>) {
		/ramdisk_size/ && do {
			@fields = split(/=/);
			$ramdisk_size=@fields[1];
			$found=1;
		};
	}
	close(CONFIG);

	($verbose && !$found) && do {
		print STDERR "xdramdisk: no ramdisk_size found in $filename\n";
		print STDERR "xdramdisk: using ramdisk_size=16\n";
	};
			
	return($found);

}

#----------------------------------------------------------------
# usage: $retval=parse_fstab;
#
# Determine what ram devices are set up in /etc/fstab, and 
# determine which ones are set up correctly in /etc/fstab.
#
# Valid /etc/fstab entries should have $mountpoint as the mountpoint,
# and the "user" flag in options.
#
# Sets the global variable @ram_devices.
#
# Returns: 1 if we found at least one properly set device, 0
# otherwise.
#----------------------------------------------------------------

sub parse_fstab () {

	my @fields;

	# We really have problems if we can't read /etc/fstab
	-r "/etc/fstab" || do {
		$verbose && print STDERR "xdramdisk: /etc/fstab isn't readable!\n";
		return(0);
	};

	# Do we have lines for our ram devices in /etc/fstab, do
	# they use the proper mount point, and have the user option set?
	open(FSTAB, "< /etc/fstab");
	while (<FSTAB>) {
		/^\/dev\/ram([0-9])*/ && do {
			# Split fields on any amount of whitespace
			@fields=split(/\s/);

			# Check the mount point
			$fields[1] =~ m/$mountpoint/ || do {
				$verbose && 
					print STDERR "xdramdisk: mountpoint for ".$fields[0]." is not $mountpoint in /etc/fstab!\n";
					print STDERR "xdramdisk: mountpoint is ".$fields[1]."\n";
				next;
			};

			# Check the options
			$fields[3] =~ /\buser\b/ ||  do {
				$verbose && 
					print STDERR "xdramdisk: mount options for ".$fields[0]." must contain \"user\"in /etc/fstab!\n";
				$verbose && 
					print STDERR "xdramdisk: mount options for ".$fields[0]." are ".$fields[3]."!\n";
				next;
			};

			push(@ram_devices,$fields[0]);
			$debug && print STDOUT "xdramdisk(parse_fstab): DEBUG: added ".$fields[0]." to \@ram_devices\n";
		};
	}
	close(FSTAB);

	if (@ram_devices) {
		$debug && print STDOUT "xdramdisk(parse_fstab): DEBUG: returning 1\n";
		return(1);
	} else {
		$debug && print STDOUT "xdramdisk(parse_fstab): DEBUG: returning 0\n";
		return(0);
	}
}

#----------------------------------------------------------------
# usage: $retval=test_devices;
#
# Test devices discovered by parse_fstab for validity.
#
# Changes the global variable @ram_devices.
#
# Returns: 1 if we found at least one properly set device, 0
# otherwise.
#----------------------------------------------------------------

sub test_devices () {

	my @good_devices;
	my @fields;
	my $target;
	my $device;
	my %seen;

	foreach $device (@ram_devices) {
		if (-e $device) {
			# Links are ok, if they link to a block device.
			-l $device && do {
				$debug && print STDOUT "xdramdisk(test_devices): DEBUG: 1st pass: $device is a link\n";
				$line=`ls -l $device`;
				chomp($line);
				@fields=split(/[ ]+/,$line);
				$target=$fields[10];
				$target !~ /\// && do {
					$target = "/dev/".$target;
				};
				$debug && print STDOUT "xdramdisk(test_devices): DEBUG: 1st pass: target of $device is $target\n";
				-b $target && do {
					push(@good_devices,$target);
					$debug && print STDOUT "xdramdisk(test_devices): DEBUG: 1st pass: kept $device in \@ram_devices\n";
					next;
				};
				$verbose && 
					print STDERR "xdramdisk: $device does not link to a block device: dropping.\n";
				next;
			};
			# Is it a block device?
			-b $device && do {
				push(@good_devices,$device);
				$debug && print STDOUT "xdramdisk(test_devices): DEBUG: 1st pass: kept $device in \@ram_devices\n";
				next;
			};
			$verbose && print STDERR "xdramdisk: $device is not a block device, nor does it link to one: dropping.\n";
		} else {
			$verbose && print STDERR "xdramdisk: $device does not exist: dropping.\n";
		}
	}

	# Uniquify @good_devices
	%seen = ();
	@ram_devices = ();
	foreach $device (@good_devices) {
		push(@ram_devices,$device) unless $seen{$device}++;
	}

	# Now test the permissions on the remaining devices
	@good_devices = ();
	foreach $device (@ram_devices) {
		-r $device || do {
			$verbose && print STDERR "xdramdisk: $device is not readable: dropping.\n";
			next;
		};
		-w $device || do {
			$verbose && print STDERR "xdramdisk: $device is not writable: dropping.\n";
			next;
		};
		push(@good_devices,$device);
		$debug && print STDOUT "xdramdisk(test_devices): DEBUG: 2nd pass: kept $device in \@ram_devices\n";
	}
	@ram_devices=@good_devices;

	if (@ram_devices) {
		$debug && print STDOUT "xdramdisk(test_devices): DEBUG: returning 1\n";
		return(1);
	} else {
		$debug && print STDOUT "xdramdisk(test_devices): DEBUG: returning 0\n";
		return(0);
	}

}

#----------------------------------------------------------------
# usage: $mountpoint=&is_mounted($device);
#
# Determine if the ram disk we want is already mounted. 
#
# References @ram_devices global variable.
#
# Returns:  "" if not, current mountpoint if so
# 
#----------------------------------------------------------------

sub is_mounted ($) {

	my $device = shift(@_);
	my @lines;
	my $user;
	my $x;
	my @fields;
	my @mountopts;

	@lines = split(/\n/,`/bin/mount`);
	chomp(@lines);

	$debug && print STDOUT "xdramdisk(is_mounted): checking $device\n";
	foreach $line (@lines) {
		$line =~ /$device/ && do {
			@fields=split(/[ ]+/,$line);
			$debug && 
				print STDOUT "xdramdisk(is_mounted): $device is mounted on ".$fields[2]."\n";

			# Remove the parentheses
			$fields[5]=~s/\(|\)//g;

			# Extract the user who mounted the ramdisk from the mount opts
			@mountopts=split(/,/ ,$fields[5]);
			foreach $x (@mountopts) {
				$x =~ /^user=/ && do {
					$x=~s/^user=//;
					$mounted_by=$x;
				};
			}
			$debug && 
				print STDOUT "xdramdisk(is_mounted): $device was mounted by $mounted_by\n";

			$mounted=1;
			return($fields[2]);
		};
	}
	$debug && print STDOUT "xdramdisk(is_mounted): $device is not mounted\n";
	return("");
}

#----------------------------------------------------------------
# usage: $retval=&check_mount;
#
# See if we can mount any of the devices in @ram_devices;
#
# References @ram_devices global variable.
# Sets $working_device global variable.
# Sets $mounted global variable.
#
# Returns: 1 if we can mount a device, 0 if not.
# 
#----------------------------------------------------------------
sub check_mount () {
	
	my $device;
	my $mp;
	my $user;

	foreach $device (@ram_devices) {

		($mp,$user)=&is_mounted($device);
		$mp && do {
			$mp =~ /$mountpoint/ || do {
				$verbose && 
					print STDOUT "xdramdisk(check_mount): $device is not mounted on $mountpoint: dropping\n";
				next;
			};
			$working_device=$device;
			$debug && print STDOUT "xdramdisk(check_mount): \$mounted is $mounted\n";
			return(1);
		};
		$working_device=$device;
		return(1);
	}
				
	$verbose &&
		print STDERR "xdramdisk: unable to find a device which was not already mounted!\n";	
	return(0);	
}
	


#----------------------------------------------------------------
# usage: $retval=&sanity;
#
# Make sure the computer is set up properly to do RAM disks in
# the way we want.
#
# Returns: 0 if sanity checks were passed, non-zero otherwise.
# 
# See header to this file for error code descriptions.
#----------------------------------------------------------------

sub sanity ($) {

	my $config_file=shift(@_);
	my @fields;

	# Is this Linux?  If not, this doesn't make sense.
	`/bin/uname` !~ /Linux/ && do {
		$verbose && print STDERR "xdramdisk: You're not running Linux!\n";
		return($ERR_OS);
	};

	# Does our config file exist?
	-f $config_file || do {
		$verbose && print STDERR "xdramdisk: $config_file doesn't exist!\n";
		return($ERR_NO_CONFIG);
	};

	# Is it readable by us?
	-r $config_file || do {
		$verbose && print STDERR "xdramdisk: $config_file isn't readable!\n";
		return($ERR_NO_CONFIG);
	};

	&get_ramdisk_size($config_file);

	# Does our desired mount point exist?
	-e $mountpoint || do {
		$verbose && print STDERR "xdramdisk: $mountpoint doesn't exist!\n";
		return($ERR_MOUNTPOINT);
	};

	# Is our desired mount point a directory?
	-d $mountpoint || do {
		$verbose && print STDERR "xdramdisk: $mountpoint is not a directory!\n";
		return($ERR_MOUNTPOINT);
	};

	# Check fstab, determining which devices are available 
	&parse_fstab || return($ERR_FSTAB);	

	# Check the devices we found in parse_fstab for validity
	&test_devices || return($ERR_DEVICES);	

	# See if we can mount any of the devices we discovered
	&check_mount || return($ERR_MOUNT);	
	
	# Does a temp file exist in /mnt/xdphys_disk?
	$mounted && do {
		if (-f "$mountpoint/$def_file") {
			$force || do {
				# If $force == 0, exit.
				$verbose && print STDERR "xdramdisk: $mountpoint/$def_file exists: not squashing it!\n";
				return($ERR_FILE_EXISTS);
			};
		}
		# Otherwise, umount
		$verbose && print STDERR "xdramdisk: unmounting $mountpoint\n";
		if ($mounted_by =~ /getlogin()/) {
			system("/bin/umount $mountpoint");
		} else {
			system("$sudo /bin/umount $mountpoint");
		}
		$mounted=0;	
		$mounted_by="";	
	};

	return($NOERR);
}

#----------------------------------------------------------------
# usage: $retval=&mount_ramdisk;
#
# Mount the damned RAM disk, already.
# 
# Returns: 1 for success, 0 for failure.
# 
#----------------------------------------------------------------
sub mount_ramdisk () {
	
	if ($debug) {
		$retval=system("/sbin/mke2fs -vm0 $working_device");
	} else {
		$retval=system("/sbin/mke2fs -vm0 $working_device > /dev/null 2>&1");
	}
	$retval && do {
		$verbose &&
			print STDERR "xdramdisk: failed to mke2fs on $working_device (retval=$retval)\n";	
		return(0);
	};
	$verbose &&
		print STDERR "xdramdisk: made fs on $working_device\n";	

	$retval=system("/bin/mount $working_device");
	$retval && do {
		$verbose &&
			print STDERR "xdramdisk: failed to mount $working_device (retval=$retval)\n";	
		return(0);
	};
	$verbose &&
		print STDERR "xdramdisk: mounted $working_device!\n";	

	return(1);
}
	
#----------------------------------------------------------------
# main
#----------------------------------------------------------------

# parse the command line args
&parse_cmdline;
# Find the home directory
$xdphysrc=&expand_tilde($xdphysrc);
# Perform sanity checks
$retval=&sanity($xdphysrc);
$debug && print STDOUT "xdramdisk: &sanity returned $retval\n";
$retval && exit($retval);

# At this point, we have a list of valid RAM disk devices
# We'll work with the first one, unless we have a problem
$debug && print STDOUT "xdramdisk: working_device is $working_device\n";

# And mount it ...
&mount_ramdisk || exit($ERR_MOUNT);

exit($NOERR);
