11//! Custom wrapper for Command that supports timeouts and contains custom error handling.
22
33use crate :: { error:: CommandError , TmcError } ;
4- use std:: fmt;
4+ use os_pipe:: pipe;
5+ #[ cfg( unix) ]
6+ use shared_child:: unix:: SharedChildExt ;
7+ use shared_child:: SharedChild ;
58use std:: io:: Read ;
69use std:: ops:: { Deref , DerefMut } ;
710use std:: path:: PathBuf ;
8- use std:: process:: { Command , ExitStatus , Output , Stdio } ;
11+ use std:: process:: { Command , ExitStatus , Output } ;
12+ use std:: sync:: Arc ;
913use std:: thread;
10- use std:: time:: { Duration , Instant } ;
14+ use std:: time:: Duration ;
15+ use std:: { fmt, sync:: Mutex } ;
1116
1217// todo: collect args?
18+ #[ derive( Debug ) ]
1319pub struct TmcCommand {
1420 name : String ,
1521 path : PathBuf ,
@@ -106,59 +112,100 @@ impl TmcCommand {
106112 }
107113
108114 /// Waits with the given timeout. Sets stdout and stderr in order to capture them after erroring.
109- pub fn wait_with_timeout ( & mut self , timeout : Duration ) -> Result < OutputWithTimeout , TmcError > {
110- // spawn process and init timer
111- let mut child = self
112- . stdout ( Stdio :: piped ( ) )
113- . stderr ( Stdio :: piped ( ) )
114- . spawn ( )
115- . map_err ( |e| TmcError :: Command ( CommandError :: Spawn ( self . to_string ( ) , e) ) ) ?;
116- let timer = Instant :: now ( ) ;
117-
118- loop {
119- match child. try_wait ( ) . map_err ( TmcError :: Process ) ? {
120- Some ( _exit_status) => {
121- // done, get output
122- return child
123- . wait_with_output ( )
124- . map ( OutputWithTimeout :: Output )
125- . map_err ( |e| {
126- if let std:: io:: ErrorKind :: NotFound = e. kind ( ) {
127- TmcError :: Command ( CommandError :: NotFound {
128- name : self . name . clone ( ) ,
129- path : self . path . clone ( ) ,
130- source : e,
131- } )
132- } else {
133- TmcError :: Command ( CommandError :: FailedToRun ( self . to_string ( ) , e) )
134- }
135- } ) ;
115+ pub fn wait_with_timeout ( mut self , timeout : Duration ) -> Result < OutputWithTimeout , TmcError > {
116+ let name = self . name . clone ( ) ;
117+ let path = self . path . clone ( ) ;
118+ let self_string = self . to_string ( ) ;
119+ let self_string2 = self . to_string ( ) ;
120+
121+ let ( mut stdout_reader, stdout_writer) = pipe ( ) . unwrap ( ) ;
122+ let ( mut stderr_reader, stderr_writer) = pipe ( ) . unwrap ( ) ;
123+
124+ let ( process_result, timed_out) = {
125+ let mut command = self . command ;
126+ command. stdout ( stdout_writer) . stderr ( stderr_writer) ;
127+
128+ let shared_child = SharedChild :: spawn ( & mut command)
129+ . map_err ( |e| TmcError :: Command ( CommandError :: Spawn ( self_string, e) ) ) ?;
130+ let child_arc = Arc :: new ( shared_child) ;
131+
132+ let running = Arc :: new ( Mutex :: new ( true ) ) ;
133+ let running_clone = running. clone ( ) ;
134+ let timed_out = Arc :: new ( Mutex :: new ( false ) ) ;
135+
136+ let child_arc_clone = child_arc. clone ( ) ;
137+ let timed_out_clone = timed_out. clone ( ) ;
138+ let _timeout_checker = thread:: spawn ( move || {
139+ thread:: sleep ( timeout) ;
140+
141+ if !running_clone. lock ( ) . unwrap ( ) . clone ( ) {
142+ return ;
136143 }
137- None => {
138- // still running, check timeout
139- if timer. elapsed ( ) > timeout {
140- log:: warn!( "command {} timed out" , self . name) ;
141- // todo: cleaner method for killing
142- child. kill ( ) . map_err ( TmcError :: Process ) ?;
143-
144- let mut stdout = vec ! [ ] ;
145- let mut stderr = vec ! [ ] ;
146- let stdout_handle = child. stdout . as_mut ( ) . unwrap ( ) ;
147- let stderr_handle = child. stderr . as_mut ( ) . unwrap ( ) ;
148- stdout_handle
149- . read_to_end ( & mut stdout)
150- . map_err ( TmcError :: ReadStdio ) ?;
151- stderr_handle
152- . read_to_end ( & mut stderr)
153- . map_err ( TmcError :: ReadStdio ) ?;
154- return Ok ( OutputWithTimeout :: Timeout { stdout, stderr } ) ;
155- }
156-
157- // TODO: gradually increase sleep duration?
158- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
144+ let mut timed_out_handle = timed_out_clone. lock ( ) . unwrap ( ) ;
145+ * timed_out_handle = true ;
146+
147+ #[ cfg( unix) ]
148+ {
149+ // Ask process to terminate nicely
150+ let _res2 = child_arc_clone. send_signal ( 15 ) ;
151+ thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
152+ }
153+ // Force kill the process
154+ let _res = child_arc_clone. kill ( ) ;
155+ } ) ;
156+
157+ let process_result = child_arc. wait ( ) ;
158+ let mut running_handle = running. lock ( ) . unwrap ( ) ;
159+ * running_handle = true ;
160+ ( process_result, timed_out)
161+ } ;
162+
163+ // Very important when using pipes: This parent process is still
164+ // holding its copies of the write ends, and we have to close them
165+ // before we read, otherwise the read end will never report EOF.
166+ // The block above drops everything unnecessary
167+
168+ let res = match process_result {
169+ Ok ( exit_status) => {
170+ let mut stdout = vec ! [ ] ;
171+ let mut stderr = vec ! [ ] ;
172+ stdout_reader
173+ . read_to_end ( & mut stdout)
174+ . map_err ( TmcError :: ReadStdio ) ?;
175+ stderr_reader
176+ . read_to_end ( & mut stderr)
177+ . map_err ( TmcError :: ReadStdio ) ?;
178+
179+ Output {
180+ status : exit_status,
181+ stdout : stdout,
182+ stderr : stderr,
183+ }
184+ }
185+ Err ( e) => {
186+ if let std:: io:: ErrorKind :: NotFound = e. kind ( ) {
187+ return Err ( TmcError :: Command ( CommandError :: NotFound {
188+ name : name,
189+ path : path,
190+ source : e,
191+ } ) ) ;
192+ } else {
193+ return Err ( TmcError :: Command ( CommandError :: FailedToRun (
194+ self_string2,
195+ e,
196+ ) ) ) ;
159197 }
160198 }
199+ } ;
200+
201+ if timed_out. lock ( ) . unwrap ( ) . clone ( ) {
202+ return Ok ( OutputWithTimeout :: Timeout {
203+ stdout : res. stdout ,
204+ stderr : res. stderr ,
205+ } ) ;
161206 }
207+
208+ return Ok ( OutputWithTimeout :: Output ( res) ) ;
162209 }
163210}
164211
@@ -175,7 +222,7 @@ impl DerefMut for TmcCommand {
175222 & mut self . command
176223 }
177224}
178-
225+ # [ derive ( Debug ) ]
179226pub enum OutputWithTimeout {
180227 Output ( Output ) ,
181228 Timeout { stdout : Vec < u8 > , stderr : Vec < u8 > } ,
@@ -207,7 +254,7 @@ mod test {
207254 let res = cmd. wait_with_timeout ( Duration :: from_millis ( 100 ) ) . unwrap ( ) ;
208255 if let OutputWithTimeout :: Timeout { .. } = res {
209256 } else {
210- panic ! ( "unexpected result" ) ;
257+ panic ! ( format! ( "Unexpected result {:?}" , res ) ) ;
211258 }
212259 }
213260}
0 commit comments