#! /usr/bin/perl
# name: /etc/dev.d/default/hotplug-mount.pl
# version: 0.0.1
# purpose: automatic handling of an autofs map for
#          hotpluggable mass storage devices
#          under kernel 2.6 with udev
# This script is automatically called by udevd at each plug/unplug
# event of an usb device.
# It adds a line in /etc/auto.hotplug for each block device found,
# with a pretty name as mountpoint. (based on label or info read from /sys)
# it takes three environment variables as input : ACTION, DEVNAME and DEVPATH
# These variables are provided by udevd
# FIXME: this script is still in development, so don't rely too much
# on its current features, they might change
##################################################################
# rewrite in Perl:
# Copyright (C) 2004 Thomas Jahns, Thomas dot Jahns at gmx dot net
# original shell version:
# Copyright (C) 2004 Christophe Combelles (ccomb@free.fr)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
##################################################################

use strict;
use warnings;
use diagnostics;

use Data::Dumper ();
use Fcntl qw(:DEFAULT :flock);
use File::Basename ();
use POSIX;
use Sys::Syslog ();

# to debug this script, uncomment the line my $debug=1
# and see /tmp/hotplug-mount.pl.debug after execution
my $debug=1;

# FIXME: general settings, should be read from configuration file later
my $mapfile='/etc/auto.hotplug';
my $mapdir='/media/hotplug';
my $markertag='# hotplug-mount';


# exit immediately if /usr/bin/ is not yet available
# (during boot if /usr is a separate partition)
exit unless -d '/usr/bin';

my $debugout='/tmp/'. File::Basename::basename($0) . '.debug.' . $$;
open(DEBUGOUT, '>', $debugout) or die('Failed to open debug output.',
                                    "\n", $!);
if($debug and !$ARGV[2])
{
    print(DEBUGOUT "executing $0 $@",
          "with the following environment variables:", "\n",
          join("\n", map { $_ . "=" . $ENV{$_} } (keys %ENV)), "\n",
          '----' ,"\n");
    open(STDOUT, ">&DEBUGOUT");
    open(STDERR, ">&DEBUGOUT");
}

my ($action, $devname, $devpath, $subsystem)
    =($ENV{'ACTION'}, $ENV{'DEVNAME'},
      $ENV{'DEVPATH'}, $ENV{'SUBSYSTEM'});

$action='' if(!defined($action));
$devname='' if(!defined($devname));
$devpath='' if(!defined($devpath));
$subsystem='' if(!defined($subsystem));

# we only manage block devices
exit if($ARGV[0] ne 'block');

# we only manage usb_storage and ieee1394 sbp2 devices
if($action eq 'add')
{
    my $devfamily='';
    opendir(DEVDIR, '/sys' . $devpath . '/../device/../../../')
        or exit;
    my $device;
  DEVICE_DIR:
    while(defined($device=readdir(DEVDIR)))
    {
        if($device =~ /:/)
        {
            $devfamily='usb-storage';
            last DEVICE_DIR;
        }
        elsif($device =~ /[0-9a-f]{16}-[0-9]/)
        {
            $devfamily='ieee1394-sbp2';
            last DEVICE_DIR;
        }
    }
    sleep(5);
    closedir(DEVDIR);
    print(STDERR 'computed devicename: ', $device, "\n",
          'guessed device family: ', $devfamily, "\n") if $debug;
    unless(defined($device) and $device
           and (( $devfamily eq 'usb-storage'
                  and -e '/sys/bus/usb/drivers/usb-storage/' . $device)
                or ( $devfamily eq 'ieee1394-sbp2'
                     and -e '/sys/bus/ieee1394/drivers/sbp2/' . $device)))
    {
        print(STDERR 'Device ', $device,
              ' is neither usb-storage nor ieee1394',
              ' sbp2 device.', "\n");
        exit;
    }
}

# functions for syslog
Sys::Syslog::setlogsock(['unix', 'tcp', 'udp', 'stream', 'console']);
Sys::Syslog::openlog(File::Basename::basename($^X),
                     'pid ndelay', 'LOG_USER');

# unlock when exiting (should happen automatically, but better safe
# than sorry)
my $map_locked=0;
sub cleanquit()
{
    flock(MAP, LOCK_UN) if($map_locked);
    Sys::Syslog::closelog();
}

END {
    cleanquit();
}
use sigtrap qw(die INT QUIT TERM HUP);

sub write_syslog(@)
{
    Sys::Syslog::syslog('notice', join(' ', @_));
}

# mapfile is a static autofs map that will be modified by this script
open(MAP, '+<', $mapfile)
    or die('Mapfile ', $mapfile, ' not accessible: ', $!, "\n");
flock(MAP, LOCK_EX)
    or die('Failed to lock ', $mapfile, ': ', $!, "\n");
$map_locked=1;
my %mapentries;
my @maprewrite;
while(<MAP>)
{
    push(@maprewrite, $_);
    if(!/^\#/ and
       /^(\S+)\s+(\S+)\s+(\S+)$/)
    {
        die('Duplicate map entry! ', $1, "\n")
            if(exists($mapentries{$1}));
        my $comment=(!defined($4))?'':$4;
        $mapentries{$1} = { 'options'=> $2, 'location' => $3,
                            'lineno' => $#maprewrite
                            }
    }
}
if($debug)
{
    print(STDERR Data::Dumper::Dumper(\%mapentries, \@maprewrite), "\n");
}

# the following block prevents duplicate mounts of devices
# already in /etc/fstab
{
    open(FSTAB, '<', '/etc/fstab')
        or die('/etc/fstab not accessible: ', $!, "\n");
    my %fstabentries;
    while(<FSTAB>)
    {
        if(!/^\s*\#/ and
           /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/)
        {
            die('Duplicate fstab entry! ', $2, "\n")
                if(exists($fstabentries{$2}));
            $fstabentries{$2} = { 'options'=> $4, 'location' => $1,
                                  'fstype' => $3, 'freq'=> $5,
                                  'passno'=> $6 }
        }
    }
    if($debug)
    {
        print(STDERR Data::Dumper::Dumper(\%fstabentries), "\n");
    }
    close(FSTAB);
    foreach (keys %fstabentries)
    {
        # don't try to mount devices already in fstab
        exit if($fstabentries{$_}{'location'} eq $devname);
    }
}

# we need DEVPATH, and ACTION, and (DEVNAME or SUBSYSTEM)so we warn the user 
# that executes this script by hand
if(!$devname and !$subsystem or !$devpath or !$action)
{
  print("\n",
        'This script must be called by udevd because it needs the',
        ' following', "\n",
        ' environment variables: DEVPATH, DEVNAME, ACTION', "\n",
        'So you must copy this script as', 
        ' /etc/dev.d/default/hotplug-mount.dev', "\n",
	'and set its mode to executable for root', "\n",
        'See: ',
        'http://www.kernel.org/pub/linux/utils/kernel/hotplug/RFC-dev.d',
        "\n", "\n");
  exit;
}
#------------------------------------REMOVE----------------------------------
# if $DEVPATH/device exists, we are a device, not a partition, so exit
exit if -d '/sys'.$devpath.'/device';

# Since udev 0.46, it seems there is no more DEVNAME at removal.
$devname='/dev/' . File::Basename::basename($devpath)
    if(! $devname);
# remove the fstab entry and the mountpoint if they already exist
 MAP_ENTRY:
    foreach my $entry (keys %mapentries) {
        if($mapentries{$entry}{'location'} eq ':'.$devname
           and $maprewrite[$mapentries{$entry}{'lineno'}-1] eq
           $markertag . "\n")
        {
            my ($mntpoint, $options, $location)=
                $entry =~ /^($devname)\s+(?:(.*)\s+)?\S+$/;
            open(MOUNTS, '<', '/proc/mounts')
                or die('/proc/mounts unavailable: ', $!);
          ACTIVE_MOUNT:
            while(<MOUNTS>)
            {
                my ($dev, $mnt, $fs, $options, undef, undef)
                    = /^(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$/;
                if($devname eq $dev)
                {
                    write_syslog('Now removing an active filesystem,',
                                 'fasten your seatbelts!');
                    my $restart_fam=
                        `fuser -v -m $mntpoint` =~ / famd$/m;
                    system('fuser', '-k', $mntpoint);
                    system('fuser', '-k', '-9', $mntpoint);
                    system('/etc/init.d/fam', 'restart')
                        if($restart_fam and -x '/etc/init.d/fam');
                    last ACTIVE_MOUNT;
                }
            }
            close(MOUNTS);
            # remove the map entry corresponding to the device
            foreach my $otherentry (keys %mapentries)
            {
                $mapentries{$otherentry}{'lineno'}-=2
                if($mapentries{$otherentry}{'lineno'}
                   > $mapentries{$entry}{'lineno'});
            }
            splice(@maprewrite, $mapentries{$entry}{'lineno'}-1, 2);
            delete($mapentries{$entry});
            write_syslog('Removed', $devname, 'from', $mapfile);
        }
        
    }

if($debug)
{
    print(STDERR Data::Dumper::Dumper(\%mapentries, \@maprewrite), "\n");
}

#------------------------------------ADD----------------------------------
# if the current device is being added
if($action eq 'add')
{
    # get partition information
    my @diskid;
    open(DISKTYPE, 'disktype ' . $devname . '|')
        or die('Can\'t execute disktype: ', $!);
    push(@diskid, $_) while(<DISKTYPE>);
    close(DISKTYPE)
        or die('can\'t close disktype: ', $!);
    die('disktype failed!') if($?);
    my ($key, $options);
    my ($unix_options)=('noexec,nodev,nosuid,noatime');
    if($debug)
    {
        print(STDERR @diskid);
    }
    shift @diskid if(defined($diskid[0]) and $diskid[0] eq "\n");
    shift @diskid if(defined($diskid[0])
                     and $diskid[0] eq '--- ' . $devname . "\n");
    shift @diskid if(defined($diskid[0])
                     and $diskid[0] =~ /^Block device, size.*$/);

    # %volume_attributes only contains parsed content of the disktype
    # output for $devname
    my %volume_attributes;
    foreach (@diskid[0 .. $#diskid])
    {
        if(/^  (Volume name)\s+\"([^\"]*)\"/)
        { $volume_attributes{$1}=$2 }
        elsif(/^  (Volume size)\s+(.*)$/)
        { $volume_attributes{$1}=$2 }
        elsif(/^  (UUID)\s+(\S+)(?: (.*))?$/)
        { $volume_attributes{$1}=[ $2, $3 ] }
    }
    $volume_attributes{'Volume name'}=''
        if(!defined($volume_attributes{'Volume name'}));
    if($diskid[0] =~ /^DOS partition map/)
    {
        # if we are an extended or some other non-mountable
        # partition, just exit
        exit(0);
    }
    elsif($diskid[0] =~ /^FAT(?:32|16|12) file system/)
    {
        # FIXME: the static group assignment must be replaced with 
        #        a way to identify the currently logged in user
        #        (possibly the one that has been added to group
        #         floppy or something)
        # write entry for (v)fat style file systems
        $options='-fstype=vfat,noexec,uid=1000,gid=1000,fmask=133,dmask=022';
        $options.=',iocharset=utf8';
    }
    elsif($diskid[0] =~ /^XFS file system/)
    {
        $options='-fstype=xfs,'.$unix_options;
    }
    elsif($diskid[0] =~ /^Ext2 file system/)
    {
        $options='-fstype=ext2,'.$unix_options;
    }
    else
    {
        write_syslog('Unhandled filesystem type:',
                     (defined($diskid[0]) and $diskid[0])?
                     $diskid[0]:'undef', 'on',
                     'or read failure on',
                     $devname.'.');
        write_syslog('Please send bug report to hotplug-mount.pl',
                     'maintainer unless type equals undef.');
        exit;
    }
    my $label=$volume_attributes{'Volume name'};
    if(!$label)
    {
        my $partition='/sys' . $devpath .'/../device/../../..';
        my ($product, $manufacturer);
        if(-e $partition . '/product')
        {
            open(PROD, '<', $partition . '/product');
            $product = <PROD>;
            close(PROD);
        }
        if($product) { $label=$product; }
        elsif(-e $partition . '/manufacturer')
        {
            open(MANU, '<', $partition . '/manufacturer');
            $manufacturer = <MANU>;
            close(MANU);
        }
        if($manufacturer) { $label = $manufacturer; }
    }
    # build a mountpoint name = label (or usb-disk if label is empty)
    $label =~ s/[ \/]//g;
    $label =~ tr/,/_/;
    chomp($label);
    if($label) { $key=$label; }
    else { $key='usb-disk'; }
    if(exists($mapentries{$key}))
    {
        my $num_append=0;
        # if the mount point is already used in the map, append a number
        ++$num_append while(exists($mapentries{$key.$num_append}));
        $key.=$num_append;
    }

#   # if we are a FAT or NTFS, set the charset to utf8
#   if echo $FILETYPE | grep -q "NTFS"; then IOCHARSET=",iocharset=utf8"; fi
print(STDERR 'key: "', $key, "\"\n",
    'options: ', $options, "\n",
    'devname: ', $devname, "\n") if($debug);
    push(@maprewrite, '# hotplug-mount' . "\n",
         join("\t", $key, $options, ':'.$devname) . "\n");
    write_syslog('Added', $devname, 'to', $mapfile);
}

# rewrite map
seek(MAP, 0, SEEK_SET);
truncate(MAP, 0);
print(MAP @maprewrite);
# signal automount process
{
    my $pidfile;
    ($pidfile=$mapdir)=~ tr:/:_:;
    $pidfile='/var/run/autofs/'.$pidfile.'.pid';
    local *AUTOFSPID;
    open(AUTOFSPID, '<', $pidfile)
        or die('Can\'t open automount pid file: ', $pidfile, "\n",
               $!, "\n");
    my $pid=<AUTOFSPID>;
    close(AUTOFSPID);
    chomp($pid);
    unless(defined($pid) and $pid and $pid =~ /^[0-9]+$/)
    {
        die('Can\'t read automount pid: ', $pid, "\n",
               $!, "\n");
    }
    print(STDERR 'kill SIGHUP ', $pid, "\n") if($debug);
    kill(SIGHUP, $pid);
}
    


