File Coverage

lib/Command/Interactive.pm
Criterion Covered Total %
statement 124 126 98.4
branch 52 60 86.6
condition 10 12 83.3
subroutine 14 14 100.0
pod 1 1 100.0
total 201 213 94.3


line stmt bran cond sub pod time code
1             package Command::Interactive;
2             # ABSTRACT: handles interactive (and non-interactive) process invocation
3              
4 7     7   188001 use strict;
  7         716  
  7         163  
5 7     7   22 use warnings;
  7         8  
  7         863  
6              
7             our $VERSION = '1.21';
8              
9 7     7   3143 use Moose;
  7         2104052  
  7         43  
10              
11             =head1 NAME
12              
13             Command::Interactive- handles interactive (and non-interactive) process invocation through a reliable and easily configured interface.
14              
15             =head1 SYNOPSIS
16              
17             This module can be used to invoke both interactive and non-interactive commands with predicatable results.
18              
19             use Command::Interactive;
20             use Carp;
21              
22             # Simple, non-interactive usage
23             my $result = Command::Interactive->new->run("cp foo /tmp/");
24             croak "Could not copy foo to /tmp/: $result!" if($result);
25              
26             # Interactive usage supports output parsing
27             # and automated responses to discovered strings
28             my $password_prompt = Command::Interactive::Interaction->new({
29             expected_string => 'Please enter your password:',
30             response => 'secret',
31             });
32              
33             my $my_logging_fh;
34             my $command = Command::Interactive->new({
35             echo_output => 1,
36             output_stream => $my_logging_fh,
37             interactions => [ $password_prompt ],
38             });
39             my $restart_result = $command->run("ssh user\@somehost 'service apachectl restart'");
40             if($restart_result)
41             {
42             warn "Couldn't restart server!";
43             }
44              
45             =cut
46              
47 7     7   36954 use Command::Interactive::Interaction;
  7         20  
  7         318  
48 7     7   3235 use IO::File;
  7         26155  
  7         782  
49 7     7   37 use Carp;
  7         8  
  7         276  
50 7     7   4995 use Expect;
  7         130359  
  7         6920  
51              
52             =head1 FIELDS
53              
54             =head2 always_use_expect (DEFAULT: FALSE)
55              
56             Whether to use the C<Expect> module to execute system commands. By default, Expect is only used if one or more interactions() are specified.
57              
58             =cut
59              
60             has always_use_expect => (
61             is => 'rw',
62             isa => 'Bool',
63             default => 0,
64             );
65              
66             =head2 debug_logfile (DEFAULT: undef)
67              
68             The name of a file to which lots of debugging information should be written. Typically useful only for maintainers. If you want to see what your command is doing, use echo_output() and a debugging filehandle (or just STDOUT).
69              
70             =cut
71              
72             has debug_logfile => (
73             is => 'rw',
74             isa => 'Str',
75             );
76              
77             =head2 echo_output (DEFAULT: FALSE)
78              
79             Whether to echo output to the specified output_stream(). This allows users of
80             Command::Interactive to see what is going on, but it also can clutter an interface with lots of superfluous command output. Use it wisely.
81              
82             See web_format() for a discussion about how to format command output for web interfaces.
83              
84             =cut
85              
86             has echo_output => (
87             is => 'rw',
88             isa => 'Bool',
89             default => 0,
90             );
91              
92             =head2 interactions (DEFAULT: [], the empty arrayref)
93              
94             An array reference of Command::Interactive::Interaction objects that specify the
95             interactions that may (or must) occur during the execution of the command. See
96             C<Command::Interactive::Interaction> for more information on specifying rules about command interactions.
97              
98             =cut
99              
100             has interactions => (
101             is => 'rw',
102             isa => 'ArrayRef',
103             default => sub { [] }, # References are not allowed as defaults
104             );
105              
106             =head2 output_stream (DEFAULT: STDOUT)
107              
108             The stream object to which output should be sent when echo_output() is enabled. This is any object with a print() method; it needn't have a full C<IO>-compliant interface.
109              
110             =cut
111              
112             has output_stream => (
113             is => 'rw',
114             default => *STDOUT,
115             );
116              
117             =head2 timeout (DEFAULT: undef)
118              
119             If defined, represents the timeout (in seconds) that Command::Interactive will wait for output when run() is called.
120              
121             =cut
122              
123             has timeout => (
124             is => 'rw',
125             isa => 'Int',
126             );
127              
128             =head2 web_format (DEFAULT: FALSE)
129              
130             Whether to format strings for web output when print command output as a result of echo_output(). If this is true, \r, \n, and \r\n will be replaced with "<br/>\n".
131              
132             =cut
133              
134             has web_format => (
135             is => 'rw',
136             isa => 'Bool',
137             default => 0,
138             );
139              
140             =head1 METHODS
141              
142             =head2 run($command)
143              
144             This method does the heavy lifting of Command::Interactive. If one or more interactions() are specified (or if always_use_expect() is true), then the heavy lifting is dispatched to _run_via_expect(); otherwise this method uses traditional perl C<open("$command |")> approach.
145              
146             $command is expected to be a scalar (string), properly escaped, that could be
147             executed (e.g.) via system() or ``. No matter what command you provide, the bash
148             file descriptors for stdout and stderr are tied together using '2>&1' unless you
149             have done so already. This allows Command::Interactive to capture and react to both regular output and errors using the same mechanism.
150              
151             run() returns undef if the command is successful, otherwise it returns a string describing why the command failed (or was thought to have failed).
152              
153             The command you pass in via $command is expected to exit with status code 0 on
154             success. If it returns something different, Command::Interactive will incorrectly conclude that the command failed and will return a message to that effect.
155              
156             =cut
157              
158             sub run {
159 20     20 1 1113 my $self = shift;
160 20         47 my $command = shift;
161              
162 20 100       79 confess "No command provided" unless ($command);
163              
164 19         17 my $result;
165 19 100 100     625 if ($self->always_use_expect or @{$self->interactions}) {
  13         308  
166             # We'll to use Expect to handle this,
167             # which means that we will be able to
168             # respond to input requests
169 14         42 $result = $self->_run_via_expect($command);
170             } else {
171 5         16 my $use_command = $self->_fixup_command_to_catch_stderr($command);
172 5         21 $self->_log("Executing $use_command");
173 4         31 my $cfh = IO::File->new("$use_command|");
174 4 100       6713 if ($cfh) {
175 2         640 while (my $output = <$cfh>) {
176 1         18 $self->_log("open() returned output: $output");
177 1         7 $self->_show_output($output);
178             }
179 2         34 $cfh->close;
180 2 100       75 $result = ($? >> 8) ? "Error executing $command: $!" : undef;
181             } else {
182 2         33 $result = "Could not execute $command: $!";
183             }
184             }
185              
186 18 100       534 $self->_log($result ? "Returning result: $result for command $command" : "Returning undef result, signifying success");
187 18         183 return $result;
188             }
189              
190             =head2 _show_output($chunk_of_output)
191              
192             If echo_output() is true, this command prints any output from $command to the chosen output_stream(). If web_format() is true, the output is first formatted for HTML by replacing end-of-line characters with "<br/>\n".
193              
194             =cut
195              
196             sub _show_output {
197 22     22   472 my $self = shift;
198 22         26 my $output = shift;
199              
200 22 100       558 return unless ($self->echo_output);
201              
202 2 100       39 $output =~ s/[\r\n]+/<br\/>\n/g if ($self->web_format);
203 2         11 $self->_log("Stream output: $output");
204 2         32 return $self->output_stream->print($output);
205             }
206              
207             =head2 _run_via_expect($command)
208              
209             This method handles running commands with one or more interactions() (or for which always_use_expect() is true) via the Perl module C<Expect>.
210              
211             The return semantics of _run_via_expect() are identical to those of run().
212              
213             =cut
214              
215             sub _run_via_expect {
216 14     14   16 my $self = shift;
217 14         42 my $command = shift;
218              
219 14         77 my $use_command = $self->_fixup_command_to_catch_stderr($command);
220              
221 14         51 $self->_log("Using Expect to spawn command: $use_command");
222              
223 14         86 my $exp = Expect->new;
224 14         6993 $exp->raw_pty(1);
225 14         399 $exp->log_stdout(0);
226              
227 14         261 my $e = $exp->spawn($use_command);
228 14 50       40489 return "Could not start $command: $!" unless ($e);
229              
230 14         117 my ($expect_array, $indexed_interactions) = $self->_generate_interaction_list;
231              
232 14         20 my $result;
233              
234 14         16 my ($match_position, $error, $matched_string, $before, $after);
235              
236 14         26 my $occurrences = [];
237              
238 14         22 my $already_closed = 0;
239              
240             EXPECT_READ:
241 14         37 while (!$error) {
242 20         627 ($match_position, $error, $matched_string, $before, $after) = $e->expect($self->timeout, @$expect_array);
243 20 100       1021529 if ($match_position) {
    50          
244             # Collapse this all into string just in case
245             # you have a f-ing retarded value of $/ or an
246             # overridden version of CORE::print() that puts
247             # newlines at the end of each print call
248 8         23 my $show;
249 8 100       26 $show .= $before if (length($before));
250 8         15 $show .= $matched_string;
251 8 50       18 $show .= $after if (length($after));
252 8         33 $self->_show_output($show);
253              
254             # Determine whether this was an interactive
255             # request for a response, or an error
256 8         11 $match_position -= 1;
257 8         15 my $i = $indexed_interactions->[$match_position];
258 8         52 my $type = $i->type;
259 8 100       31 $occurrences->[$match_position] = 0 unless ($occurrences->[$match_position]);
260 8         10 $occurrences->[$match_position]++;
261 8 100       246 if ($i->is_error) {
262 1         26 $result = "Got error string '$matched_string', which matched error detection $type '" . $i->expected_string . "'";
263 1         2 last EXPECT_READ;
264             }
265              
266 7 100       192 if ($occurrences->[$match_position] > $i->max_allowed_occurrences) {
267 1         30 $result =
268             "Got string '$matched_string', which matched expected $type '"
269             . $i->expected_string
270             . "'. This was occurrence #"
271             . $occurrences->[$match_position]
272             . ", which exceeds the specified limit of "
273             . $i->max_allowed_occurrences
274             . " occurrence(s) set for this $type";
275 1         4 last EXPECT_READ;
276             }
277 6 100       148 if ($i->response) {
278 1         20 $self->_log("Stream send: " . $i->response);
279 1         5 $e->send($i->actual_response_to_send);
280             }
281             } elsif ($error) {
282 12 100 66     148 if (($error eq '2:EOF') or $error =~ /3:Child PID (\d+) exited with/) {
    50          
    0          
    0          
283             # Let's see if there were any required
284             # interactions that failed to occur
285 11         45 for (my $count = 0; $count < scalar(@$indexed_interactions); $count++) {
286 7         11 my $i = $indexed_interactions->[$count];
287 7 100 66     213 if ($i->is_required and not $occurrences->[$count]) {
288 2         22 $result = "Failed to encounter required " . $i->type . " '" . $i->expected_string . "' before exit";
289             }
290             }
291             } elsif ($error eq '1:TIMEOUT') {
292 1         65 $result = "Got TIMEOUT from Expect (timeout=" . $self->timeout . " seconds)";
293 1         9 $e->hard_close;
294 1         358 $already_closed = 1;
295             } elsif ($error =~ /^3: (.+)/) # uncoverable
296             {
297 0         0 $result = "Failure on command: $1";
298             } elsif ($error =~ /^4:(.+)/) # uncoverable
299             {
300 0         0 $result = "Got error reading command filehandle: $1";
301             }
302 12         27 last EXPECT_READ;
303             }
304             }
305              
306             # Need to capture any remaining output
307 14 100       113 $self->_show_output($e->exp_before) if ($e->exp_before);
308 14 100       150 $e->expect(0) unless ($already_closed);
309             # In case the call to expect(0) caught any remaining output
310 14 100       1519 $self->_show_output($e->exp_before) if ($e->exp_before);
311 14 100       280 $e->soft_close unless ($already_closed);
312              
313 14 100 100     173 if ($e->exitstatus and not defined($result)) {
314 1         24 $result = 'Got back return value ' . $e->exitstatus . " from $command";
315             }
316              
317 14         287 return $result;
318             }
319              
320             =head2 _generate_instruction_list()
321              
322             This method returns information to be passed to C<Expect>'s expect() method, as well as a bookkeeping array using for tracking number of times a given interaction has occurred.
323              
324             =cut
325              
326             sub _generate_interaction_list {
327 14     14   43 my $self = shift;
328              
329 14         30 my $expect_array = [];
330 14         27 my $indexed_interactions = [];
331              
332 14         22 foreach my $i (@{$self->interactions}) {
  14         624  
333 10 100       318 push @$expect_array, '-re' if ($i->expected_string_is_regex);
334 10         280 push @$expect_array, $i->expected_string;
335 10         48 push @$indexed_interactions, $i;
336             }
337              
338 14         33 return ($expect_array, $indexed_interactions);
339              
340             }
341              
342             =head2 _fixup_command_to_catch_stderr($original_command)
343              
344             This method appends '2>&1' to the end of any command submitted to run(), except when that filehandle-tying string is already present in the command.
345              
346             Returns the modified version of $original_command.
347              
348             =cut
349              
350             sub _fixup_command_to_catch_stderr {
351 19     19   21 my $self = shift;
352 19         22 my $original_command = shift;
353              
354 19         21 my $use_command = $original_command;
355 19 100       74 $use_command .= " 2>&1" unless ($use_command =~ m#2>&1#);
356 19         32 return $use_command;
357             }
358              
359             =head2 _log($line_to_log)
360              
361             Used for internal logging purposes when debug_logfile() is defined. See the
362             discussion of debug_logfile() for a better way to debug YOUR command's
363             execution; this method is intended for consumption by developers of
364             Command::Interactive.
365              
366             =cut
367              
368             sub _log {
369 41     41   56 my $self = shift;
370 41         55 my $message = shift;
371              
372 41 100       1013 if ($self->debug_logfile) {
373 3         51 my $f = IO::File->new(">>" . $self->debug_logfile);
374 3 100       275 croak("Could not open debugging log file " . $self->debug_logfile) unless ($f);
375 2         10 $f->print(map { POSIX::strftime("[%Y-%m-%dT%H:%M:%SZ] $_\n", gmtime) } split(/[\r\n]/, $message));
  2         129  
376 2         19 $f->close;
377             }
378              
379 40         97 return;
380             }
381              
382 7     7   61 no Moose;
  7         13  
  7         59  
383             __PACKAGE__->meta->make_immutable;
384              
385             1;
386              
387             =head1 AUTHOR
388              
389             Binary.com, <perl@binary.com>
390              
391             =head1 LICENSE
392              
393             This module is free software; you can redistribute it and/or modify it under the
394             same terms as Perl itself.
395              
396             =cut
397