AFS-Command

 view release on metacpan or  search on metacpan

lib/AFS/Command/Base.pm  view on Meta::CPAN

#
# $Id$
#
# (c) 2003-2004 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 Date::Format;

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

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

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 timestamps ) ) {
	$self->{$key}++ if $args{$key};
    }

    # AFS::Command::VOS -> vos
    if ( $args{command} ) {
        my @commands = (split /\s+/,$args{command});
        push (@{$self->{command}},@commands);
    } else {
        @{$self->{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 = ();

	#
	# This hack is necessary to support the offline/online "hidden"
	# vos commands.  These won't show up in the normal help output,
	# so we have to check for them individually.  Since offline and
	# online are implemented as a pair, we can just check one of
	# them, and assume the other is there, too.
	#

	foreach my $type ( qw(default hidden) ) {

	    if ( $type eq 'hidden' ) {
		next unless $self->isa("AFS::Command::VOS");
	    }

	    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 ) {

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

		if ( $type eq 'default' ) {
		    exec @{$self->{command}}, 'help';
		} else {
		    exec @{$self->{command}}, 'offline', '-help';
		}
		die "Unable to exec @{$self->{command}} help: $ERRNO\n";

	    } else {

		$pipe->reader();

		while ( defined($_ = $pipe->getline()) ) {
		    if ( $type eq 'default' ) {
			next if /Commands are:/;
			my ($command) = split;
			next if $command =~ /^(apropos|help)$/;
			$operations{$command}++;
		    } else {
			if ( /^Usage:/ ) {
			    $operations{offline}++;
			    $operations{online}++;
			}
		    }
		}

	    }

	    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		=> {},
       aliases		=> {},
      };

    my @command;
    push (@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 ) {

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

    } else {

	$pipe->reader();

	while ( <$pipe> ) {

	    if ( /Unrecognized operation '$operation'/ ) {
		$self->_Carp("Unsupported @command operation '$operation'\n");
		$errors++;
		last;
	    }

	    next unless s/^Usage:.*\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;

	}

    }

    #
    # XXX -- Hack Alert!!!
    #
    # Because some asshole decided to change the force option to vos
    # release from -f to -force, you can't use the API tranparently
    # with 2 different vos binaries that support the 2 different options.
    #
    # If we need more of these, we can add them, as this let's us
    # alias one argument to another.
    #
    if ( $self->isa("AFS::Command::VOS") && $operation eq 'release' ) {
	if ( exists $arguments->{optional}->{f} ) {
	    $arguments->{aliases}->{force} = 'f';
	} elsif ( exists $arguments->{optional}->{force} ) {
	    $arguments->{aliases}->{f} = 'force';
	}
    }

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

    if ( $? ) {
	$self->_Carp("Error running @command $operation -help.  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((split /\s+/,@{$self->{command}})[0]);

    $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 ( keys %args ) {
	    next unless $arguments->{aliases}->{$key};
	    $args{$arguments->{aliases}->{$key}} = delete $args{$key};
	}

	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};
		}

		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;
			}
			push(@argv,"-$key");
			foreach my $value ( ref $args{$key} eq 'HASH' ? %{$args{$key}} : @{$args{$key}} ) {
			    push(@argv,$value);
			}
		    } else {
			push(@argv,"-$key",$args{$key});
		    }
		} else {
		    push(@argv,"-$key") if $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} .= time2str("[%Y-%m-%d %H:%M:%S] ",time,'GMT') if $self->{timestamps};
	$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;



( run in 0.240 second using v1.01-cache-2.11-cpan-4d50c553e7e )