diff options
| -rw-r--r-- | feature-tests/exec.pl | 248 | 
1 files changed, 248 insertions, 0 deletions
| diff --git a/feature-tests/exec.pl b/feature-tests/exec.pl new file mode 100644 index 0000000..e268291 --- /dev/null +++ b/feature-tests/exec.pl @@ -0,0 +1,248 @@ +# 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(); | 
