# 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 | -notice ] [-name ] # EXEC -out | -window | -msg | -notice | -close | - % # EXEC -in % # # -: 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. # # -: Send a signal to process. 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();