File Coverage

blib/lib/App/RemoteCommand/SSH.pm
Criterion Covered Total %
statement 30 128 23.4
branch 0 52 0.0
condition 0 39 0.0
subroutine 10 19 52.6
pod 0 8 0.0
total 40 246 16.2


line stmt bran cond sub pod time code
1             package App::RemoteCommand::SSH;
2 1     1   7 use strict;
  1         5  
  1         33  
3 1     1   5 use warnings;
  1         2  
  1         31  
4 1     1   475 use App::RemoteCommand::Util qw(DEBUG logger);
  1         3  
  1         57  
5 1     1   1271 use Net::OpenSSH;
  1         39824  
  1         46  
6 1     1   548 use IO::Pty;
  1         15058  
  1         156  
7              
8             sub _ssh {
9 0     0     my ($self, %args) = @_;
10 0           my $loglevel = DEBUG ? "error" : "quiet";
11             Net::OpenSSH->new($args{host},
12             async => 1,
13             strict_mode => 0,
14             timeout => 6,
15             kill_ssh_on_timeout => 1,
16             master_setpgrp => 1,
17             master_opts => [
18             -o => "ConnectTimeout=5",
19             -o => "StrictHostKeyChecking=no",
20             -o => "UserKnownHostsFile=/dev/null",
21             -o => "LogLevel=$loglevel",
22 0 0         ($args{configfile} ? (-F => $args{configfile}) : ()),
23             ],
24             );
25             }
26              
27 1     1   9 use constant STATE_TODO => 1;
  1         2  
  1         75  
28 1     1   6 use constant STATE_CONNECTING => 2;
  1         3  
  1         57  
29 1     1   7 use constant STATE_CONNECTED => 3;
  1         2  
  1         47  
30 1     1   5 use constant STATE_DISCONNECTING => 4;
  1         2  
  1         57  
31 1     1   14 use constant STATE_DONE => 5;
  1         5  
  1         1225  
32              
33             sub new {
34 0     0 0   my ($class, %args) = @_;
35 0           bless {
36             %args,
37             ssh => undef,
38             cmd => [],
39             at_exit => undef,
40             state => STATE_TODO,
41             exit => -1,
42             current => undef,
43             sudo => undef,
44             }, $class;
45             }
46              
47             sub add {
48 0     0 0   my ($self, $cmd) = @_;
49 0           push @{$self->{cmd}}, $cmd;
  0            
50             }
51              
52             sub at_exit {
53 0     0 0   my $self = shift;
54 0 0         @_ ? $self->{at_exit} = shift : $self->{at_exit};
55             }
56              
57             sub cancel {
58 0     0 0   my ($self, $signal) = @_;
59              
60 0           DEBUG and logger " CANCEL %s, state %s", $self->host, $self->{state};
61              
62 0 0         if ($self->{state} == STATE_TODO) {
    0          
    0          
    0          
    0          
63 0           $self->{state} == STATE_DONE;
64             } elsif ($self->{state} == STATE_CONNECTING) {
65 0           @{$self->{cmd}} = ();
  0            
66 0           undef $self->{at_exit};
67 0           my $master_pid = $self->{ssh}->get_master_pid;
68 0           DEBUG and logger " SEND SIG$signal %s, process group for master pid %d", $self->host, $master_pid;
69 0           kill $signal => -$master_pid;
70             } elsif ($self->{state} == STATE_CONNECTED) {
71 0           @{$self->{cmd}} = ();
  0            
72 0 0 0       if ($signal
      0        
      0        
73             and $self->{current}
74             and $self->{current}{type} eq "cmd"
75             and my $pid = $self->{current}{pid}
76             ) {
77 0           DEBUG and logger " SEND SIG$signal %s, pid %s", $self->host, $pid;
78 0           kill $signal => $pid;
79             }
80             } elsif ($self->{state} == STATE_DISCONNECTING) {
81             # nop
82             } elsif ($self->{state} == STATE_DONE) {
83             # nop
84             } else {
85 0           die;
86             }
87             }
88              
89             sub error {
90 0     0 0   my $self = shift;
91 0 0 0       ($self->{ssh} && $self->{ssh}->error) || $self->{_error};
92             }
93              
94             sub host {
95 0     0 0   my $self = shift;
96 0           $self->{host};
97             }
98              
99             sub exit {
100 0     0 0   my $self = shift;
101 0           $self->{exit};
102             }
103              
104             sub one_tick {
105 0     0 0   my ($self, %args) = @_;
106              
107 0           my $exit_pid = $args{pid};
108 0           my $exit_code = $args{exit};
109 0           my $select = $args{select};
110              
111 0           my $ssh = $self->{ssh};
112              
113 0 0 0       if ($ssh and $exit_pid and $ssh->get_master_pid and $exit_pid == $ssh->get_master_pid) {
      0        
      0        
114 0           DEBUG and logger " FAIL %s, master process exited unexpectedly", $self->host;
115 0           $ssh->master_exited;
116 0   0       $self->{exit} = $exit_code || -1;
117 0   0       $self->{_error} = $self->{ssh}->error || "master process exited unexpectedly";
118 0           $self->{state} = STATE_DONE;
119 0           undef $self->{ssh};
120             }
121              
122 0 0         if ($self->{state} == STATE_TODO) {
123 0           DEBUG and logger " CONNECT %s", $self->host;
124 0           $ssh = $self->{ssh} = $self->_ssh(host => $self->{host}, configfile => $self->{configfile});
125 0           $self->{state} = STATE_CONNECTING;
126             }
127              
128 0 0         if ($self->{state} == STATE_CONNECTING) {
129 0           my $master_state = $ssh->wait_for_master(1);
130 0 0         if ($master_state) {
    0          
131 0           DEBUG and logger " CONNECTED %s", $self->host;
132 0           $self->{state} = STATE_CONNECTED;
133             } elsif (!defined $master_state) {
134             # still connecting...
135 0           return 1;
136             } else {
137 0           DEBUG and logger " FAIL TO CONNECT %s", $self->host;
138 0           $self->{exit} = -1;
139 0   0       $self->{_error} = $self->{ssh}->error || "master process exited unexpectedly";
140 0           $self->{state} = STATE_DONE;
141 0           undef $self->{ssh};
142             }
143             }
144              
145 0 0         if ($self->{state} == STATE_CONNECTED) {
146 0 0 0       if (!$self->{current} or $exit_pid && $self->{current} && $self->{current}{pid} == $exit_pid) {
      0        
      0        
147              
148 0 0         if ($self->{current}) {
149             DEBUG and logger " FINISH %s, pid %d, type %s, exit %d",
150 0           $self->host, $exit_pid, $self->{current}{type}, $exit_code;
151 0 0         if ($self->{current}{type} eq "cmd") {
152 0           $self->{exit} = $exit_code;
153             }
154             }
155              
156 0           my ($cmd, $type);
157 0 0         if (@{$self->{cmd}}) {
  0 0          
158 0           $cmd = shift @{$self->{cmd}};
  0            
159 0           $type = "cmd";
160             } elsif ($self->{at_exit}) {
161 0           $cmd = delete $self->{at_exit};
162 0           $type = "at_exit";
163             }
164              
165 0 0         if ($cmd) {
166 0           my $ssh = $self->{ssh};
167 0           my ($pid, $fh) = $cmd->($ssh);
168 0 0         if ($pid) {
169 0           DEBUG and logger " START %s, pid %d, type %s", $self->host, $pid, $type;
170 0           $self->{current} = {pid => $pid, type => $type};
171 0 0         $select->add(pid => $pid, fh => $fh, host => $self->host) if $fh;
172 0           return 1;
173             }
174             # save error
175 0           $self->{_error} = $self->{ssh}->error;
176             }
177              
178 0           undef $self->{current};
179 0           DEBUG and logger " DISCONNECTING %s", $self->host;
180 0           $ssh->disconnect(0); # XXX block disconnect
181 0           $self->{state} = STATE_DISCONNECTING;
182             } else {
183 0           return 1;
184             }
185             }
186              
187 0 0         if ($self->{state} == STATE_DISCONNECTING) {
188 0           my $master_state = $ssh->wait_for_master(1);
189 0 0 0       if (defined $master_state && !$master_state) {
190 0           DEBUG and logger " DISCONNECTED %s", $self->host;
191 0           $self->{state} = STATE_DONE;
192 0           undef $self->{ssh};
193             } else {
194 0           return 1;
195             }
196             }
197              
198 0 0         if ($self->{state} == STATE_DONE) {
199 0           DEBUG and logger " DONE %s", $self->host;
200 0           return;
201             }
202              
203 0           die;
204             }
205              
206             1;