File Coverage

blib/lib/App/RemoteCommand/SSH.pm
Criterion Covered Total %
statement 32 149 21.4
branch 0 52 0.0
condition 0 39 0.0
subroutine 11 20 55.0
pod 0 8 0.0
total 43 268 16.0


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