File Coverage

lib/Command/Interactive.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.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   210477 use strict;
  7         13  
  7         204  
5 7     7   24 use warnings;
  7         9  
  7         217  
6              
7             our $VERSION = 1.3;
8              
9 7     7   5252 use Moose;
  0            
  0            
10              
11              
12             =head1 NAME
13              
14             Command::Interactive- handles interactive (and non-interactive) process invocation through a reliable and easily configured interface.
15              
16             =head1 SYNOPSIS
17              
18             This module can be used to invoke both interactive and non-interactive commands with predicatable results.
19              
20             use Command::Interactive;
21             use Carp;
22              
23             # Simple, non-interactive usage
24             my $result1 = Command::Interactive->new->run("cp foo /tmp/");
25             croak "Could not copy foo to /tmp/: $result!" if($result);
26              
27             # Interactive usage supports output parsing
28             # and automated responses to discovered strings
29             my $password_prompt = Command::Interactive::Interaction->new({
30             expected_string => 'Please enter your password:',
31             response => 'secret',
32             });
33              
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             use Command::Interactive::Interaction;
48             use IO::File;
49             use Carp;
50             use Expect;
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             my $self = shift;
160             my $command = shift;
161              
162             confess "No command provided" unless ($command);
163              
164             my $result;
165             if ($self->always_use_expect or @{$self->interactions}) {
166             # We'll to use Expect to handle this,
167             # which means that we will be able to
168             # respond to input requests
169             $result = $self->_run_via_expect($command);
170             } else {
171             my $use_command = $self->_fixup_command_to_catch_stderr($command);
172             $self->_log("Executing $use_command");
173             my $cfh = IO::File->new("$use_command|");
174             if ($cfh) {
175             while (my $output = <$cfh>) {
176             $self->_log("open() returned output: $output");
177             $self->_show_output($output);
178             }
179             $cfh->close;
180             $result = ($? >> 8) ? "Error executing $command: $!" : undef;
181             } else {
182             $result = "Could not execute $command: $!";
183             }
184             }
185              
186             $self->_log($result ? "Returning result: $result for command $command" : "Returning undef result, signifying success");
187             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             my $self = shift;
198             my $output = shift;
199              
200             return unless ($self->echo_output);
201              
202             $output =~ s/[\r\n]+/<br\/>\n/g if ($self->web_format);
203             $self->_log("Stream output: $output");
204             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             my $self = shift;
217             my $command = shift;
218              
219             my $use_command = $self->_fixup_command_to_catch_stderr($command);
220              
221             $self->_log("Using Expect to spawn command: $use_command");
222              
223             my $exp = Expect->new;
224             $exp->raw_pty(1);
225             $exp->log_stdout(0);
226              
227             my $e = $exp->spawn($use_command);
228             return "Could not start $command: $!" unless ($e);
229              
230             my ($expect_array, $indexed_interactions) = $self->_generate_interaction_list;
231              
232             my $result;
233              
234             my ($match_position, $error, $matched_string, $before, $after);
235              
236             my $occurrences = [];
237              
238             my $already_closed = 0;
239              
240             EXPECT_READ:
241             while (!$error) {
242             ($match_position, $error, $matched_string, $before, $after) = $e->expect($self->timeout, @$expect_array);
243             if ($match_position) {
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             my $show;
249             $show .= $before if (length($before));
250             $show .= $matched_string;
251             $show .= $after if (length($after));
252             $self->_show_output($show);
253              
254             # Determine whether this was an interactive
255             # request for a response, or an error
256             $match_position -= 1;
257             my $i = $indexed_interactions->[$match_position];
258             my $type = $i->type;
259             $occurrences->[$match_position] = 0 unless ($occurrences->[$match_position]);
260             $occurrences->[$match_position]++;
261             if ($i->is_error) {
262             $result = "Got error string '$matched_string', which matched error detection $type '" . $i->expected_string . "'";
263             last EXPECT_READ;
264             }
265              
266             if ($occurrences->[$match_position] > $i->max_allowed_occurrences) {
267             $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             last EXPECT_READ;
276             }
277             if ($i->response) {
278             $self->_log("Stream send: " . $i->response);
279             $e->send($i->actual_response_to_send);
280             }
281             } elsif ($error) {
282             if (($error eq '2:EOF') or $error =~ /3:Child PID (\d+) exited with/) {
283             # Let's see if there were any required
284             # interactions that failed to occur
285             for (my $count = 0; $count < scalar(@$indexed_interactions); $count++) {
286             my $i = $indexed_interactions->[$count];
287             if ($i->is_required and not $occurrences->[$count]) {
288             $result = "Failed to encounter required " . $i->type . " '" . $i->expected_string . "' before exit";
289             }
290             }
291             } elsif ($error eq '1:TIMEOUT') {
292             $result = "Got TIMEOUT from Expect (timeout=" . $self->timeout . " seconds)";
293             $e->hard_close;
294             $already_closed = 1;
295             } elsif ($error =~ /^3: (.+)/) # uncoverable
296             {
297             $result = "Failure on command: $1";
298             } elsif ($error =~ /^4:(.+)/) # uncoverable
299             {
300             $result = "Got error reading command filehandle: $1";
301             }
302             last EXPECT_READ;
303             }
304             }
305              
306             # Need to capture any remaining output
307             $self->_show_output($e->exp_before) if ($e->exp_before);
308             $e->expect(0) unless ($already_closed);
309             # In case the call to expect(0) caught any remaining output
310             $self->_show_output($e->exp_before) if ($e->exp_before);
311             $e->soft_close unless ($already_closed);
312              
313             if ($e->exitstatus and not defined($result)) {
314             $result = 'Got back return value ' . $e->exitstatus . " from $command";
315             }
316              
317             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             my $self = shift;
328              
329             my $expect_array = [];
330             my $indexed_interactions = [];
331              
332             my $counter;
333             foreach my $i (@{$self->interactions}) {
334             push @$expect_array, '-re' if ($i->expected_string_is_regex);
335             push @$expect_array, $i->expected_string;
336             push @$indexed_interactions, $i;
337             }
338              
339             return ($expect_array, $indexed_interactions);
340              
341             }
342              
343             =head2 _fixup_command_to_catch_stderr($original_command)
344              
345             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.
346              
347             Returns the modified version of $original_command.
348              
349             =cut
350              
351             sub _fixup_command_to_catch_stderr {
352             my $self = shift;
353             my $original_command = shift;
354              
355             my $use_command = $original_command;
356             $use_command .= " 2>&1" unless ($use_command =~ m#2>&1#);
357             return $use_command;
358             }
359              
360             =head2 _log($line_to_log)
361              
362             Used for internal logging purposes when debug_logfile() is defined. See the
363             discussion of debug_logfile() for a better way to debug YOUR command's
364             execution; this method is intended for consumption by developers of
365             Command::Interactive.
366              
367             =cut
368              
369             sub _log {
370             my $self = shift;
371             my $message = shift;
372              
373             my $result;
374              
375             if ($self->debug_logfile) {
376             my $f = IO::File->new(">>" . $self->debug_logfile);
377             croak("Could not open debugging log file " . $self->debug_logfile) unless ($f);
378             my $result = $f->print(map { POSIX::strftime("[%Y-%m-%dT%H:%M:%SZ] $_\n", gmtime) } split(/[\r\n]/, $message));
379             $f->close;
380             }
381              
382             return $result;
383             }
384              
385             no Moose;
386             __PACKAGE__->meta->make_immutable;
387              
388             1;
389              
390             =head1 AUTHOR
391              
392             Binary.com, <perl@binary.com>
393              
394             =head1 LICENSE
395              
396             This module is free software; you can redistribute it and/or modify it under the
397             same terms as Perl itself.
398              
399             =cut
400