File Coverage

bin/run-stop-run
Criterion Covered Total %
statement 125 216 57.8
branch 50 160 31.2
condition 10 51 19.6
subroutine 18 25 72.0
pod n/a
total 203 452 44.9


line stmt bran cond sub pod time code
1             #!perl
2 7     7   35484 use strict;
  7         13  
  7         280  
3 7     7   35 use warnings;
  7         11  
  7         412  
4              
5 7     7   3214 use App::RunStopRun;
  7         20  
  7         264  
6 7     7   39 use Config;
  7         16  
  7         327  
7 7     7   41 use File::Spec;
  7         11  
  7         204  
8 7     7   5372 use Getopt::Long qw(:config bundling no_ignore_case require_order);
  7         123598  
  7         61  
9 7     7   5558 use IO::Pty;
  7         197569  
  7         759  
10 7     7   121 use List::Util qw(first uniq);
  7         15  
  7         1367  
11 7     7   55 use POSIX qw(:unistd_h :sys_wait_h);
  7         16  
  7         53  
12 7     7   23073 use Pod::Usage;
  7         567848  
  7         1344  
13 7     7   4237 use Proc::ProcessTable;
  7         45352  
  7         478  
14 7     7   4222 use Time::HiRes qw(sleep);
  7         11591  
  7         51  
15              
16             # NOTE: docs claim getpgrp($PID) isn't portable. Also, the POSIX module
17             # doesn't provide a separate interface for getpgid, though it's part of the
18             # standard and used internally.
19             use constant HAS_PORTABLE_GETPGRP =>
20 7     7   1356 !! grep defined, @Config{qw( d_getpgid d_bsdgetpgrp d_getpgrp2 )};
  7         16  
  7         36707  
21              
22 7         1219959 my $filename = (File::Spec->splitpath(__FILE__))[-1];
23 7         74 my @option = (
24             'verbose|v' => \my $verbose,
25             'dry-run|n' => \my $dryrun,
26              
27             'limit|l=f' => \my $limit,
28             'pid|p=s' => \my @pid,
29             'run|r=f' => \my $run,
30             'stop|s=f' => \my $stop,
31             'group|g' => \my $group,
32             'nogroup|no-group|G' => \my $nogroup,
33             'children|c' => \my $children,
34             'notty|no-tty|T' => \my $notty,
35             'tty|t+' => \my $tty,
36              
37             'version|V' => \my $version,
38             'help|h' => \my $help,
39             'man|H' => \my $man,
40             );
41 7         23 my @getopt_msg;
42 7 50       15 eval {
43 7     0   89 local @SIG{qw(__DIE__ __WARN__)} = (sub { push @getopt_msg, $_[0] }) x 2;
  0         0  
44 7         56 GetOptions(@option);
45             } or usage(@getopt_msg);
46 7         11392 @pid = uniq split /,/, join ',', @pid;
47              
48 7 50       27 usage() if $help;
49 7 50       20 exit printf "$filename $App::RunStopRun::VERSION\n" if $version;
50 7 50       19 pod2usage(-exitval => 0, -verbose => 2) if $man;
51 7 50 33     45 usage('Missing command or PIDs') unless @pid or @ARGV;
52 7 50 33     36 usage("Can't use both command and PIDs") if @pid and @ARGV;
53 7 0 0     25 if (my @bad = grep { ! /^-?\d+$/ or 1 >= abs or ! kill 0, $_ } @pid) {
  0 50       0  
54 0 0       0 usage(sprintf "Bad PID%s: %s", 1 < @bad ? 's' : '', join ',', @bad);
55             }
56 7 50 33     27 usage('--run must be >0') if defined $run and 0 >= $run;
57 7 50 33     28 usage('--stop must be >0') if defined $stop and 0 >= $stop;
58 7 0 33     24 usage("Can't use --limit with both --run and --stop")
      33        
59             if defined $limit and defined $run and defined $stop;
60 7 50       24 if (defined $limit) {
61 0 0 0     0 $limit *= 100 if $limit and 1 > $limit;
62 0 0 0     0 usage('--limit must be between 1..99') if 1 > $limit or 99 < $limit;
63             }
64              
65 7   50     33 $limit ||= 50;
66 7   50     62 $run ||= 1;
67 7   33     72 $stop ||= 100 * $run / $limit - $run;
68              
69 7         13 my $status = 0;
70 7         13 my @kill;
71 7         102 my $isfg = (getpgrp == tcgetpgrp STDIN_FILENO);
72 7 100       24 if ($verbose) {
73 2         154 warn "Controller PID: $$\n";
74 2 50       14 warn "Controller is in the foreground\n" if $isfg;
75             }
76              
77 7         14 my $sigint;
78             $SIG{INT} = $SIG{TERM} = sub {
79 0     0   0 $sigint = shift;
80 0 0       0 warn "Received SIG$sigint\n" if $verbose;
81             # Ensure END block is run.
82 0         0 exit;
83 7         151 };
84             $SIG{USR1} = sub {
85 0     0   0 my $pids = join ',', sort { $a <=> $b } @pid;
  0         0  
86 0 0       0 my $info = sprintf "Invoked on PID%s: %s\n", 1 < @pid ? 's' : '', $pids;
87 0         0 my $kill = join ',', sort { $a <=> $b } @kill;
  0         0  
88 0 0       0 $info .= sprintf "Signals to PID%s: %s\n", 1 < @kill ? 's' : '', $kill
    0          
89             if $pids ne $kill;
90 0         0 warn $info;
91 7         129 };
92             # SIGINFO isn't standard.
93 7 50       37 $SIG{INFO} = $SIG{USR1} if exists $SIG{INFO};
94              
95 7         85 my $table = Proc::ProcessTable->new;
96              
97             # TODO: if ptrace is available, it might be possible to attach to the process
98             # to do something with the file descriptors.
99 7 50       9991 if (@pid) {
100 0         0 @kill = get_kill_list();
101 0 0       0 exit printf "Would signal %s\n", join ',', @kill if $dryrun;
102 0         0 run_stop_run() while kill 0, @kill;
103 0         0 exit;
104             }
105              
106 7 50       28 exit print "Would exec command: @ARGV\n" if $dryrun;
107              
108 7 50       280 pipe my ($parent_reader, $child_writer) or die "Can't pipe: $!";
109              
110 7         25 my ($pty, $termios, $ttyfh);
111 7 100       23 unless ($notty) {
112 5     15   111 $ttyfh = first { isatty $_ } *STDIN, *STDERR, *STDOUT;
  15         2190  
113 5 50       97 if (! $ttyfh) {
114 5 100       145 warn "No attached terminal found\n" if $verbose;
115 5 100       149 $tty = 0 if 1 == $tty;
116             }
117 0   0     0 else { $tty ||= 1 }
118 5 50       25 if ($tty) {
119 0         0 $pty = IO::Pty->new;
120 0 0       0 warn sprintf "Opened pseudo-terminal: %s\n", $pty->ttyname if $verbose;
121 0 0       0 if ($ttyfh) {
122 0 0       0 warn sprintf "Using terminal attached to %s\n",
123             [qw(STDIN STDERR STDOUT)]->[fileno $ttyfh] if $verbose;
124 0 0 0     0 if (@ARGV and $isfg) {
125 0         0 $termios = POSIX::Termios->new;
126 0 0       0 $termios->getattr(fileno $ttyfh) or $termios = undef;
127             }
128             }
129             }
130             }
131              
132             # Child
133 7 100       16355 unless ($pid[0] = fork) {
134 4 50       226 die "Can't fork: $!" unless defined $pid[0];
135 4         921 local @SIG{qw(INT TERM USR1)};
136 4 50       228 local $SIG{INFO} if exists $SIG{INFO};
137              
138 4         230 close $parent_reader;
139              
140             # Calls setsid(), so no need create a new process group with setpgrp().
141 4 50       251 if ($pty) {
    50          
142 0         0 $pty->make_slave_controlling_terminal;
143 0         0 my $slave = $pty->slave;
144 0         0 close $pty;
145 0 0       0 $slave->clone_winsize_from($ttyfh) if $ttyfh;
146 0         0 $slave->set_raw;
147 0         0 close $slave;
148             }
149             elsif (! $nogroup) {
150 4         47 setpgrp;
151             }
152              
153 4         155 syswrite $child_writer, "\0";
154              
155 4 100       267 warn "Exec'ing command: @ARGV\n" if $verbose;
156 4 0       41 exec { $ARGV[0] } @ARGV or exit $!;
  4         0  
157             }
158              
159 3         195 close $child_writer;
160             # Block until the child is ready.
161 3         2135 sysread $parent_reader, my $ready, 1;
162 3         143 close $parent_reader;
163              
164 3 50       89 if ($pty) {
165 0         0 $pty->close_slave;
166 0         0 $pty->set_raw;
167             }
168              
169 3         47 $group = ! $nogroup;
170              
171             $SIG{CHLD} = sub {
172 3     3   115 local ($!, $?);
173 3 100       81 warn "Received SIGCHLD\n" if $verbose;
174 3         213 while (0 < (my $pid = waitpid -1, WNOHANG)) {
175 3 50       70 $status = WIFEXITED($?) ? WEXITSTATUS($?)
    100          
176             : WIFSIGNALED($?) ? WTERMSIG($?) : $? >> 8;
177 3 100       131 warn "Reaped $pid; exit($status)\n" if $verbose;
178             }
179 3         402 };
180             $SIG{TSTP} = sub {
181 0     0   0 @kill = get_kill_list();
182 0 0       0 warn sprintf "Sending SIGTSTP to %s\n", join ',', @kill, $$ if $verbose;
183 0         0 kill $_ => @kill for qw(TSTP STOP);
184 0         0 kill STOP => $$;
185 3         167 };
186             $SIG{CONT} = sub {
187 0 0   0   0 warn sprintf "Sending SIGCONT to %s\n", join ',', @kill if $verbose;
188 0         0 kill CONT => @kill;
189 3         195 };
190             $SIG{WINCH} = sub {
191 0 0   0   0 $pty->slave->clone_winsize_from($ttyfh) if $ttyfh;
192 0 0       0 warn sprintf "Sending SIGWINCH to %s\n", join ',', @kill if $verbose;
193 0         0 kill WINCH => @kill;
194 3         129 };
195              
196 3         230 run_stop_run() until waitpid $pid[0], WNOHANG;
197              
198 3         567 exit $status;
199              
200              
201             END {
202 3 50   3   21 return unless @pid;
203              
204 3 50       37 close $pty if $pty;
205              
206 3 50       74 if (@kill) {
207             # Ensure the processes aren't left stopped.
208 0 0       0 warn sprintf "Sending SIGCONT to %s\n", join ',', @kill if $verbose;
209 0         0 kill CONT => @kill;
210              
211             # Controller process is in the foreground.
212 0 0 0     0 if (@ARGV and $isfg) {
213 0 0       0 warn sprintf "Sending SIGTERM to %s\n", join ',', @pid if $verbose;
214 0         0 kill TERM => @pid;
215             }
216             }
217              
218             # Restore terminal settings.
219 3 50       88 $termios->setattr(fileno $ttyfh, &POSIX::TCSANOW) if $termios;
220              
221 3 50         if ($sigint) {
222 0 0         warn "Sending SIG$sigint to self\n" if $verbose;
223 0           local @SIG{qw(INT TERM)};
224 0           kill $sigint => $$;
225             }
226             }
227              
228              
229             sub run_stop_run {
230 3     3   2402261 sleep $run;
231 3 50       118 @kill = get_kill_list() or return;
232 0 0       0 warn sprintf "Sending SIGSTOP to %s\n", join ',', @kill if $verbose;
233 0 0       0 kill STOP => @kill or return;
234 0         0 sleep $stop;
235 0 0       0 warn sprintf "Sending SIGCONT to %s\n", join ',', @kill if $verbose;
236 0 0       0 kill CONT => @kill or return;
237             }
238              
239              
240             sub get_kill_list {
241 3 0 33 3   26 return @pid if ! $group and ! $children;
242 3         25 my @p = @pid;
243              
244             # Generating the process table is slow- avoid if possible. The runtime of
245             # Proc::ProcessTable is similar to that of running `ps` and parsing it's
246             # output.
247 3 50       42 if (HAS_PORTABLE_GETPGRP and ! $children) {
248 3         11 my %g;
249 3         30 for my $p (@p) {
250 3 50       41 if (0 > $p) { $g{$p} = undef }
  0         0  
251             else {
252 3         18 my $g = getpgrp $p;
253 3 50       25 $g{ 0 - $g } = undef if 1 < $g;
254             }
255             }
256 3         33 @p = keys %g;
257             }
258             else {
259 0         0 my (%group, %child);
260 0         0 for my $p (@{ $table->table }) {
  0         0  
261 0         0 $group{$p->pid} = $p->pgrp;
262              
263 0         0 push @{ $child{ $p->ppid } }, $p->pid;
  0         0  
264             # Detached process.
265 0 0 0     0 push @{ $child{ $p->pgrp } }, $p->pid
  0         0  
266             if 1 == $p->ppid and $p->pid != $p->pgrp;
267             }
268              
269 0 0       0 if ($children) {
270 0         0 my %g; @g{ grep { 0 > $_ } @p } = ();
  0         0  
  0         0  
271 0         0 my @c = @p;
272 0 0       0 while (@c = map { @{ $child{$_} || [] } } @c) {
  0         0  
  0         0  
273             # Don't include members of any given process group.
274 0         0 push @p, grep { ! exists $g{$_} } @c;
  0         0  
275             }
276             }
277              
278 0 0 0     0 if ($group and ! $nogroup) {
279 0 0       0 my %m; @m{ map { my $g = $group{$_}; $g ? 0 - $g : $_ } @p } = ();
  0         0  
  0         0  
  0         0  
280 0         0 @p = keys %m;
281             }
282             }
283              
284 3         48 return @p;
285             }
286              
287              
288             sub usage {
289 0 0   0   0 my @msg = grep { defined and length } @_;
  0         0  
290 0         0 @msg = map { split "\n" } @msg;
  0         0  
291 0         0 warn "$filename: $_\n" for @msg;
292 0 0       0 warn "\n" if @_;
293              
294 0         0 print <<" END_OF_USAGE";
295             Usage:
296             $filename [options] command [arguments]
297             $filename [options] -p PID[,-PGID,...]
298             END_OF_USAGE
299              
300 0 0       0 print <<' END_OF_OPTIONS' unless @_;
301              
302             Options:
303             -v, --verbose Be verbose
304             -n, --dry-run Dry run, don't run command or send signals
305              
306             -l, --limit PERCENT Limit runtime to between 1..99 (default: 50.0)
307             -r, --run SECONDS Run the process for SECONDS (default: 1.0)
308             -s, --stop SECONDS Stop the process for SECONDS
309             -p, --pid PIDS Operate on PIDS
310             -c, --children Operate also on all child processes of the PIDs
311             -g, --group Operate on the process groups of the PIDs
312             -G --no-group Don't create a new process group or determine PGIDs
313             -T, --no-tty Disable pseudo-terminal allocation
314             -t, --tty Force pseudo-terminal allocation
315              
316             -V --version Display the version number
317             -h, --help Display this help message
318             -H, --man Display the complete documentation
319             END_OF_OPTIONS
320              
321 0 0       0 exit(@_ ? 2 : 0);
322             }
323              
324              
325             __END__