@@ -109,51 +109,91 @@ sub _is_connection_expired {
109109sub _is_connection_healthy {
110110 my ($self ) = @_ ;
111111
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+
112121 return 0 unless $self -> _has_connection && $self -> _authenticated;
113122 return 0 if $self -> _is_connection_expired;
114123
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 ) = @_ ;
117129
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 ) = @_ ;
122156
123- # Perform actual health check with a simple command
124- my $health_result = eval {
157+ my $result = eval {
125158 my $ssh2 = $self -> _ssh2;
126159 my $channel = $ssh2 -> channel();
127160 return 0 unless $channel ;
128161
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 );
147163 };
148164
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 ) = @_ ;
150171
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/ ;
154194 }
155195
156- return 1 ;
196+ return $output ;
157197}
158198
159199sub test_password_connection {
@@ -216,64 +256,98 @@ sub _execute_command_with_retry {
216256 my ($self , $command , $max_retries ) = @_ ;
217257
218258 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 );
227260
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 ;
260263
261264 # 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();
276267 }
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+ };
277351}
278352
279353sub execute_command_with_sudo {
@@ -316,17 +390,24 @@ sub force_reconnect {
316390sub _ensure_authenticated {
317391 my ($self ) = @_ ;
318392
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 ) = @_ ;
323401
324- # Connection is unhealthy or expired, clear it and reconnect
325402 if ($self -> _has_connection) {
326403 $self -> _clear_connection();
327404 $self -> _clear_connection_timestamp();
328405 $self -> _authenticated(0);
329406 }
407+ }
408+
409+ sub _attempt_authentication {
410+ my ($self ) = @_ ;
330411
331412 # Try password authentication first
332413 return 1 if $self -> test_password_connection();
@@ -356,38 +437,69 @@ sub _find_public_key_path {
356437sub _read_channel_output {
357438 my ($self , $channel ) = @_ ;
358439
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 ) = @_ ;
361449
362- # Set up non-blocking read with timeout
363450 $channel -> blocking(0);
451+ }
452+
453+ sub _read_with_timeout {
454+ my ($self , $channel ) = @_ ;
364455
456+ my $output = ' ' ;
365457 my $start_time = time ();
366458 my $timeout = $self -> command_timeout;
367459
368460 while (time () - $start_time < $timeout ) {
369- my $bytes_read = $channel -> read ( $buffer , 4096 );
461+ my $buffer = $self -> _try_read_chunk( $channel );
370462
371- if (defined $bytes_read && $bytes_read > 0) {
463+ if (defined $buffer && length ( $buffer ) > 0) {
372464 $output .= $buffer ;
373465 next ;
374466 }
375467
376- # Check if channel is closed
377468 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();
381470 }
382471
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+
383495 # Final blocking read to get any remaining data
384496 $channel -> blocking(1);
385- while (my $bytes_read = $channel -> read ($buffer , 4096)) {
497+ while (my $bytes_read = $channel -> read (my $buffer , 4096)) {
386498 last unless defined $bytes_read && $bytes_read > 0;
387- $output .= $buffer ;
499+ $remaining_output .= $buffer ;
388500 }
389501
390- return $output ;
502+ return $remaining_output ;
391503}
392504
393505# Cleanup on destruction
0 commit comments