aboutsummaryrefslogblamecommitdiffstats
path: root/feature-tests/exec.pl
blob: e268291f3fb4a79169f177b29fc2d5d78a97d492 (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 Data::Dumper;

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


my $forked = 0;

my $command;
my $command_options;


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

        print Dumper($opt_hash);
        return ($opt_hash, $rest);
    } else {
        _error("Error parsing $command options");
        return ();
    }
}



sub do_fork_and_exec {
    my ($options, $cmd) = @_;

    my $stdout_pipe = IO::Pipe->new;
    my $stderr_pipe = IO::Pipe->new;

#    return if $forked;

    #my $pid = fork();

    if (not defined $pid) {
        _error("Fork failed: $! Aborting");
        $_->close for $stdout_pipe->handles;
        undef $stdout_pipe;
        return;
    }

#    $forked = 1;

    if ($pid > 0) {             # this is the parent (Irssi)
        my $tag;

        Irssi::pidwait_add($pid);

        my $stdout_reader = $stdout_pipe->reader;
        $stdout_reader->autoflush;

        my @args = ($stdout_reader, \$tag, $pid, $cmd, $options);
        $tag = Irssi::input_add($stdout_reader->fileno,
                                Irssi::INPUT_READ,
                                \&child_output,
                                \@args);

    } else {                    # child
        # make up some data - block if we like.
        drop_privs();
        my $stdout_fh = $stdout_pipe->writer;
        $stdout_fh->autoflush;

        my @data = qx/$cmd/;
        my $retval = ${^CHILD_ERROR_NATIVE};

        $stdout_fh->print($_) for @data;

        my $done_str = "__DONE__$retval\n";
        if ($data[$#data] =~ m/\n$/) {
        } else {
            $done_str = "\n" . $done_str;
        }
        $stdout_fh->print($done_str);

        $stdout_fh->close;

        POSIX::_exit(1);
    }
}
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_output {
    my $args = shift;
    my ($stdout_reader, $tag_ref, $pid, $cmd, $options) = @$args;

    my $return_value = 0;

    while (defined(my $data = <$stdout_reader>)) {

        chomp $data;

        # TODO: do we want to remove empty lines?
        #return unless length $data;

        if ($data =~ m/^__DONE__(\d+)$/) {
            $return_value = $1;
            last;
        } else {
            _msg("$data");
        }
    }

    if (not exists $options->{'-'}) {
        _msg("process %d (%s) terminated with return code %d",
             $pid, $cmd, $return_value);
    }

    $stdout_reader->close;
    Irssi::input_remove($$tag_ref);
}

sub _error {
    my ($msg) = @_;
    my $win = Irssi::active_win();
    $win->print($msg, 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) = @_;
    # TODO: parse some options here.
    Irssi::signal_stop;

    my @options = parse_options($args);
    if (@options) {
        do_fork_and_exec(@options)
    }

}

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

exec_init();