File Coverage

blib/lib/Command/Runner.pm
Criterion Covered Total %
statement 145 180 80.5
branch 53 78 67.9
condition 15 27 55.5
subroutine 22 26 84.6
pod 3 3 100.0
total 238 314 75.8


line stmt bran cond sub pod time code
1             package Command::Runner;
2 13     13   915268 use strict;
  13         379  
  13         430  
3 13     13   70 use warnings;
  13         26  
  13         340  
4              
5 13     13   6981 use Capture::Tiny ();
  13         378834  
  13         408  
6 13     13   5654 use Command::Runner::Format ();
  13         38  
  13         291  
7 13     13   5422 use Command::Runner::LineBuffer;
  13         38  
  13         382  
8 13     13   91 use Command::Runner::Quote ();
  13         33  
  13         207  
9 13     13   5199 use Command::Runner::Timeout;
  13         39  
  13         397  
10 13     13   87 use Config ();
  13         26  
  13         195  
11 13     13   6318 use IO::Select;
  13         21441  
  13         584  
12 13     13   6535 use POSIX ();
  13         82559  
  13         361  
13 13     13   91 use Time::HiRes ();
  13         31  
  13         316  
14              
15 13     13   65 use constant WIN32 => $^O eq 'MSWin32';
  13         25  
  13         2360  
16              
17             our $VERSION = '0.103';
18             our $TICK = 0.02;
19              
20             sub new {
21 64     64 1 311365 my ($class, %option) = @_;
22 64         203 my $command = delete $option{command};
23 64         141 my $commandf = delete $option{commandf};
24 64 50 66     347 die "Cannot specify both command and commandf" if $command && $commandf;
25 64 100 100     335 if (!$command && $commandf) {
26 2         22 $command = Command::Runner::Format::commandf @$commandf;
27             }
28             bless {
29 64 100       804 keep => 1,
30             _buffer => {},
31             %option,
32             ($command ? (command => $command) : ()),
33             }, $class;
34             }
35              
36             for my $attr (qw(command redirect timeout keep stdout stderr env)) {
37 13     13   91 no strict 'refs';
  13         26  
  13         21000  
38             *$attr = sub {
39 164     164   718 my $self = shift;
40 164         433 $self->{$attr} = $_[0];
41 164         872 $self;
42             };
43             }
44              
45             sub commandf {
46 9     9 1 110 my ($self, $format, @args) = @_;
47 9         113 $self->{command} = Command::Runner::Format::commandf $format, @args;
48 9         149 $self;
49             }
50              
51             sub run {
52 64     64 1 247 my $self = shift;
53 64 50       222 local %ENV = %{$self->{env}} if $self->{env};
  0         0  
54 64         133 my $command = $self->{command};
55 64 100       273 if (ref $command eq 'CODE') {
56 23     23   193 $self->_wrap(sub { $self->_run_code($command) });
  23         37357  
57             } elsif (WIN32) {
58 0     0   0 $self->_wrap(sub { $self->_system_win32($command) });
59             } else {
60 41         228 $self->_exec($command);
61             }
62             }
63              
64             sub _wrap {
65 23     23   71 my ($self, $code) = @_;
66              
67 23         43 my ($stdout, $stderr, $res);
68 23 100       79 if ($self->{redirect}) {
69 7         210 ($stdout, $res) = &Capture::Tiny::capture_merged($code);
70             } else {
71 16         603 ($stdout, $stderr, $res) = &Capture::Tiny::capture($code);
72             }
73              
74 23 50 33     20823 if (length $stdout and my $sub = $self->{stdout}) {
75 23         275 my $buffer = Command::Runner::LineBuffer->new(buffer => $stdout);
76 23         102 my @line = $buffer->get(1);
77 23         151 $sub->($_) for @line;
78             }
79 23 50 66     487 if (!$self->{redirect} and length $stderr and my $sub = $self->{stderr}) {
      66        
80 16         73 my $buffer = Command::Runner::LineBuffer->new(buffer => $stderr);
81 16         61 my @line = $buffer->get(1);
82 16         72 $sub->($_) for @line;
83             }
84              
85 23 100       169 if ($self->{keep}) {
86 19         101 $res->{stdout} = $stdout;
87 19         56 $res->{stderr} = $stderr;
88             }
89              
90 23         143 return $res;
91             }
92              
93             sub _run_code {
94 23     23   75 my ($self, $code) = @_;
95              
96 23 100       122 if (!$self->{timeout}) {
97 21         70 my $result = $code->();
98 21         2628 return { pid => $$, result => $result };
99             }
100              
101 2         8 my ($result, $err);
102             {
103 2         4 local $SIG{__DIE__} = 'DEFAULT';
  2         24  
104 2     2   62 local $SIG{ALRM} = sub { die "__TIMEOUT__\n" };
  2         2000400  
105 2         8 eval {
106 2         30 alarm $self->{timeout};
107 2         22 $result = $code->();
108             };
109 2         12 $err = $@;
110 2         74 alarm 0;
111             }
112 2 50       84 if (!$err) {
    50          
113 0         0 return { pid => $$, result => $result, };
114             } elsif ($err eq "__TIMEOUT__\n") {
115 2         52 return { pid => $$, result => $result, timeout => 1 };
116             } else {
117 0         0 die $err;
118             }
119             }
120              
121             sub _system_win32 {
122 0     0   0 my ($self, $command) = @_;
123              
124 0         0 my $pid;
125 0 0       0 if (ref $command) {
126 0         0 my @cmd = map { Command::Runner::Quote::quote_win32($_) } @$command;
  0         0  
127 0         0 $pid = system { $command->[0] } 1, @cmd;
  0         0  
128             } else {
129 0         0 $pid = system 1, $command;
130             }
131              
132 0 0       0 my $timeout = $self->{timeout} ? Command::Runner::Timeout->new($self->{timeout}, 1) : undef;
133 0     0   0 my $INT; local $SIG{INT} = sub { $INT++ };
  0         0  
  0         0  
134 0         0 my $result;
135 0         0 while (1) {
136 0 0       0 if ($INT) {
137 0         0 kill INT => $pid;
138 0         0 $INT = 0;
139             }
140              
141 0         0 my $res = waitpid $pid, POSIX::WNOHANG();
142 0 0       0 if ($res == -1) {
    0          
143 0         0 warn "waitpid($pid, POSIX::WNOHANG()) returns unexpectedly -1";
144 0         0 last;
145             } elsif ($res > 0) {
146 0         0 $result = $?;
147 0         0 last;
148             } else {
149 0 0 0     0 if ($timeout and my $signal = $timeout->signal) {
150 0         0 kill $signal => $pid;
151             }
152 0         0 Time::HiRes::sleep($TICK);
153             }
154             }
155 0   0     0 return { pid => $pid, result => $result, timeout => $timeout && $timeout->signaled };
156             }
157              
158             sub _exec {
159 41     41   143 my ($self, $command) = @_;
160              
161 41         1933 pipe my $stdout_read, my $stdout_write;
162 41         617 $self->{_buffer}{stdout} = Command::Runner::LineBuffer->new(keep => $self->{keep});
163              
164 41         105 my ($stderr_read, $stderr_write);
165 41 100       137 if (!$self->{redirect}) {
166 31         1025 pipe $stderr_read, $stderr_write;
167 31         268 $self->{_buffer}{stderr} = Command::Runner::LineBuffer->new(keep => $self->{keep});
168             }
169              
170 41         38369 my $pid = fork;
171 41 50       2621 die "fork: $!" unless defined $pid;
172 41 100       1229 if ($pid == 0) {
173 10         1265 close $_ for grep $_, $stdout_read, $stderr_read;
174 10         912 open STDOUT, ">&", $stdout_write;
175 10 100       536 if ($self->{redirect}) {
176 2         87 open STDERR, ">&", \*STDOUT;
177             } else {
178 8         387 open STDERR, ">&", $stderr_write;
179             }
180 10 50       1422 if ($Config::Config{d_setpgrp}) {
181 10 50       1410 POSIX::setpgid(0, 0) or die "setpgid: $!";
182             }
183              
184 10 100       288 if (ref $command) {
185 7         89 exec { $command->[0] } @$command;
  7         0  
186             } else {
187 3         0 exec $command;
188             }
189 0         0 exit 127;
190             }
191 31         3455 close $_ for grep $_, $stdout_write, $stderr_write;
192              
193 31 50       6199 my $signal_pid = $Config::Config{d_setpgrp} ? -$pid : $pid;
194              
195 31     0   686 my $INT; local $SIG{INT} = sub { $INT++ };
  31         3301  
  0         0  
196 31 100       1107 my $timeout = $self->{timeout} ? Command::Runner::Timeout->new($self->{timeout}, 1) : undef;
197 31         2408 my $select = IO::Select->new(grep $_, $stdout_read, $stderr_read);
198              
199 31         5716 while ($select->count) {
200 746 50       13726681 if ($INT) {
201 0         0 kill INT => $signal_pid;
202 0         0 $INT = 0;
203             }
204 746 100 100     5047 if ($timeout and my $signal = $timeout->signal) {
205 6         2276 kill $signal => $signal_pid;
206             }
207 746         3460 for my $ready ($select->can_read($TICK)) {
208 107 100       302014 my $type = $ready == $stdout_read ? "stdout" : "stderr";
209 107         1778 my $len = sysread $ready, my $buf, 64*1024;
210 107 100       595 if ($len) {
211 53         337 my $buffer = $self->{_buffer}{$type};
212 53         754 $buffer->add($buf);
213 53 100       352 next unless my @line = $buffer->get;
214 49 100       335 next unless my $sub = $self->{$type};
215 43         425 $sub->($_) for @line;
216             } else {
217 54 50       186 warn "sysread $type pipe failed: $!" unless defined $len;
218 54         272 $select->remove($ready);
219 54         3890 close $ready;
220             }
221             }
222             }
223 31         282 for my $type (qw(stdout stderr)) {
224 62 100       336 next unless my $sub = $self->{$type};
225 53 100       301 my $buffer = $self->{_buffer}{$type} or next;
226 48 100       219 my @line = $buffer->get(1) or next;
227 7         310 $sub->($_) for @line;
228             }
229 31         163 close $_ for $select->handles;
230 31         1572 waitpid $pid, 0;
231             my $res = {
232             pid => $pid,
233             result => $?,
234             timeout => $timeout && $timeout->signaled,
235             stdout => $self->{_buffer}{stdout} ? $self->{_buffer}{stdout}->raw : "",
236 31 50 66     442 stderr => $self->{_buffer}{stderr} ? $self->{_buffer}{stderr}->raw : "",
    100          
237             };
238 31         617 $self->{_buffer} = +{}; # cleanup
239 31         2495 return $res;
240             }
241              
242             1;
243             __END__