File Coverage

blib/lib/App/RemoteCommand/SSH.pm
Criterion Covered Total %
statement 30 128 23.4
branch 0 50 0.0
condition 0 37 0.0
subroutine 10 19 52.6
pod 0 8 0.0
total 40 242 16.5


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