Skip to content

Commit 7fb4b22

Browse files
committed
create threads to read stdout and stderr while a command is running
1 parent ebbc6ce commit 7fb4b22

File tree

1 file changed

+42
-29
lines changed

1 file changed

+42
-29
lines changed

tmc-langs-framework/src/command.rs

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ use crate::{
44
error::{CommandError, FileIo},
55
file_util, TmcError,
66
};
7-
use std::ffi::OsStr;
87
use std::fs::File;
9-
use std::io::{Read, Seek, SeekFrom};
8+
use std::io::Read;
109
use std::time::Duration;
10+
use std::{ffi::OsStr, thread::JoinHandle};
1111
use subprocess::{Exec, ExitStatus, PopenError};
1212

1313
/// Wrapper around subprocess::Exec
@@ -18,21 +18,6 @@ pub struct TmcCommand {
1818
stderr: Option<File>,
1919
}
2020

21-
fn read_output(
22-
file_handle: Option<File>,
23-
exec_output: Option<&mut File>,
24-
) -> Result<Vec<u8>, TmcError> {
25-
let mut v = vec![];
26-
if let Some(mut file) = file_handle {
27-
let _ = file.seek(SeekFrom::Start(0)); // ignore errors
28-
file.read_to_end(&mut v).map_err(FileIo::Generic)?;
29-
} else if let Some(file) = exec_output {
30-
let _ = file.seek(SeekFrom::Start(0)); // ignore errors
31-
file.read_to_end(&mut v).map_err(FileIo::Generic)?;
32-
}
33-
Ok(v)
34-
}
35-
3621
impl TmcCommand {
3722
/// Creates a new command
3823
pub fn new(cmd: impl AsRef<OsStr>) -> Self {
@@ -44,6 +29,7 @@ impl TmcCommand {
4429
}
4530

4631
/// Creates a new command with stdout and stderr redirected to files
32+
// todo: remember why this is useful
4733
pub fn new_with_file_io(cmd: impl AsRef<OsStr>) -> Result<Self, TmcError> {
4834
let stdout = file_util::temp_file()?;
4935
let stderr = file_util::temp_file()?;
@@ -78,6 +64,8 @@ impl TmcCommand {
7864

7965
// starts executing the command
8066
let mut popen = exec.popen().map_err(|e| popen_to_tmc_err(cmd.clone(), e))?;
67+
let stdout_handle = spawn_reader(stdout, popen.stdout.take());
68+
let stderr_handle = spawn_reader(stderr, popen.stderr.take());
8169

8270
let exit_status = if let Some(timeout) = timeout {
8371
// timeout set
@@ -92,8 +80,12 @@ impl TmcCommand {
9280
popen
9381
.terminate()
9482
.map_err(|e| CommandError::Terminate(cmd.clone(), e))?;
95-
let stdout = read_output(stdout, popen.stdout.as_mut())?;
96-
let stderr = read_output(stderr, popen.stderr.as_mut())?;
83+
let stdout = stdout_handle
84+
.join()
85+
.expect("the thread should not be able to panic");
86+
let stderr = stderr_handle
87+
.join()
88+
.expect("the thread should not be able to panic");
9789
return Err(TmcError::Command(CommandError::TimeOut {
9890
command: cmd,
9991
timeout,
@@ -108,17 +100,21 @@ impl TmcCommand {
108100
};
109101

110102
log::info!("finished executing {}", cmd);
111-
let stdout = read_output(stdout, popen.stdout.as_mut())?;
112-
let stderr = read_output(stderr, popen.stderr.as_mut())?;
103+
let stdout = stdout_handle
104+
.join()
105+
.expect("the thread should not be able to panic");
106+
let stderr = stderr_handle
107+
.join()
108+
.expect("the thread should not be able to panic");
113109

114110
// on success, log stdout trace and stderr debug
115111
// on failure if checked, log warn
116112
// on failure if not checked, log debug
117113
if !exit_status.success() {
118114
// if checked is set, error with failed exit status
119115
if checked {
120-
log::warn!("stdout: {}", String::from_utf8_lossy(&stdout));
121-
log::warn!("stderr: {}", String::from_utf8_lossy(&stderr));
116+
log::warn!("stdout: {}", String::from_utf8_lossy(&stdout).into_owned());
117+
log::warn!("stderr: {}", String::from_utf8_lossy(&stderr).into_owned());
122118
return Err(CommandError::Failed {
123119
command: cmd,
124120
status: exit_status,
@@ -127,12 +123,12 @@ impl TmcCommand {
127123
}
128124
.into());
129125
} else {
130-
log::debug!("stdout: {}", String::from_utf8_lossy(&stdout));
131-
log::debug!("stderr: {}", String::from_utf8_lossy(&stderr));
126+
log::debug!("stdout: {}", String::from_utf8_lossy(&stdout).into_owned());
127+
log::debug!("stderr: {}", String::from_utf8_lossy(&stderr).into_owned());
132128
}
133129
} else {
134-
log::trace!("stdout: {}", String::from_utf8_lossy(&stdout));
135-
log::debug!("stderr: {}", String::from_utf8_lossy(&stderr));
130+
log::trace!("stdout: {}", String::from_utf8_lossy(&stdout).into_owned());
131+
log::debug!("stderr: {}", String::from_utf8_lossy(&stderr).into_owned());
136132
}
137133
Ok(Output {
138134
status: exit_status,
@@ -167,6 +163,23 @@ impl TmcCommand {
167163
}
168164
}
169165

166+
// it's assumed the thread will never panic
167+
fn spawn_reader(temp_file: Option<File>, file: Option<File>) -> JoinHandle<Vec<u8>> {
168+
std::thread::spawn(move || {
169+
if let Some(mut temp_file) = temp_file {
170+
let mut buf = vec![];
171+
let _ = temp_file.read_to_end(&mut buf);
172+
buf
173+
} else if let Some(mut file) = file {
174+
let mut buf = vec![];
175+
let _ = file.read_to_end(&mut buf);
176+
buf
177+
} else {
178+
vec![]
179+
}
180+
})
181+
}
182+
170183
// convenience function to convert an error while checking for command not found error
171184
fn popen_to_tmc_err(cmd: String, err: PopenError) -> TmcError {
172185
if let PopenError::IoError(io) = &err {
@@ -198,7 +211,7 @@ mod test {
198211
.with(|e| e.arg("1"));
199212
assert!(matches!(
200213
cmd.output_with_timeout(Duration::from_nanos(1)),
201-
Err(TmcError::Command(CommandError::TimeOut {..}))
214+
Err(TmcError::Command(CommandError::TimeOut { .. }))
202215
));
203216
}
204217

@@ -207,7 +220,7 @@ mod test {
207220
let cmd = TmcCommand::new_with_file_io("nonexistent command").unwrap();
208221
assert!(matches!(
209222
cmd.output(),
210-
Err(TmcError::Command(CommandError::NotFound {..}))
223+
Err(TmcError::Command(CommandError::NotFound { .. }))
211224
));
212225
}
213226
}

0 commit comments

Comments
 (0)