# 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 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;