aboutsummaryrefslogtreecommitdiffstats
path: root/feature-tests/exec.pl
diff options
context:
space:
mode:
Diffstat (limited to 'feature-tests/exec.pl')
-rw-r--r--feature-tests/exec.pl442
1 files changed, 442 insertions, 0 deletions
diff --git a/feature-tests/exec.pl b/feature-tests/exec.pl
new file mode 100644
index 0000000..f6d6377
--- /dev/null
+++ b/feature-tests/exec.pl
@@ -0,0 +1,442 @@
+# 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;