PageRenderTime 25ms CodeModel.GetById 4ms RepoModel.GetById 0ms app.codeStats 0ms

/feature-tests/exec.pl

http://github.com/shabble/irssi-scripts
Perl | 442 lines | 285 code | 79 blank | 78 comment | 36 complexity | 799804cbcfb0dc9c1c940477fc460ea5 MD5 | raw file
Possible License(s): GPL-2.0
  1. # exec.pl
  2. # a (currently stupid) alternative to the built-in /exec, because it's broken
  3. # on OSX. This thing stll needs a whole bunch of actual features, but for now,
  4. # you can actually run commands.
  5. # Obviously, that's pretty dangerous. Use at your own risk.
  6. # EXEC [-] [-nosh] [-out | -msg <target> | -notice <target>] [-name <name>] <cmd line>
  7. # EXEC -out | -window | -msg <target> | -notice <target> | -close | -<signal> %<id>
  8. # EXEC -in %<id> <text to send to process>
  9. #
  10. # -: Don't print "process terminated ..." message
  11. #
  12. # -nosh: Don't start command through /bin/sh
  13. #
  14. # -out: Send output to active channel/query
  15. #
  16. # -msg: Send output to specified nick/channel
  17. #
  18. # -notice: Send output to specified nick/channel as notices
  19. #
  20. # -name: Name the process so it could be accessed easier
  21. #
  22. # -window: Move the output of specified process to active window
  23. #
  24. # -close: Forcibly close (or "forget") a process that doesn't die.
  25. # This only removes all information from irssi concerning the
  26. # process, it doesn't send SIGKILL or any other signal
  27. # to the process.
  28. #
  29. # -<signal>: Send a signal to process. <signal> can be either numeric
  30. # or one of the few most common ones (hup, term, kill, ...)
  31. #
  32. # -in: Send text to standard input of the specified process
  33. #
  34. # -interactive: Creates a query-like window item. Text written to it is
  35. # sent to executed process, like /EXEC -in.
  36. #
  37. # Execute specified command in background. Output of process is printed to
  38. # active window by default, but can be also sent as messages or notices to
  39. # specified nick or channel.
  40. #
  41. # Processes can be accessed either by their ID or name if you named it. Process
  42. # identifier must always begin with '%' character, like %0 or %name.
  43. #
  44. # Once the process is started, its output can still be redirected elsewhere with
  45. # the -window, -msg, etc. options. You can send text to standard input of the
  46. # process with -in option.
  47. #
  48. # -close option shouldn't probably be used if there's a better way to kill the
  49. # process. It is meant to remove the processes that don't die even with
  50. # SIGKILL. This option just closes the pipes used to communicate with the
  51. # process and frees all memory it used.
  52. #
  53. # EXEC without any arguments displays the list of started processes.
  54. #
  55. use 5.010; # 5.10 or above, necessary to get the return value from a command.
  56. use strict;
  57. use warnings;
  58. use English '-no_match_vars';
  59. use Irssi;
  60. use POSIX;
  61. use Time::HiRes qw/sleep/;
  62. use IO::Handle;
  63. use IO::Pipe;
  64. use IPC::Open3;
  65. use Symbol 'geniosym';
  66. use Data::Dumper;
  67. our $VERSION = '0.1';
  68. our %IRSSI = (
  69. authors => 'shabble',
  70. contact => 'shabble+irssi@metavore.org',
  71. name => 'exec.pl',
  72. description => '',
  73. license => 'Public Domain',
  74. );
  75. my @processes = ();
  76. sub get_processes { return @processes }
  77. # the /exec command, nothing to do with the actual command being run.
  78. my $command;
  79. my $command_options;
  80. sub get_new_id {
  81. my $i = 1;
  82. foreach my $proc (@processes) {
  83. if ($proc->{id} != $i) {
  84. next;
  85. }
  86. $i++;
  87. }
  88. return $i;
  89. }
  90. sub add_process {
  91. #my ($pid) = @_;
  92. my $id = get_new_id();
  93. my $new = {
  94. id => $id,
  95. pid => 0,
  96. in_tag => 0,
  97. out_tag => 0,
  98. err_tag => 0,
  99. s_in => geniosym(), #IO::Handle->new,
  100. s_err => geniosym(), #IO::Handle->new,
  101. s_out => geniosym(), #IO::Handle->new,
  102. cmd => '',
  103. opts => {},
  104. };
  105. # $new->{s_in}->autoflush(1);
  106. # $new->{s_out}->autoflush(1);
  107. # $new->{s_err}->autoflush(1);
  108. push @processes, $new;
  109. _msg("New process item created: $id");
  110. return $new;
  111. }
  112. sub find_process_by_id {
  113. my ($id) = @_;
  114. my @matches = grep { $_->{id} == $id } @processes;
  115. _error("wtf, multiple id matches for $id. BUG") if @matches > 1;
  116. return $matches[0];
  117. }
  118. sub find_process_by_pid {
  119. my ($pid) = @_;
  120. my @matches = grep { $_->{pid} == $pid } @processes;
  121. _error("wtf, multiple pid matches for $pid. BUG") if @matches > 1;
  122. return $matches[0];
  123. }
  124. sub remove_process {
  125. my ($id, $verbose) = @_;
  126. my $del_index = 0;
  127. foreach my $proc (@processes) {
  128. if ($id == $proc->{id}) {
  129. last;
  130. }
  131. $del_index++;
  132. }
  133. print "remove: del index: $del_index";
  134. if ($del_index <= $#processes) {
  135. my $dead = splice(@processes, $del_index, 1, ());
  136. #_msg("removing " . Dumper($dead));
  137. Irssi::input_remove($dead->{err_tag});
  138. Irssi::input_remove($dead->{out_tag});
  139. close $dead->{s_out};
  140. close $dead->{s_in};
  141. close $dead->{s_err};
  142. } else {
  143. $verbose = 1;
  144. if ($verbose) {
  145. print "remove: No such process with ID $id";
  146. }
  147. }
  148. }
  149. sub show_current_processes {
  150. if (@processes == 0) {
  151. print "No processes running";
  152. return;
  153. }
  154. foreach my $p (@processes) {
  155. printf("ID: %d, PID: %d, Command: %s", $p->{id}, $p->{pid}, $p->{cmd});
  156. }
  157. }
  158. sub parse_options {
  159. my ($args) = @_;
  160. my @options = Irssi::command_parse_options($command, $args);
  161. if (@options) {
  162. my $opt_hash = $options[0];
  163. my $rest = $options[1];
  164. $rest =~ s/^\s*(.*?)\s*$/$1/; # trim surrounding space.
  165. #print Dumper([$opt_hash, $rest]);
  166. if (length $rest) {
  167. return ($opt_hash, $rest);
  168. } else {
  169. show_current_processes();
  170. return ();
  171. }
  172. } else {
  173. _error("Error parsing $command options");
  174. return ();
  175. }
  176. }
  177. sub schedule_cleanup {
  178. my $fd = shift;
  179. Irssi::timeout_add_once(100, sub { $_[0]->close }, $fd);
  180. }
  181. sub do_fork_and_exec {
  182. my ($rec) = @_;
  183. #Irssi::timeout_add_once(100, sub { die }, {});
  184. return unless exists $rec->{cmd};
  185. drop_privs();
  186. _msg("Executing command " . join(", ", @{ $rec->{cmd} }));
  187. my $c = join(" ", @{ $rec->{cmd} });
  188. my $pid = open3($rec->{s_sin}, $rec->{s_out}, $rec->{s_err}, $c);
  189. _msg("PID is $pid");
  190. $rec->{pid} = $pid;
  191. # _msg("Pid %s, in: %s, out: %s, err: %s, cmd: %s",
  192. # $pid, $sin, $sout, $serr, $cmd);
  193. # _msg("filenos, Pid %s, in: %s, out: %s, err: %s",
  194. # $pid, $sin->fileno, $sout->fileno, $serr->fileno);
  195. if (not defined $pid) {
  196. _error("open3 failed: $! Aborting");
  197. close($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out});
  198. undef($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out});
  199. return;
  200. }
  201. # parent
  202. if ($pid) {
  203. # eval {
  204. print "fileno is " . fileno($rec->{s_out});
  205. $rec->{out_tag} = Irssi::input_add( fileno($rec->{s_out}),
  206. Irssi::INPUT_READ,
  207. \&child_output,
  208. $rec);
  209. #die unless $rec->{out_tag};
  210. $rec->{err_tag} = Irssi::input_add(fileno($rec->{s_err}),
  211. Irssi::INPUT_READ,
  212. \&child_error,
  213. $rec);
  214. #die unless $rec->{err_tag};
  215. # };
  216. Irssi::pidwait_add($pid);
  217. die "input_add failed to initialise: $@" if $@;
  218. }
  219. }
  220. sub drop_privs {
  221. my @temp = ($EUID, $EGID);
  222. my $orig_uid = $UID;
  223. my $orig_gid = $GID;
  224. $EUID = $UID;
  225. $EGID = $GID;
  226. # Drop privileges
  227. $UID = $orig_uid;
  228. $GID = $orig_gid;
  229. # Make sure privs are really gone
  230. ($EUID, $EGID) = @temp;
  231. die "Can't drop privileges"
  232. unless $UID == $EUID && $GID eq $EGID;
  233. }
  234. sub child_error {
  235. my $rec = shift;
  236. my $err_fh = $rec->{s_err};
  237. my $done = 0;
  238. while (not $done) {
  239. my $data = '';
  240. _msg("Stderr: starting sysread");
  241. my $bytes_read = sysread($err_fh, $data, 256);
  242. if (not defined $bytes_read) {
  243. _error("stderr: sysread failed:: $!");
  244. $done = 1;
  245. } elsif ($bytes_read == 0) {
  246. _msg("stderr: sysread got EOF");
  247. $done = 1;
  248. } elsif ($bytes_read < 256) {
  249. # that's all, folks.
  250. _msg("%%_stderr:%%_ read %d bytes: %s", $bytes_read, $data);
  251. } else {
  252. # we maybe need to read some more
  253. _msg("%%_stderr:%%_ read %d bytes: %s, maybe more", $bytes_read, $data);
  254. }
  255. }
  256. _msg('removing input stderr tag');
  257. Irssi::input_remove($rec->{err_tag});
  258. }
  259. sub sig_pidwait {
  260. my ($pidwait, $status) = @_;
  261. my @matches = grep { $_->{pid} == $pidwait } @processes;
  262. foreach my $m (@matches) {
  263. _msg("PID %d has terminated. Status %d (or maybe %d .... %d)",
  264. $pidwait, $status, $?, ${^CHILD_ERROR_NATIVE} );
  265. remove_process($m->{id});
  266. }
  267. }
  268. sub child_output {
  269. my $rec = shift;
  270. my $out_fh = $rec->{s_out};
  271. my $done = 0;
  272. while (not $done) {
  273. my $data = '';
  274. _msg("Stdout: starting sysread");
  275. my $bytes_read = sysread($out_fh, $data, 256);
  276. if (not defined $bytes_read) {
  277. _error("stdout: sysread failed:: $!");
  278. $done = 1;
  279. } elsif ($bytes_read == 0) {
  280. _msg("stdout: sysread got EOF");
  281. $done = 1;
  282. } elsif ($bytes_read < 256) {
  283. # that's all, folks.
  284. _msg("%%_stdout:%%_ read %d bytes: %s", $bytes_read, $data);
  285. } else {
  286. # we maybe need to read some more
  287. _msg("%%_stdout:%%_ read %d bytes: %s, maybe more", $bytes_read, $data);
  288. }
  289. }
  290. _msg('removing input stdout tag');
  291. Irssi::input_remove($rec->{out_tag});
  292. #schedule_cleanup($stdout_reader);
  293. #$stdout_reader->close;
  294. }
  295. sub _error {
  296. my ($msg, @params) = @_;
  297. my $win = Irssi::active_win();
  298. my $str = sprintf($msg, @params);
  299. $win->print($str, Irssi::MSGLEVEL_CLIENTERROR);
  300. }
  301. sub _msg {
  302. my ($msg, @params) = @_;
  303. my $win = Irssi::active_win();
  304. my $str = sprintf($msg, @params);
  305. $win->print($str, Irssi::MSGLEVEL_CLIENTCRAP);
  306. }
  307. sub cmd_exec {
  308. my ($args, $server, $witem) = @_;
  309. Irssi::signal_stop;
  310. my @options = parse_options($args);
  311. if (@options) {
  312. my $rec = add_process();
  313. my ($options, $cmd) = @options;
  314. $cmd = [split ' ', $cmd];
  315. if (not exists $options->{nosh}) {
  316. unshift @$cmd, ("/bin/sh -c");
  317. }
  318. $rec->{opts} = $options;
  319. $rec->{cmd} = $cmd;
  320. do_fork_and_exec($rec)
  321. }
  322. }
  323. sub cmd_input {
  324. my ($args) = @_;
  325. my $rec = $processes[0]; # HACK, make them specify.
  326. if ($rec->{pid}) {
  327. print "INput writing to $rec->{pid}";
  328. my $fh = $rec->{s_in};
  329. my $ret = syswrite($fh, "$args\n");
  330. if (not defined $ret) {
  331. print "Error writing to process $rec->{pid}: $!";
  332. } else {
  333. print "Wrote $ret bytes to $rec->{pid}";
  334. }
  335. } else {
  336. _error("no execs are running to accept input");
  337. }
  338. }
  339. sub exec_init {
  340. $command = "exec";
  341. $command_options = join ' ',
  342. (
  343. '!-', 'interactive', 'nosh', '+name', '+msg',
  344. '+notice', 'window', 'close', '+level', 'quiet'
  345. );
  346. Irssi::command_bind($command, \&cmd_exec);
  347. Irssi::command_set_options($command, $command_options);
  348. Irssi::command_bind('input', \&cmd_input);
  349. Irssi::signal_add('pidwait', \&sig_pidwait);
  350. }
  351. exec_init();
  352. package Irssi::UI;
  353. {
  354. no warnings 'redefine';
  355. sub processes() {
  356. return Irssi::Script::exec::get_processes();
  357. }
  358. }
  359. 1;