#!/usr/bin/perl

#
# cnid_maint: A script to maintain the consistency of CNID databases.
#
# $Id: cnid_maint,v 1.1.1.1 2004/04/15 02:32:18 louistsai Exp $
#

use strict;
use Getopt::Std;
use vars qw(
  $APPLE_VOLUMES_FILE
  $STOP_CMD
  $START_CMD
  $PS_CMD
  $GREP
  $DB_STAT
  $DB_RECOVER
  $DB_VERIFY
  $VERSION
  $START_NETATALK
  $LOCK_FILE
  $HOLDING_LOCK
);

## Edit ME
$STOP_CMD  = '/usr/local/etc/rc.d/netatalk.sh stop';
$START_CMD = '/usr/local/etc/rc.d/netatalk.sh start';

# This ps command needs to output the following fields in the following order:
# USER,PID,PPID,COMMAND
# Below is the example of a BSD ps.  A SYSV example is:
# /bin/ps -eflouid,pid,ppid,comm
$PS_CMD             = '/bin/ps -axouser,pid,ppid,command';
$DB_STAT            = 'bin/db_stat';
$DB_RECOVER         = 'bin/db_recover';
$DB_VERIFY          = 'bin/db_verify';
$APPLE_VOLUMES_FILE = '${prefix}/etc/netatalk/AppleVolumes.default';
## End edit section

$VERSION = '1.0';
$GREP           = '/bin/grep';
$START_NETATALK = 0;
$LOCK_FILE      = tmpdir() . '/cnid_maint.LOCK';
$HOLDING_LOCK   = 0;

sub LOCK_SH { 1 }
sub LOCK_EX { 2 }
sub LOCK_NB { 4 }
sub LOCK_UN { 8 }

my $opts        = {};
my $extra_safe  = 0;
my $do_verify   = 0;
my $remove_logs = 0;

getopts( 'hsvVl', $opts );

if ( $opts->{'v'} ) {
    version();
    exit(0);
}
if ( $opts->{'h'} ) {
    help();
    exit(0);
}
if ( $opts->{'s'} ) {
    $extra_safe = 1;
}
if ( $opts->{'V'} ) {
    $do_verify = 1;
}
if ( $opts->{'l'} ) {
    $remove_logs = 1;
}

if ( $< != 0 ) {
    die "You must be root to run this script.\n";
}

print "Beginning run of CNID DB Maintanence script at "
  . scalar(localtime) . ".\n\n";

if ( -f $LOCK_FILE ) {
    error( 1, "Lock file $LOCK_FILE exists." );
    end();
}

unless ( open( LOCK, ">" . $LOCK_FILE ) ) {
    error( 2, "Unable to create $LOCK_FILE: $!" );
}
flock( LOCK, LOCK_EX );
$HOLDING_LOCK = 1;

# Check to see if the AppleVolumes.default file exists.  We will use this file
# to get a list of database environments to recover.  We will ignore users'
# home directories since that could be a monumental under taking.
if ( !-f $APPLE_VOLUMES_FILE ) {
    error( 2, "Unable to locate $APPLE_VOLUMES_FILE" );
}

# Use ps to get a list of running afpds.  We will track all afpd PIDs that are
# running as root.
unless ( open( PS, $PS_CMD . " | $GREP afpd | $GREP -v grep |" ) ) {
    error( 2, "Unable to open a pipe to ps: $!" );
}

my $children  = 0;
my $processes = 0;
while (<PS>) {
    chomp;
    $processes++;
    my ( $user, $pid, $ppid, $command ) = split (/\s+/);
    if ( ( $user eq "root" && $ppid != 1 ) || ( $user ne "root" ) ) {
        $children++;
    }
}

close(PS);

if ( $children > 1 ) {

    # We have some children.  We cannot run recovery.
    error( 1,
"Clients are still connected.  Database recovery will not be run at this time."
    );
    end();
}

if ($processes) {

    # Shutdown the running afpds.
    $START_NETATALK = 1;
    error( 0, "Shutting down afpd process..." );
    error( 2, "Failed to shutdown afpd" )
      if system( $STOP_CMD . ">/dev/null 2>&1" );
}

# Now, we parse AppleVolumes.default to get a list of volumes to run recovery
# on.
unless ( open( VOLS, $APPLE_VOLUMES_FILE ) ) {
    error( 2, "Unable to open $APPLE_VOLUMES_FILE: $!" );
}

my @paths = ();
while (<VOLS>) {
    s/#.*//;
    s/^\s+//;
    s/\s+$//;
    next unless length;
    my ( $path, @options ) = split ( /\s+/, $_ );
    next if ( $path =~ /^~/ );
    my $option = "";
    foreach $option (@options) {

        # We need to check for the dbpath option on each volume.  If that
        # option is present, we should use its path instead of the actual
        # volume path.
        if ( $option =~ /^dbpath:/ ) {
            push @paths, $';
        }
        else {
            push @paths, $path;
        }
    }
}

close(VOLS);

my $path = "";
foreach $path (@paths) {
    my $dbpath = $path . "/.AppleDB";
    if ( !-d $dbpath ) {
        error( 1, "Database environment $dbpath does not exist" );
        next;
    }
    if ($extra_safe) {
        error( 0,
            "Checking database environment $dbpath for open connections..." );
        unless ( open( STAT, $DB_STAT . " -h $dbpath -e |" ) ) {
            error( 1, "Failed to open a pipe to $DB_STAT: $!" );
            next;
        }

        # Now, check each DB environment for any open connections (db_stat calls 
        # them as references).  If a volume has no references, we can do
        # recovery on it.  Only check this option if the user wants to play 
        # things extra safe.
        my $connections = 0;
        while (<STAT>) {
            chomp;
            s/\s//g;
            if (/References\.$/) {
                $connections = $`;
                last;
            }
        }

        close(STAT);

        # Print out two different skip messages.  This is just for anality.
        if ( $connections == 1 ) {
            error( 1, "Skipping $dbpath since it has one active connection" );
            next;
        }

        if ( $connections > 0 ) {
            error( 1,
                "Skipping $dbpath since it has $connections active connections"
            );
            next;
        }
    }

    # Run the db_recover command on the environment.
    error( 0, "Running db_recover on $dbpath" );
    if ( system( $DB_RECOVER . " -h $dbpath >/dev/null 2>&1" ) ) {
        error( 1, "Failed to run db_recover on $dbpath" );
        next;
    }

    if ($do_verify) {
        error( 0, "Verifying $dbpath/cnid.db" );
        if ( system( $DB_VERIFY . " -q -h $dbpath cnid.db" ) ) {
            error( 1, "Verification of $dbpath/cnid.db failed" );
            next;
        }

        error( 0, "Verifying $dbpath/devino.db" );
        if ( system( $DB_VERIFY . " -q -h $dbpath devino.db" ) ) {
            error( 1, "Verification of $dbpath/devino.db failed" );
            next;
        }

        error( 0, "Verifying $dbpath/didname.db" );
        if ( system( $DB_VERIFY . " -q -h $dbpath didname.db" ) ) {
            error( 1, "Verification of $dbpath/didname.db failed" );
            next;
        }
    }

    if ($remove_logs) {

        # Remove the log files if told to do so.
        unless ( opendir( DIR, $dbpath ) ) {
            error( 1, "Failed to open $dbpath for browsing: $!" );
            next;
        }

        my $file = "";
        while ( defined( $file = readdir(DIR) ) ) {
            if ( $file =~ /^log\.\d+$/ ) {
                error( 0, "Removing $dbpath/$file" );
                unless ( unlink( $dbpath . "/" . $file ) ) {
                    error( 1, "Failed to remove $dbpath/$file: $!" );
                    next;
                }
            }
        }

        closedir(DIR);
    }

}

end();

sub tmpdir {
    my $tmpdir;

    foreach ( $ENV{TMPDIR}, "/tmp" ) {
        next unless defined && -d && -w _;
        $tmpdir = $_;
        last;
    }
    $tmpdir = '' unless defined $tmpdir;
    return $tmpdir;
}

sub error {
    my ( $code, $msg ) = @_;

    my $err_types = {
        0 => "INFO",
        1 => "WARNING",
        2 => "ERROR",
    };

    print $err_types->{$code} . ": " . $msg . "\n";

    end() if ( $code == 2 );
}

sub end {
    if ($START_NETATALK) {
        error( 0, "Restarting Netatalk" );
        if ( system( $START_CMD . " >/dev/null 2>&1" ) ) {
            print "ERROR: Failed to restart Netatalk\n";
        }
    }
    if ($HOLDING_LOCK) {
        close(LOCK);
        unlink($LOCK_FILE);
    }
    print "\nRun of CNID DB Maintenance script ended at "
      . scalar(localtime) . ".\n";
    exit(0);
}

sub version {
    print "$0 version $VERSION\n";
}

sub help {
    print "usage: $0 [-hlsvV]\n";
    print "\t-h   view this message\n";
    print "\t-l   remove transaction logs after running recovery\n";
    print
      "\t-s   be extra safe in verifying there are no open DB connections\n";
    print "\t-v   print version and exit\n";
    print "\t-V   run a verification on all database files after recovery\n";
}
