11//! Blocking shell command execution.
22
3- use super :: { BashOutput , PIPE_BUFFER_CAPACITY } ;
3+ use super :: {
4+ timeout_error_with_kill_failure, timeout_message_with_buffered_output, BashOutput ,
5+ PIPE_BUFFER_CAPACITY ,
6+ } ;
47use crate :: error:: { ToolError , ToolResult } ;
58use process_wrap:: std:: * ;
69use std:: io:: Read ;
@@ -9,6 +12,12 @@ use std::process::Stdio;
912use std:: thread;
1013use std:: time:: { Duration , Instant } ;
1114
15+ enum WaitOutcome {
16+ Exited ( std:: process:: ExitStatus ) ,
17+ TimedOut { kill_error : Option < std:: io:: Error > } ,
18+ WaitError ( std:: io:: Error ) ,
19+ }
20+
1221/// Executes a shell command with optional working directory and timeout.
1322///
1423/// Uses bash on Unix, cmd on Windows. Process tree is killed on timeout via:
@@ -90,22 +99,20 @@ pub fn execute_command(
9099
91100 let start = Instant :: now ( ) ;
92101
93- // Poll for completion with timeout
94- let exit_status = loop {
102+ // Poll for completion with timeout.
103+ let wait_outcome = loop {
95104 match child. try_wait ( ) {
96- Ok ( Some ( status) ) => break Ok ( status) ,
105+ Ok ( Some ( status) ) => break WaitOutcome :: Exited ( status) ,
97106 Ok ( None ) => {
98107 if start. elapsed ( ) >= timeout {
99108 // Kill entire process tree via Job Object (Windows) or process group (Unix)
100- let _ = child. kill ( ) ;
101- break Err ( ToolError :: Timeout ( format ! (
102- "command timed out after {}ms" ,
103- timeout. as_millis( )
104- ) ) ) ;
109+ break WaitOutcome :: TimedOut {
110+ kill_error : child. kill ( ) . err ( ) ,
111+ } ;
105112 }
106113 thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
107114 }
108- Err ( e) => break Err ( ToolError :: Execution ( e . to_string ( ) ) ) ,
115+ Err ( e) => break WaitOutcome :: WaitError ( e ) ,
109116 }
110117 } ;
111118
@@ -118,13 +125,17 @@ pub fn execute_command(
118125 . map_err ( |_| ToolError :: Execution ( "stderr reader thread panicked" . to_string ( ) ) ) ?;
119126
120127 // Return result
121- match exit_status {
122- Ok ( status) => Ok ( BashOutput {
128+ match wait_outcome {
129+ WaitOutcome :: Exited ( status) => Ok ( BashOutput {
123130 exit_code : status. code ( ) ,
124131 stdout : String :: from_utf8_lossy ( & stdout_data) . into_owned ( ) ,
125132 stderr : String :: from_utf8_lossy ( & stderr_data) . into_owned ( ) ,
126133 } ) ,
127- Err ( e) => Err ( e) ,
134+ WaitOutcome :: TimedOut { kill_error } => Err ( timeout_error_with_kill_failure (
135+ timeout_message_with_buffered_output ( timeout, & stdout_data, & stderr_data) ,
136+ kill_error. map ( |e| e. to_string ( ) ) ,
137+ ) ) ,
138+ WaitOutcome :: WaitError ( e) => Err ( ToolError :: Execution ( e. to_string ( ) ) ) ,
128139 }
129140}
130141
@@ -166,7 +177,10 @@ mod tests {
166177 } ;
167178
168179 let result = execute_command ( cmd, None , Duration :: from_millis ( 100 ) ) ;
169- assert ! ( matches!( result, Err ( ToolError :: Timeout ( _) ) ) ) ;
180+ assert ! ( matches!(
181+ result,
182+ Err ( ToolError :: Timeout ( _) | ToolError :: TimeoutWithKillFailure { .. } )
183+ ) ) ;
170184 }
171185
172186 #[ test]
0 commit comments