The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Archer::Shell;
use strict;
use warnings;
use Net::SSH;
use Term::ReadLine;
use POSIX;
use File::HomeDir;
use Path::Class;
use List::MoreUtils qw/uniq/;

sub new {
    my ( $class, $args ) = @_;

    return bless {%$args}, $class;
}

sub run_loop {
    my ( $self, ) = @_;

    # initialize parallel manager.
    $self->{parallel} = $self->{context}->{config}->{global}->{parallel}
        || 'Archer::Parallel::ForkManager';
    $self->{parallel}->use or die $@;

    # initialize readline library.
    my $term = Term::ReadLine->new('Archer');

    my $HISTFILE = file( File::HomeDir->my_home, "/.archer_shell_history" );
    my $HISTSIZE = 256;

   # this won't work with Term::ReadLine::Perl
   # If there is Term::ReadLine::Gnu, be sure to do : export "PERL_RL=Gnu o=0"
    eval { $term->stifle_history($HISTSIZE); };

    if ($@) {
        $self->{context}
            ->log( 'debug' => "You will need Term::ReadLine::Gnu" );
    }
    else {
        if ( -f $HISTFILE ) {
            $term->ReadHistory($HISTFILE)
                or $self->{context}
                ->log( 'warn' => "cannot read history file: $!" );
        }
    }

    while ( defined( my $line = $term->readline('archer> ') ) ) {
        next if $line =~ /^\s*$/;
        $self->catch_run($line);
    }

    print "\n";

    eval { $term->WriteHistory($HISTFILE); };
    if ($@) {
        $self->{context}
            ->log( 'debug' => "perlsh: cannot write history file: $!" );
    }

}

sub catch_run {
    my ( $self, $cmd ) = @_;

    if ( $cmd =~ /^on\s+/ ) {
        if ( $cmd =~ /^on\s(.*)\sdo\s(.*)$/ ) {
            $self->process_host( $1, $2 );
        }
        else {
            print "[WARNING] error in your syntax, see help\n";
        }
    }
    elsif ( $cmd =~ /^with\s+/ ) {
        if ( $cmd =~ /^with\s(.*)\sdo\s(.*)$/ ) {
            $self->process_role( $1, $2 );
        }
        else {
            print "[WARNING] error in your syntax, see help\n";
        }
    }
    elsif ( $cmd =~ /^help/ ) {
        $self->help();
    }
    elsif ( $cmd =~ /^(quit|exit)/ ) {
        print "bye bye\n";
        exit;
    }
    elsif ( $cmd =~ /^!/ ) {
        if ( $cmd =~ /^!(\w+)\s?(on|with)?\s?(.*)?$/ ) {
            my $task     = $1;
            my $action   = $2;
            my $machines = $3;
            if (   defined $action
                && defined $machines
                && length($machines) < 1 )
            {
                return print "[WARNING] error in your syntax, see help\n";
            }
            my $executed = 0;
            my %valid_host = map {$_=>1} @{$self->{servers}};
            for my $plugin ( @{ $self->{config}->{tasks}->{process} } ) {
                next if $plugin->{name} ne $task;
                $executed = 1;
                if ( defined $action ) {
                    if ( $action eq "on" ) {
                        my @hosts = split " ", $machines;
                        for my $host (uniq @hosts) {
                            $self->process_task( $plugin, $host ) if $valid_host{$host};
                        }
                    }
                    else {
                        my @roles = split " ", $machines;
                        my $server_tree = $self->{config}->{projects}->{$self->{context}->{project}};
                        for my $role (@roles) {
                            for my $host ( @{ $server_tree->{$role} } ) {
                                $self->process_task( $plugin, $host ) if $valid_host{$host};
                            }
                        }
                    }
                }
                else {
                    for my $host (@{$self->{servers}}) {
                        $self->process_task( $plugin, $host );
                    }
                }
            }
            if ( $executed == 0 ) {
                print "[WARNING] unable to find the requested task: $task\n";
            }
        }
        else {
            print "[WARNING] error in your syntax\n";
        }
    }
    else {
        $self->process_command($cmd);
    }
}

sub process_host {
    my ( $self, $hosts, $cmd ) = @_;

    my @hosts = split /\s/, $hosts;

    # check if hosts are in our config.
    my %valid_host = map {$_=>1} @{$self->{servers}};
    @hosts = grep { $valid_host{$_} } @hosts;

    if (@hosts) {
        $self->process_command( $cmd, \@hosts );
    }
}

sub process_role {
    my ( $self, $roles, $cmd ) = @_;

    my @roles      = split /\s/, $roles;
    my @hosts      = ();
    my @inexistant = ();
    my $server_tree = $self->{config}->{projects}->{$self->{context}->{project}};

    for my $role (@roles) {
        if ( !defined $server_tree->{$role} ) {
            push( @inexistant, $role );
            next;
        }
        for my $host ( @{ $server_tree->{$role} } ) {
            push @hosts, $host;
        }
    }
    if (@inexistant) {
        print "[WARNING] inexisting role(s) for "
            . join( ' ', @inexistant ) . "\n";
    }
    $self->process_command( $cmd, \@hosts );
}

sub process_command {
    my ( $self, $cmd, $hosts ) = @_;
    my $manager = $self->{parallel}->new;

    $hosts ||= $self->{servers};
    $hosts = [ sort( uniq(@{$hosts}) ) ];

    $manager->run(
        {   elems    => $hosts,
            callback => sub {
                my $server = shift;
                $self->callback( $server, $cmd );
            },
            num => $self->{context}->{parallel_num},
        }
    );
}

sub process_task {
    my ( $self, $plugin, $host ) = @_;
    my $class = ($plugin->{module} =~ /^\+(.+)$/) ? $1 : "Archer::Plugin::$plugin->{module}";
    $class->use or die $@;
    $class->new(
        {   config  => $plugin->{config},
            project => $self->{context}->{project},
            server  => $host
        }
    )->run( $self->{context} );
}

sub callback {
    my ( $self, $server, $cmd ) = @_;

    Net::SSH::sshopen2( $server, *READER, *WRITER, $cmd );
    while (<READER>) {
        chomp;
        print "[$server] $_\n";
    }
    close READER;
    close WRITER;
}

sub help {
    my ($self) = @_;
    my $help = <<HELP;
 To quit, just type quit, exit, or press ctrl-D. 
 This shell is still experimental.

 execute a command on all servers, just type it directly, like:

archer> ping

 To execute a command on a specific set of servers, specify an 'on' clause.
 Note that if you specify more than one host name, they must be 
 space-delimited.

archer> on app1.foo.com app2.foo.com do ping

 To execute a command on all servers matching a set of roles:

archer> with web db do ping

 To execute an Archer task, prefix the name with a bang, by default it
 will be executed only on the role applyed to this task.

archer> !restart

 To execute an Archer task on a specific set of servers:

archer> !restart on app1.foo.com app2.foo.com

 To execute an Archer task on all servers matching a set of roles:

archer> !restart with web db

HELP
    print $help;
}

1;
__END__

=head1 NAME

Archer::Shell - display shell prompt for remote servers.

=head1 DESCRIPTION

Shell prompt for remote servers.

=head1 FILES

    ~/.archer_shell_history

=head1 AUTHORS

    Gosuke Miyashita
    Tokuhiro Matsuno

=head1 SEE ALSO

L<Term::ReadLine>

=cut