@@ -109,51 +109,91 @@ sub _is_connection_expired {
109
109
sub _is_connection_healthy {
110
110
my ($self ) = @_ ;
111
111
112
+ return 0 unless $self -> _basic_connection_checks_pass();
113
+ return 1 unless $self -> _should_perform_health_check();
114
+
115
+ return $self -> _perform_and_cache_health_check();
116
+ }
117
+
118
+ sub _basic_connection_checks_pass {
119
+ my ($self ) = @_ ;
120
+
112
121
return 0 unless $self -> _has_connection && $self -> _authenticated;
113
122
return 0 if $self -> _is_connection_expired;
114
123
115
- # Skip health check if disabled or recently checked
116
- return 1 unless $self -> health_check_enabled;
124
+ return 1;
125
+ }
126
+
127
+ sub _should_perform_health_check {
128
+ my ($self ) = @_ ;
117
129
118
- if ($self -> _last_health_check) {
119
- my $time_since_check = time () - $self -> _last_health_check;
120
- return 1 if $time_since_check < 30; # Skip check if done within 30 seconds
121
- }
130
+ return 0 unless $self -> health_check_enabled;
131
+ return 0 if $self -> _is_recent_health_check_cached();
132
+
133
+ return 1;
134
+ }
135
+
136
+ sub _is_recent_health_check_cached {
137
+ my ($self ) = @_ ;
138
+
139
+ return 0 unless $self -> _last_health_check;
140
+
141
+ my $time_since_check = time () - $self -> _last_health_check;
142
+ return $time_since_check < 30; # Cache for 30 seconds
143
+ }
144
+
145
+ sub _perform_and_cache_health_check {
146
+ my ($self ) = @_ ;
147
+
148
+ my $health_result = $self -> _execute_health_check_command();
149
+ $self -> _last_health_check(time ());
150
+
151
+ return $health_result ;
152
+ }
153
+
154
+ sub _execute_health_check_command {
155
+ my ($self ) = @_ ;
122
156
123
- # Perform actual health check with a simple command
124
- my $health_result = eval {
157
+ my $result = eval {
125
158
my $ssh2 = $self -> _ssh2;
126
159
my $channel = $ssh2 -> channel();
127
160
return 0 unless $channel ;
128
161
129
- # Use a simple echo command as health check
130
- my $test_command = ' echo "health_check"' ;
131
- return 0 unless $channel -> exec ($test_command );
132
-
133
- # Try to read response
134
- my $output = ' ' ;
135
- my $timeout = time () + 5; # 5 second timeout for health check
136
-
137
- while (time () < $timeout ) {
138
- my $buffer ;
139
- my $bytes = $channel -> read ($buffer , 1024);
140
- last if $bytes <= 0;
141
- $output .= $buffer ;
142
- last if $output =~ / health_check/ ;
143
- }
144
-
145
- $channel -> close ();
146
- return $output =~ / health_check/ ;
162
+ return $self -> _run_echo_health_check($channel );
147
163
};
148
164
149
- $self -> _last_health_check(time ());
165
+ return 0 if $@ || !$result ;
166
+ return 1;
167
+ }
168
+
169
+ sub _run_echo_health_check {
170
+ my ($self , $channel ) = @_ ;
150
171
151
- if ($@ || !$health_result ) {
152
- # Health check failed, connection is unhealthy
153
- return 0;
172
+ # Use a simple echo command as health check
173
+ my $test_command = ' echo "health_check"' ;
174
+ return 0 unless $channel -> exec ($test_command );
175
+
176
+ my $output = $self -> _read_health_check_output($channel );
177
+ $channel -> close ();
178
+
179
+ return $output =~ / health_check/ ;
180
+ }
181
+
182
+ sub _read_health_check_output {
183
+ my ($self , $channel ) = @_ ;
184
+
185
+ my $output = ' ' ;
186
+ my $timeout = time () + 5; # 5 second timeout for health check
187
+
188
+ while (time () < $timeout ) {
189
+ my $buffer ;
190
+ my $bytes = $channel -> read ($buffer , 1024);
191
+ last if $bytes <= 0;
192
+ $output .= $buffer ;
193
+ last if $output =~ / health_check/ ;
154
194
}
155
195
156
- return 1 ;
196
+ return $output ;
157
197
}
158
198
159
199
sub test_password_connection {
@@ -216,64 +256,98 @@ sub _execute_command_with_retry {
216
256
my ($self , $command , $max_retries ) = @_ ;
217
257
218
258
for my $attempt (1..$max_retries ) {
219
- # Ensure we have an authenticated connection
220
- unless ($self -> _ensure_authenticated()) {
221
- return {
222
- output => " Authentication failed" ,
223
- success => 0,
224
- exit_code => 255,
225
- };
226
- }
259
+ my $result = $self -> _attempt_command_execution($command );
227
260
228
- my $result = eval {
229
- my $ssh2 = $self -> _ssh2;
230
- my $channel = $ssh2 -> channel();
231
-
232
- unless ($channel ) {
233
- croak " Failed to create channel: " . ($ssh2 -> error || ' Unknown error' );
234
- }
235
-
236
- # Execute command
237
- unless ($channel -> exec ($command )) {
238
- croak " Failed to execute command '$command ': " . ($ssh2 -> error || ' Unknown error' );
239
- }
240
-
241
- # Read output with timeout
242
- my $output = $self -> _read_channel_output($channel );
243
-
244
- # Wait for command completion and get exit status
245
- $channel -> wait_closed();
246
- my $exit_code = $channel -> exit_status();
247
- $exit_code = 0 unless defined $exit_code ;
248
-
249
- return {
250
- output => $output ,
251
- success => $exit_code == 0,
252
- exit_code => $exit_code ,
253
- };
254
- };
255
-
256
- # If command succeeded, return result
257
- if (!$@ && $result ) {
258
- return $result ;
259
- }
261
+ # If command succeeded (result exists and is defined), return it
262
+ return $result if $result ;
260
263
261
264
# Command failed - check if we should retry
262
- if ($attempt < $max_retries && $self -> auto_reconnect) {
263
- # Clear connection and try again
264
- $self -> _clear_connection();
265
- $self -> _clear_connection_timestamp();
266
- $self -> _authenticated(0);
267
- next ;
268
- }
269
-
270
- # No more retries or auto_reconnect disabled
271
- return {
272
- output => " SSH command execution failed: $@ " ,
273
- success => 0,
274
- exit_code => 255,
275
- };
265
+ last unless $self -> _should_retry_command($attempt , $max_retries );
266
+ $self -> _prepare_for_retry();
276
267
}
268
+
269
+ # No more retries or auto_reconnect disabled
270
+ return $self -> _create_failure_result(" SSH command execution failed: " . ($@ || " Unknown error" ));
271
+ }
272
+
273
+ sub _attempt_command_execution {
274
+ my ($self , $command ) = @_ ;
275
+
276
+ # Ensure we have an authenticated connection
277
+ return $self -> _create_failure_result(" Authentication failed" )
278
+ unless $self -> _ensure_authenticated();
279
+
280
+ my $result = eval { $self -> _execute_single_command($command ) };
281
+
282
+ # Return result if successful (even if command failed with non-zero exit)
283
+ return $result if !$@ && $result ;
284
+
285
+ # eval failed, return undef to trigger retry logic
286
+ return undef ;
287
+ }
288
+
289
+ sub _execute_single_command {
290
+ my ($self , $command ) = @_ ;
291
+
292
+ my $ssh2 = $self -> _ssh2;
293
+ my $channel = $self -> _create_command_channel($ssh2 , $command );
294
+
295
+ my $output = $self -> _read_channel_output($channel );
296
+ my $exit_code = $self -> _get_command_exit_code($channel );
297
+
298
+ return {
299
+ output => $output ,
300
+ success => $exit_code == 0,
301
+ exit_code => $exit_code ,
302
+ };
303
+ }
304
+
305
+ sub _create_command_channel {
306
+ my ($self , $ssh2 , $command ) = @_ ;
307
+
308
+ my $channel = $ssh2 -> channel();
309
+ croak " Failed to create channel: " . ($ssh2 -> error || ' Unknown error' )
310
+ unless $channel ;
311
+
312
+ croak " Failed to execute command '$command ': " . ($ssh2 -> error || ' Unknown error' )
313
+ unless $channel -> exec ($command );
314
+
315
+ return $channel ;
316
+ }
317
+
318
+ sub _get_command_exit_code {
319
+ my ($self , $channel ) = @_ ;
320
+
321
+ $channel -> wait_closed();
322
+ my $exit_code = $channel -> exit_status();
323
+ return defined $exit_code ? $exit_code : 0;
324
+ }
325
+
326
+ sub _should_retry_command {
327
+ my ($self , $attempt , $max_retries ) = @_ ;
328
+
329
+ return 0 if $attempt >= $max_retries ;
330
+ return 0 unless $self -> auto_reconnect;
331
+
332
+ return 1;
333
+ }
334
+
335
+ sub _prepare_for_retry {
336
+ my ($self ) = @_ ;
337
+
338
+ $self -> _clear_connection();
339
+ $self -> _clear_connection_timestamp();
340
+ $self -> _authenticated(0);
341
+ }
342
+
343
+ sub _create_failure_result {
344
+ my ($self , $error_message ) = @_ ;
345
+
346
+ return {
347
+ output => $error_message ,
348
+ success => 0,
349
+ exit_code => 255,
350
+ };
277
351
}
278
352
279
353
sub execute_command_with_sudo {
@@ -316,17 +390,24 @@ sub force_reconnect {
316
390
sub _ensure_authenticated {
317
391
my ($self ) = @_ ;
318
392
319
- # Check if existing connection is healthy
320
- if ($self -> _is_connection_healthy()) {
321
- return 1;
322
- }
393
+ return 1 if $self -> _is_connection_healthy();
394
+
395
+ $self -> _reset_unhealthy_connection();
396
+ return $self -> _attempt_authentication();
397
+ }
398
+
399
+ sub _reset_unhealthy_connection {
400
+ my ($self ) = @_ ;
323
401
324
- # Connection is unhealthy or expired, clear it and reconnect
325
402
if ($self -> _has_connection) {
326
403
$self -> _clear_connection();
327
404
$self -> _clear_connection_timestamp();
328
405
$self -> _authenticated(0);
329
406
}
407
+ }
408
+
409
+ sub _attempt_authentication {
410
+ my ($self ) = @_ ;
330
411
331
412
# Try password authentication first
332
413
return 1 if $self -> test_password_connection();
@@ -356,38 +437,69 @@ sub _find_public_key_path {
356
437
sub _read_channel_output {
357
438
my ($self , $channel ) = @_ ;
358
439
359
- my $output = ' ' ;
360
- my $buffer ;
440
+ $self -> _setup_non_blocking_read($channel );
441
+ my $output = $self -> _read_with_timeout($channel );
442
+ $output .= $self -> _read_remaining_data($channel );
443
+
444
+ return $output ;
445
+ }
446
+
447
+ sub _setup_non_blocking_read {
448
+ my ($self , $channel ) = @_ ;
361
449
362
- # Set up non-blocking read with timeout
363
450
$channel -> blocking(0);
451
+ }
452
+
453
+ sub _read_with_timeout {
454
+ my ($self , $channel ) = @_ ;
364
455
456
+ my $output = ' ' ;
365
457
my $start_time = time ();
366
458
my $timeout = $self -> command_timeout;
367
459
368
460
while (time () - $start_time < $timeout ) {
369
- my $bytes_read = $channel -> read ( $buffer , 4096 );
461
+ my $buffer = $self -> _try_read_chunk( $channel );
370
462
371
- if (defined $bytes_read && $bytes_read > 0) {
463
+ if (defined $buffer && length ( $buffer ) > 0) {
372
464
$output .= $buffer ;
373
465
next ;
374
466
}
375
467
376
- # Check if channel is closed
377
468
last if $channel -> eof ();
378
-
379
- # Small sleep to prevent busy waiting
380
- select (undef , undef , undef , 0.1);
469
+ $self -> _small_delay_to_prevent_busy_waiting();
381
470
}
382
471
472
+ return $output ;
473
+ }
474
+
475
+ sub _try_read_chunk {
476
+ my ($self , $channel ) = @_ ;
477
+
478
+ my $buffer ;
479
+ my $bytes_read = $channel -> read ($buffer , 4096);
480
+
481
+ return (defined $bytes_read && $bytes_read > 0) ? $buffer : undef ;
482
+ }
483
+
484
+ sub _small_delay_to_prevent_busy_waiting {
485
+ my ($self ) = @_ ;
486
+
487
+ select (undef , undef , undef , 0.1);
488
+ }
489
+
490
+ sub _read_remaining_data {
491
+ my ($self , $channel ) = @_ ;
492
+
493
+ my $remaining_output = ' ' ;
494
+
383
495
# Final blocking read to get any remaining data
384
496
$channel -> blocking(1);
385
- while (my $bytes_read = $channel -> read ($buffer , 4096)) {
497
+ while (my $bytes_read = $channel -> read (my $buffer , 4096)) {
386
498
last unless defined $bytes_read && $bytes_read > 0;
387
- $output .= $buffer ;
499
+ $remaining_output .= $buffer ;
388
500
}
389
501
390
- return $output ;
502
+ return $remaining_output ;
391
503
}
392
504
393
505
# Cleanup on destruction
0 commit comments