/feature-tests/exec.pl
Perl | 442 lines | 285 code | 79 blank | 78 comment | 36 complexity | 799804cbcfb0dc9c1c940477fc460ea5 MD5 | raw file
Possible License(s): GPL-2.0
- # 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;