File Coverage

blib/lib/App/RemoteCommand/SSH.pm
Criterion Covered Total %
statement 29 127 22.8
branch 0 52 0.0
condition 0 39 0.0
subroutine 10 19 52.6
pod 0 8 0.0
total 39 245 15.9


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