#
# $Id: Base.pm,v 2.2 2003/09/02 21:20:58 wpm Exp $
#
# (c) 2003 Morgan Stanley and Co.
# See ..../src/LICENSE for terms of distribution.
#

package AFS::Command::Base;

require 5.6.0;

use strict;
use English;
use Carp;
use File::Basename qw(basename);

use IO::File;
use IO::Pipe;

our $AUTOLOAD	= "";
our $VERSION = '1.1';

our %Carp =
  (
   carp		=> \&Carp::carp,
   croak	=> \&Carp::croak,
  );

sub setCarp {

    my $class = shift;
    my (%args) = @_;

    foreach my $key ( keys %args ) {
	unless ( $Carp{$key} ) {
	    croak("Unsupported argument: '$key'");
	}
	unless ( ref $args{$key} eq 'CODE' ) {
	    croak("Not a code reference: '$args{$key}'");
	}
	$Carp{$key} = $args{$key};
    }

    return AFS::Object->_setCarp(@_);

}

sub new {

    my $proto = shift;
    my $class = ref($proto) || $proto;
    my %args = @_;

    my $self = {};

    foreach my $key ( qw( localtime noauth localauth encrypt quiet ) ) {
	$self->{$key}++ if $args{$key};
    }

    # AFS::Command::VOS -> vos
    $self->{command} = $args{command} || lc((split(/::/,$class))[2]);

    bless $self, $class;

    return $self;

}

sub errors {
    my $self = shift;
    return $self->{errors};
}

sub supportsOperation {
    my $self = shift;
    my $operation = shift;
    return $self->_operations($operation);
}

sub supportsArgument {
    my $self = shift;
    my $operation = shift;
    my $argument = shift;
    return unless $self->_operations($operation);
    return unless $self->_arguments($operation);
    return exists $self->{_arguments}->{$operation}->{$argument}
}

sub _Carp {
    my $self = shift;
    $Carp{carp}->(@_);
}

sub _Croak {
    my $self = shift;
    $Carp{croak}->(@_);
}

sub _operations {

    my $self = shift;
    my $operation = shift;

    my $class = ref $self;

    unless ( $self->{_operations} ) {

	my %operations = ();

	my $pipe = IO::Pipe->new() || do {
	    $self->_Carp("Unable to create pipe: $ERRNO\n");
	    return;
	};

	my $pid = fork();

	unless ( defined $pid ) {
	    $self->_Carp("Unable to fork: $ERRNO\n");
	    return;
	}

	if ( $pid == 0 ) {

	    STDOUT->fdopen( $pipe->writer()->fileno(), "w" ) || do {
		$self->_Croak("Unable to redirect stdout: $ERRNO\n");
	    };

	    exec $self->{command}, 'help';
	    die "Unable to exec $self->{command} help: $ERRNO\n";

	} else {
	    $pipe->reader();
	    while ( defined($_ = $pipe->getline()) ) {
		next if /Commands are:/;
		my ($command) = split;
		next if $command =~ /^(apropos|help)$/;
		$operations{$command}++;
	    }
	}

	unless ( waitpid($pid,0) ) {
	    $self->_Carp("Unable to get status of child process ($pid)");
	    return;
	}

	if ( $? ) {
	    $self->_Carp("Error running $self->{command} help.  Unable to configure $class");
	    return;
	}

	$self->{_operations} = \%operations;

    }

    return $self->{_operations}->{$operation};

}

sub _arguments {

    my $self		= shift;
    my $operation 	= shift;

    my $arguments =
      {
       optional		=> {},
       required		=> {},
      };

    my $command = $self->{command};

    unless ( $self->_operations($operation) ) {
	$self->_Carp("Unsupported $command operation '$operation'\n");
	return;
    }

    return $self->{_arguments}->{$operation}
      if ref $self->{_arguments}->{$operation} eq 'HASH';

    my $pipe = IO::Pipe->new() || do {
	$self->_Carp("Unable to create pipe: $ERRNO");
	return;
    };

    my $pid = fork();

    my $errors = 0;

    unless ( defined $pid ) {
	$self->_Carp("Unable to fork: $ERRNO");
	return;
    }

    if ( $pid == 0 ) {

	STDOUT->fdopen( $pipe->writer()->fileno(), "w" ) ||
	  die "Unable to redirect stdout: $ERRNO\n";
	exec $command, 'help', $operation;
	die "Unable to exec $command help $operation: $ERRNO\n";

    } else {

	$pipe->reader();

	while ( <$pipe> ) {

	    next unless s/^Usage:.*$command\s+$operation\s+//;

	    while ( $_ ) {
		if ( s/^\[\s*-(\w+?)\s*\]\s*//  ) {
		    $arguments->{optional}->{$1} = 0
		      unless $1 eq 'help'; # Yeah, skip it...
		} elsif ( s/^\[\s*-(\w+?)\s+<[^>]*?>\+\s*]\s*// ) {
		    $arguments->{optional}->{$1} = [];
		} elsif ( s/^\[\s*-(\w+?)\s+<[^>]*?>\s*]\s*// ) {
		    $arguments->{optional}->{$1} = 1;
		} elsif ( s/^\s*-(\w+?)\s+<[^>]*?>\+\s*// ) {
		    $arguments->{required}->{$1} = [];
		} elsif ( s/^\s*-(\w+?)\s+<[^>]*?>\s*// ) {
		    $arguments->{required}->{$1} = 1;
		} elsif ( s/^\s*-(\w+?)\s*// ) {
		    $arguments->{required}->{$1} = 0;
		} else {
		    $self->_Carp("Unable to parse $command help for $operation\n" .
				 "Unrecognized string: '$_'");
		    $errors++;
		    last;
		}
	    }

	    last;

	}

    }

    unless ( waitpid($pid,0) ) {
	$self->_Carp("Unable to get status of child process ($pid)");
	$errors++;
    }

    if ( $? ) {
	$self->_Carp("Error running $command help $operation.  Unable to configure $command $operation");
	$errors++;
    }

    return if $errors;
    return $self->{_arguments}->{$operation} = $arguments;

}

sub _save_stderr {

    my $self = shift;

    $self->{olderr} = IO::File->new(">&STDERR") || do {
	$self->_Carp("Unable to dup stderr: $ERRNO");
	return;
    };

    my $command = basename($self->{command});

    $self->{tmpfile} = "/tmp/.$command.$self->{operation}.$$";

    my $newerr = IO::File->new(">$self->{tmpfile}") || do {
	$self->_Carp("Unable to open $self->{tmpfile}: $ERRNO");
	return;
    };

    STDERR->fdopen( $newerr->fileno(), "w" ) || do {
	$self->_Carp("Unable to reopen stderr: $ERRNO");
	return;
    };

    $newerr->close() || do {
	$self->_Carp("Unable to close $self->{tmpfile}: $ERRNO");
	return;
    };

    return 1;

}

sub _restore_stderr {

    my $self = shift;

    STDERR->fdopen( $self->{olderr}->fileno(), "w") || do {
	$self->_Carp("Unable to restore stderr: $ERRNO");
	return;
    };

    $self->{olderr}->close() || do {
	$self->_Carp("Unable to close saved stderr: $ERRNO");
	return;
    };

    delete $self->{olderr};

    my $newerr = IO::File->new($self->{tmpfile}) || do {
	$self->_Carp("Unable to reopen $self->{tmpfile}: $ERRNO");
	return;
    };

    $self->{errors} = "";

    while ( <$newerr> ) {
	$self->{errors} .= $_;
    }

    $newerr->close() || do {
	$self->_Carp("Unable to close $self->{tmpfile}: $ERRNO");
	return;
    };

    unlink($self->{tmpfile}) || do {
	$self->_Carp("Unable to unlink $self->{tmpfile}: $ERRNO");
	return;
    };

    delete $self->{tmpfile};

    return 1;

}

sub _parse_arguments {

    my $self = shift;
    my $class = ref($self);
    my (%args) = @_;

    my $arguments = $self->_arguments($self->{operation});

    unless ( defined $arguments ) {
	$self->_Carp("Unable to obtain arguments for $class->$self->{operation}");
	return;
    }

    $self->{errors} = "";

    $self->{cmds} = [];

    if ( $args{inputfile} ) {

	push( @{$self->{cmds}}, [ 'cat', $args{inputfile} ] );

    } else {

	my @argv = ( $self->{command}, $self->{operation} );

	foreach my $key ( qw( noauth localauth encrypt ) ) {
	    next unless $self->{$key};
	    $args{$key}++ if exists $arguments->{required}->{$key};
	    $args{$key}++ if exists $arguments->{optional}->{$key};
	}

	unless ( $self->{quiet} ) {
	    $args{verbose}++ if exists $arguments->{optional}->{verbose};
	}

	foreach my $type ( qw( required optional ) ) {

	    foreach my $key ( keys %{$arguments->{$type}} ) {

		my $hasvalue = $arguments->{$type}->{$key};

		if ( $type eq 'required' ) {
		    unless ( exists $args{$key} ) {
			$self->_Carp("Required argument '$key' not provided");
			return;
		    }
		} else {
		    next unless exists $args{$key};
		}

		push(@argv,"-$key");

		if ( $hasvalue ) {
		    if ( ref $args{$key} eq 'HASH' || ref $args{$key} eq 'ARRAY' ) {
			unless ( ref $hasvalue eq 'ARRAY' ) {
			    $self->_Carp("Invalid argument '$key': can't provide a list of values");
			    return;
			}
			foreach my $value ( ref $args{$key} eq 'HASH' ? %{$args{$key}} : @{$args{$key}} ) {
			    push(@argv,$value);
			}
		    } else {
			push(@argv,$args{$key});
		    }
		}

		delete $args{$key};

	    }

	}

	if ( %args ) {
	    $self->_Carp("Unsupported arguments: " . join(' ',sort keys %args));
	    return;
	}

	push( @{$self->{cmds}}, \@argv );

    }

    return 1;

}

sub _exec_cmds {

    my $self = shift;

    my %args = @_;

    my @cmds = @{$self->{cmds}};

    $self->{pids} = {};

    for ( my $index = 0 ; $index <= $#cmds ; $index++ ) {

	my $cmd = $cmds[$index];

	my $pipe = IO::Pipe->new() || do {
	    $self->_Carp("Unable to create pipe: $ERRNO");
	    return;
	};

	my $pid = fork();

	unless ( defined $pid ) {
	    $self->_Carp("Unable to fork: $ERRNO");
	    return;
	}

	if ( $pid == 0 ) {

	    if ( $index == $#cmds &&
		 exists $args{stdout} && $args{stdout} ne 'stdout' ) {
		my $stdout = IO::File->new(">$args{stdout}") ||
		  $self->_Croak("Unable to open $args{stdout}: $ERRNO");
		STDOUT->fdopen( $stdout->fileno(), "w" ) ||
		  $self->_Croak("Unable to redirect stdout: $ERRNO");
	    } else {
		STDOUT->fdopen( $pipe->writer()->fileno(), "w" ) ||
		  $self->_Croak("Unable to redirect stdout: $ERRNO");
	    }

	    if ( exists $args{stderr} && $args{stderr} eq 'stdout' ) {
		STDERR->fdopen( STDOUT->fileno(), "w" ) ||
		  $self->_Croak("Unable to redirect stderr: $ERRNO");
	    }

	    if ( $index == 0 ) {
		if ( exists $args{stdin} && $args{stdin} ne 'stdin' ) {
		    my $stdin = IO::File->new("<$args{stdin}") ||
		      $self->_Croak("Unable to open $args{stdin}: $ERRNO");
		    STDIN->fdopen( $stdin->fileno(), "r" ) ||
		      $self->_Croak("Unable to redirect stdin: $ERRNO");
		}
	    } else {
		STDIN->fdopen( $self->{handle}->fileno(), "r" ) ||
		  $self->_Croak("Unable to redirect stdin: $ERRNO");
	    }

	    $ENV{TZ} = 'GMT' unless $self->{localtime};

	    exec( { $cmd->[0] } @{$cmd} ) ||
	      $self->_Croak("Unable to exec @{$cmd}: $ERRNO");

	}

	$self->{handle} = $pipe->reader();

	$self->{pids}->{$pid} = $cmd;

    }

    return 1;

}

sub _parse_output {

    my $self = shift;

    $self->{errors} = "";

    while ( defined($_ = $self->{handle}->getline()) ) {
	$self->{errors} .= $_;
    }

    return 1;

}

sub _reap_cmds {

    my $self = shift;
    my (%args) = @_;

    my $errors = 0;

    $self->{handle}->close() || do {
	$self->_Carp("Unable to close pipe handle: $ERRNO");
	$errors++;
    };

    delete $self->{handle};
    delete $self->{cmds};

    $self->{status} = {};

    my %allowstatus = ();
    if ( $args{allowstatus} ) {
	if ( ref $args{allowstatus} eq 'ARRAY' ) {
	    foreach my $status ( @{$args{allowstatus}} ) {
		$allowstatus{$status}++;
	    }
	} else {
	    $allowstatus{$args{allowstatus}}++;
	}
    }

    foreach my $pid ( keys %{$self->{pids}} ) {

	$self->{status}->{$pid}->{cmd} =
	  join(' ', @{delete $self->{pids}->{$pid}} );

	if ( waitpid($pid,0) ) {

	    $self->{status}->{$pid}->{status} = $?;
	    if ( $? ) {
		if ( %allowstatus ) {
		    $errors++ unless $allowstatus{$? >> 8};
		} else {
		    $errors++;
		}
	    }


	} else {
	    $self->{status}->{$pid}->{status} = undef;
	    $errors++;
	}

    }

    return if $errors;
    return 1;

}

sub AUTOLOAD {

    my $self = shift;
    my (%args) = @_;

    $self->{operation} = $AUTOLOAD;
    $self->{operation} =~ s/.*:://;

    return unless $self->_parse_arguments(%args);

    return unless $self->_exec_cmds( stderr => 'stdout' );

    my $errors = 0;

    $errors++ unless $self->_parse_output();
    $errors++ unless $self->_reap_cmds();

    return if $errors;
    return 1;

}

sub DESTROY {}

1;

