aboutsummaryrefslogblamecommitdiffstats
path: root/feature-tests/exec.pl
blob: f6d6377964e6037e59ec2a7814cdc3cb3d5d65b9 (plain) (tree)






































































                                                                                      
                      











                                                          

                                       
 
                                                                     


                    









































                                                                    
 
















































                                                                               







                                                                

                                                               






                                          





                                                 



                                                            

                      
                   
 
                                                   
 
                                     
                 
 





                                                                     
 






                                                             
 

                                            

                                                                   
 

               
 

               
 






                                                                  
 




                                                                 
 
       
 
 
                                 
                                                       

     
 














                                            
                 






















                                                                                    

     


                                         
 
 

                                

                                                            

                                                                      
 
                                 
     


                  





















                                                                                    

     


                                         

                                      


            
                            
                                  

                                                   











                                                  
                       
                                       
 
                   












                                          



     

                    











                                                             




                                                       









                                                           


                                                

 
              












                                                    
# exec.pl
# a (currently stupid) alternative to the built-in /exec, because it's broken
# on OSX. This thing stll needs a whole bunch of actual features, but for now,
# you can actually run commands.

# Obviously, that's pretty dangerous.  Use at your own risk.

# EXEC [-] [-nosh] [-out | -msg <target> | -notice <target>] [-name <name>] <cmd line>
# EXEC -out | -window | -msg <target> | -notice <target> | -close | -<signal> %<id>
# EXEC -in %<id> <text to send to process>
#
#      -: Don't print "process terminated ..." message
#
#      -nosh: Don't start command through /bin/sh
#
#      -out: Send output to active channel/query
#
#      -msg: Send output to specified nick/channel
#
#      -notice: Send output to specified nick/channel as notices
#
#      -name: Name the process so it could be accessed easier
#
#      -window: Move the output of specified process to active window
#
#      -close: Forcibly close (or "forget") a process that doesn't die.
#              This only removes all information from irssi concerning the
#              process, it doesn't send SIGKILL or any other signal
#              to the process.
#
#      -<signal>: Send a signal to process. <signal> can be either numeric
#                 or one of the few most common ones (hup, term, kill, ...)
#
#      -in: Send text to standard input of the specified process
#
#      -interactive: Creates a query-like window item. Text written to it is
#                    sent to executed process, like /EXEC -in.
#
# Execute specified command in background. Output of process is printed to
# active window by default, but can be also sent as messages or notices to
# specified nick or channel.
#
# Processes can be accessed either by their ID or name if you named it. Process
# identifier must always begin with '%' character, like %0 or %name.
#
# Once the process is started, its output can still be redirected elsewhere with
# the -window, -msg, etc. options. You can send text to standard input of the
# process with -in option.
#
# -close option shouldn't probably be used if there's a better way to kill the
# process. It is meant to remove the processes that don't die even with
# SIGKILL. This option just closes the pipes used to communicate with the
# process and frees all memory it used.
#
# EXEC without any arguments displays the list of started processes.
#



use 5.010;    # 5.10 or above, necessary to get the return value from a command.

use strict;
use warnings;
use English '-no_match_vars';

use Irssi;
use POSIX;
use Time::HiRes qw/sleep/;
use IO::Handle;
use IO::Pipe;
use IPC::Open3;
use Symbol 'geniosym';

use Data::Dumper;

our $VERSION = '0.1';
our %IRSSI = (
              authors     => 'shabble',
              contact     => 'shabble+irssi@metavore.org',
              name        => 'exec.pl',
              description => '',
              license     => 'Public Domain',
             );

my @processes = ();
sub get_processes { return @processes }

# the /exec command, nothing to do with the actual command being run.
my $command;
my $command_options;

sub get_new_id {
    my $i = 1;
    foreach my $proc (@processes) {
        if ($proc->{id} != $i) {
            next;
        }
        $i++;
    }
    return $i;
}

sub add_process {
    #my ($pid) = @_;
    my $id = get_new_id();

    my $new = {
               id      => $id,
               pid     => 0,
               in_tag  => 0,
               out_tag => 0,
               err_tag => 0,
               s_in    => geniosym(), #IO::Handle->new,
               s_err   => geniosym(), #IO::Handle->new,
               s_out   => geniosym(), #IO::Handle->new,
               cmd     => '',
               opts    => {},
              };

    # $new->{s_in}->autoflush(1);
    # $new->{s_out}->autoflush(1);
    # $new->{s_err}->autoflush(1);

    push @processes, $new;

    _msg("New process item created: $id");
    return $new;
}

sub find_process_by_id {
    my ($id) = @_;
    my @matches =  grep { $_->{id} == $id } @processes;
    _error("wtf, multiple id matches for $id. BUG") if @matches > 1;

    return $matches[0];

}
sub find_process_by_pid {
    my ($pid) = @_;
    my @matches =  grep { $_->{pid} == $pid } @processes;
    _error("wtf, multiple pid matches for $pid. BUG") if @matches > 1;

    return $matches[0];
}

sub remove_process {
    my ($id, $verbose) = @_;
    my $del_index = 0;
    foreach my $proc (@processes) {
        if ($id == $proc->{id}) {
            last;
        }
        $del_index++;
    }
    print "remove: del index: $del_index";
    if ($del_index <= $#processes) {
        my $dead = splice(@processes, $del_index, 1, ());
        #_msg("removing " . Dumper($dead));

        Irssi::input_remove($dead->{err_tag});
        Irssi::input_remove($dead->{out_tag});

        close $dead->{s_out};
        close $dead->{s_in};
        close $dead->{s_err};

    } else {
        $verbose = 1;
        if ($verbose) {
            print "remove: No such process with ID $id";
        }
    }
}

sub show_current_processes {
    if (@processes == 0) {
        print "No processes running";
        return;
    }
    foreach my $p (@processes) {
        printf("ID: %d, PID: %d, Command: %s", $p->{id}, $p->{pid}, $p->{cmd});
    }
}

sub parse_options {
    my ($args) = @_;
    my @options = Irssi::command_parse_options($command, $args);
    if (@options) {
        my $opt_hash = $options[0];
        my $rest     = $options[1];

        $rest =~ s/^\s*(.*?)\s*$/$1/; # trim surrounding space.

        #print Dumper([$opt_hash, $rest]);
        if (length $rest) {
            return ($opt_hash, $rest);
        } else {
            show_current_processes();
            return ();
        }
    } else {
        _error("Error parsing $command options");
        return ();
    }
}

sub schedule_cleanup {
    my $fd = shift;
    Irssi::timeout_add_once(100, sub { $_[0]->close }, $fd);
}

sub do_fork_and_exec {
    my ($rec) = @_;

    #Irssi::timeout_add_once(100, sub { die }, {});

    return unless exists $rec->{cmd};
    drop_privs();

    _msg("Executing command " . join(", ", @{ $rec->{cmd} }));
    my $c = join(" ", @{ $rec->{cmd} });
    my $pid = open3($rec->{s_sin}, $rec->{s_out}, $rec->{s_err}, $c);

    _msg("PID is $pid");
    $rec->{pid} = $pid;

    # _msg("Pid %s, in: %s, out: %s, err: %s, cmd: %s",
    #      $pid, $sin, $sout, $serr, $cmd);

    # _msg("filenos, Pid %s, in: %s, out: %s, err: %s",
    #      $pid, $sin->fileno, $sout->fileno, $serr->fileno);

    if (not defined $pid) {

        _error("open3 failed: $! Aborting");

        close($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out});
        undef($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out});

        return;
    }

    # parent
    if ($pid) {

#    eval {
        print "fileno is " .  fileno($rec->{s_out});
        $rec->{out_tag} = Irssi::input_add( fileno($rec->{s_out}),
                                            Irssi::INPUT_READ,
                                            \&child_output,
                                            $rec);
        #die unless $rec->{out_tag};

        $rec->{err_tag} = Irssi::input_add(fileno($rec->{s_err}),
                                           Irssi::INPUT_READ,
                                           \&child_error,
                                           $rec);
        #die unless $rec->{err_tag};

 #   };


        Irssi::pidwait_add($pid);
        die "input_add failed to initialise: $@" if $@;
    }
}

sub drop_privs {
    my @temp = ($EUID, $EGID);
    my $orig_uid = $UID;
    my $orig_gid = $GID;
    $EUID = $UID;
    $EGID = $GID;
    # Drop privileges
    $UID = $orig_uid;
    $GID = $orig_gid;
    # Make sure privs are really gone
    ($EUID, $EGID) = @temp;
    die "Can't drop privileges"
      unless $UID == $EUID && $GID eq $EGID;
}

sub child_error {
    my $rec = shift;

    my $err_fh = $rec->{s_err};

    my $done = 0;

    while (not $done) {
        my $data = '';
        _msg("Stderr: starting sysread");
        my $bytes_read = sysread($err_fh, $data, 256);
        if (not defined $bytes_read) {
            _error("stderr: sysread failed:: $!");
            $done = 1;
        } elsif ($bytes_read == 0) {
            _msg("stderr: sysread got EOF");
            $done = 1;
        } elsif ($bytes_read < 256) {
            # that's all, folks.
            _msg("%%_stderr:%%_ read %d bytes: %s", $bytes_read, $data);
        } else {
            # we maybe need to read some more
            _msg("%%_stderr:%%_ read %d bytes: %s, maybe more", $bytes_read, $data);
        }
    }

    _msg('removing input stderr tag');
    Irssi::input_remove($rec->{err_tag});

}

sub sig_pidwait {
    my ($pidwait, $status) = @_;
    my @matches = grep { $_->{pid} == $pidwait } @processes;
    foreach my $m (@matches) {
        _msg("PID %d has terminated. Status %d (or maybe %d .... %d)",
             $pidwait, $status, $?, ${^CHILD_ERROR_NATIVE} );

        remove_process($m->{id});
    }
}

sub child_output {
    my $rec = shift;
    my $out_fh = $rec->{s_out};

    my $done = 0;

    while (not $done) {
        my $data = '';
        _msg("Stdout: starting sysread");
        my $bytes_read = sysread($out_fh, $data, 256);
        if (not defined $bytes_read) {
            _error("stdout: sysread failed:: $!");
            $done = 1;
        } elsif ($bytes_read == 0) {
            _msg("stdout: sysread got EOF");
            $done = 1;
        } elsif ($bytes_read < 256) {
            # that's all, folks.
            _msg("%%_stdout:%%_ read %d bytes: %s", $bytes_read, $data);
        } else {
            # we maybe need to read some more
            _msg("%%_stdout:%%_ read %d bytes: %s, maybe more", $bytes_read, $data);
        }
    }

    _msg('removing input stdout tag');
    Irssi::input_remove($rec->{out_tag});

    #schedule_cleanup($stdout_reader);
    #$stdout_reader->close;
}

sub _error {
    my ($msg, @params) = @_;
    my $win = Irssi::active_win();
    my $str = sprintf($msg, @params);
    $win->print($str, Irssi::MSGLEVEL_CLIENTERROR);
}

sub _msg {
    my ($msg, @params) = @_;
    my $win = Irssi::active_win();
    my $str = sprintf($msg, @params);
    $win->print($str, Irssi::MSGLEVEL_CLIENTCRAP);
}

sub cmd_exec {

    my ($args, $server, $witem) = @_;
    Irssi::signal_stop;
    my @options = parse_options($args);

    if (@options) {
        my $rec = add_process();
        my ($options, $cmd) = @options;

        $cmd = [split ' ', $cmd];

        if (not exists $options->{nosh}) {
            unshift @$cmd, ("/bin/sh -c");
        }

        $rec->{opts} = $options;
        $rec->{cmd}  = $cmd;

        do_fork_and_exec($rec)
    }

}

sub cmd_input {
    my ($args) = @_;
    my $rec = $processes[0];    # HACK, make them specify.
    if ($rec->{pid}) {
        print "INput writing to $rec->{pid}";
        my $fh = $rec->{s_in};

        my $ret = syswrite($fh, "$args\n");
        if (not defined $ret) {
            print "Error writing to process $rec->{pid}: $!";
        } else {
            print "Wrote $ret bytes to $rec->{pid}";
        }

    } else {
        _error("no execs are running to accept input");
    }
}

sub exec_init {
    $command = "exec";
    $command_options = join ' ',
      (
       '!-', 'interactive', 'nosh', '+name', '+msg',
       '+notice', 'window', 'close', '+level', 'quiet'
      );

    Irssi::command_bind($command, \&cmd_exec);
    Irssi::command_set_options($command, $command_options);
    Irssi::command_bind('input', \&cmd_input);

    Irssi::signal_add('pidwait', \&sig_pidwait);
}

  exec_init();

package Irssi::UI;

{
    no warnings 'redefine';

    sub processes() {
        return Irssi::Script::exec::get_processes();
    }

}

1;