1+ use std:: collections:: VecDeque ;
12use std:: io:: Write ;
23use std:: process:: Stdio ;
34
4- use bstr:: ByteSlice ;
55use crossterm:: style:: {
66 self ,
77 Color ,
@@ -16,77 +16,113 @@ use eyre::{
1616} ;
1717use fig_os_shim:: Context ;
1818use serde:: Deserialize ;
19+ use tokio:: io:: AsyncBufReadExt ;
20+ use tokio:: select;
21+ use tracing:: error;
1922
2023use super :: {
2124 InvokeOutput ,
2225 MAX_TOOL_RESPONSE_SIZE ,
2326 OutputKind ,
2427} ;
28+ use crate :: cli:: chat:: truncate_safe;
2529
2630#[ derive( Debug , Clone , Deserialize ) ]
2731pub struct ExecuteBash {
2832 pub command : String ,
29- pub interactive : Option < bool > ,
3033}
3134
3235impl ExecuteBash {
3336 pub async fn invoke ( & self , mut updates : impl Write ) -> Result < InvokeOutput > {
34- queue ! (
37+ execute ! (
3538 updates,
3639 style:: SetForegroundColor ( Color :: Green ) ,
3740 style:: Print ( format!( "Executing `{}`" , & self . command) ) ,
3841 style:: ResetColor ,
3942 style:: Print ( "\n " ) ,
4043 ) ?;
4144
42- let ( stdout, stderr) = match self . interactive {
43- Some ( true ) => ( Stdio :: inherit ( ) , Stdio :: inherit ( ) ) ,
44- _ => ( Stdio :: piped ( ) , Stdio :: piped ( ) ) ,
45- } ;
46-
47- let output = tokio:: process:: Command :: new ( "bash" )
45+ // We need to maintain a handle on stderr and stdout, but pipe it to the terminal as well
46+ let mut child = tokio:: process:: Command :: new ( "bash" )
4847 . arg ( "-c" )
4948 . arg ( & self . command )
5049 . stdin ( Stdio :: inherit ( ) )
51- . stdout ( stdout )
52- . stderr ( stderr )
50+ . stdout ( Stdio :: piped ( ) )
51+ . stderr ( Stdio :: piped ( ) )
5352 . spawn ( )
54- . wrap_err_with ( || format ! ( "Unable to spawn command '{}'" , & self . command) ) ?
55- . wait_with_output ( )
56- . await
57- . wrap_err_with ( || format ! ( "Unable to wait on subprocess for command '{}'" , & self . command) ) ?;
58- let status = output. status . code ( ) . unwrap_or ( 0 ) . to_string ( ) ;
59- let stdout = output. stdout . to_str_lossy ( ) ;
60- let stderr = output. stderr . to_str_lossy ( ) ;
61-
62- if let Some ( false ) = self . interactive {
63- execute ! ( updates, style:: Print ( & stdout) ) ?;
53+ . wrap_err_with ( || format ! ( "Unable to spawn command '{}'" , & self . command) ) ?;
54+
55+ let stdout = child. stdout . take ( ) . unwrap ( ) ;
56+ let stdout = tokio:: io:: BufReader :: new ( stdout) ;
57+ let mut stdout = stdout. lines ( ) ;
58+
59+ let stderr = child. stderr . take ( ) . unwrap ( ) ;
60+ let stderr = tokio:: io:: BufReader :: new ( stderr) ;
61+ let mut stderr = stderr. lines ( ) ;
62+
63+ const LINE_COUNT : usize = 1024 ;
64+ let mut stdout_buf = VecDeque :: with_capacity ( LINE_COUNT ) ;
65+ let mut stderr_buf = VecDeque :: with_capacity ( LINE_COUNT ) ;
66+
67+ let exit_status = loop {
68+ child. stdin . take ( ) ;
69+
70+ select ! {
71+ biased;
72+ line = stdout. next_line( ) => match line {
73+ Ok ( Some ( line) ) => {
74+ writeln!( updates, "{line}" ) ?;
75+ if stdout_buf. len( ) >= LINE_COUNT {
76+ stdout_buf. pop_front( ) ;
77+ }
78+ stdout_buf. push_back( line) ;
79+ } ,
80+ Ok ( None ) => { } ,
81+ Err ( err) => error!( %err, "Failed to read stdout of child process" ) ,
82+ } ,
83+ line = stderr. next_line( ) => match line {
84+ Ok ( Some ( line) ) => {
85+ writeln!( updates, "{line}" ) ?;
86+ if stderr_buf. len( ) >= LINE_COUNT {
87+ stderr_buf. pop_front( ) ;
88+ }
89+ stderr_buf. push_back( line) ;
90+ } ,
91+ Ok ( None ) => { } ,
92+ Err ( err) => error!( %err, "Failed to read stderr of child process" ) ,
93+ } ,
94+ exit_status = child. wait( ) => {
95+ break exit_status;
96+ } ,
97+ } ;
6498 }
99+ . wrap_err_with ( || format ! ( "No exit status for '{}'" , & self . command) ) ?;
65100
66- let stdout = format ! (
67- "{}{}" ,
68- & stdout[ 0 ..stdout. len( ) . min( MAX_TOOL_RESPONSE_SIZE / 3 ) ] ,
69- if stdout. len( ) > MAX_TOOL_RESPONSE_SIZE / 3 {
70- " ... truncated"
71- } else {
72- ""
73- }
74- ) ;
75-
76- let stderr = format ! (
77- "{}{}" ,
78- & stderr[ 0 ..stderr. len( ) . min( MAX_TOOL_RESPONSE_SIZE / 3 ) ] ,
79- if stderr. len( ) > MAX_TOOL_RESPONSE_SIZE / 3 {
80- " ... truncated"
81- } else {
82- ""
83- }
84- ) ;
101+ updates. flush ( ) ?;
102+
103+ let stdout = stdout_buf. into_iter ( ) . collect :: < Vec < _ > > ( ) . join ( "\n " ) ;
104+ let stderr = stderr_buf. into_iter ( ) . collect :: < Vec < _ > > ( ) . join ( "\n " ) ;
85105
86106 let output = serde_json:: json!( {
87- "exit_status" : status,
88- "stdout" : stdout,
89- "stderr" : stderr,
107+ "exit_status" : exit_status. code( ) . unwrap_or( 0 ) . to_string( ) ,
108+ "stdout" : format!(
109+ "{}{}" ,
110+ truncate_safe( & stdout, MAX_TOOL_RESPONSE_SIZE / 3 ) ,
111+ if stdout. len( ) > MAX_TOOL_RESPONSE_SIZE / 3 {
112+ " ... truncated"
113+ } else {
114+ ""
115+ }
116+ ) ,
117+ "stderr" : format!(
118+ "{}{}" ,
119+ truncate_safe( & stderr, MAX_TOOL_RESPONSE_SIZE / 3 ) ,
120+ if stderr. len( ) > MAX_TOOL_RESPONSE_SIZE / 3 {
121+ " ... truncated"
122+ } else {
123+ ""
124+ }
125+ ) ,
90126 } ) ;
91127
92128 Ok ( InvokeOutput {
@@ -127,7 +163,6 @@ mod tests {
127163 // Verifying stdout
128164 let v = serde_json:: json!( {
129165 "command" : "echo Hello, world!" ,
130- "interactive" : false
131166 } ) ;
132167 let out = serde_json:: from_value :: < ExecuteBash > ( v)
133168 . unwrap ( )
@@ -137,7 +172,7 @@ mod tests {
137172
138173 if let OutputKind :: Json ( json) = out. output {
139174 assert_eq ! ( json. get( "exit_status" ) . unwrap( ) , & 0 . to_string( ) ) ;
140- assert_eq ! ( json. get( "stdout" ) . unwrap( ) , "Hello, world!\n " ) ;
175+ assert_eq ! ( json. get( "stdout" ) . unwrap( ) , "Hello, world!" ) ;
141176 assert_eq ! ( json. get( "stderr" ) . unwrap( ) , "" ) ;
142177 } else {
143178 panic ! ( "Expected JSON output" ) ;
@@ -157,7 +192,7 @@ mod tests {
157192 if let OutputKind :: Json ( json) = out. output {
158193 assert_eq ! ( json. get( "exit_status" ) . unwrap( ) , & 0 . to_string( ) ) ;
159194 assert_eq ! ( json. get( "stdout" ) . unwrap( ) , "" ) ;
160- assert_eq ! ( json. get( "stderr" ) . unwrap( ) , "Hello, world!\n " ) ;
195+ assert_eq ! ( json. get( "stderr" ) . unwrap( ) , "Hello, world!" ) ;
161196 } else {
162197 panic ! ( "Expected JSON output" ) ;
163198 }
0 commit comments