Skip to content

Commit fcd0fb5

Browse files
committed
Add live command execution with real-time output
1 parent 8f657fb commit fcd0fb5

File tree

4 files changed

+163
-13
lines changed

4 files changed

+163
-13
lines changed

src/app.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ pub enum App {
4848

4949
pub struct Image(pub String);
5050

51+
// enum IoLayout {
52+
// Workdir(PathBuf),
53+
// InputOutput { input: PathBuf, output: PathBuf },
54+
// }
55+
// struct ContainerMounts {
56+
// io: IoLayout,
57+
// scratch: Option<PathBuf>,
58+
// }
59+
5160
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5261
pub enum MountRole {
5362
WorkingDir,

src/executor/docker.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl Executor {
5050

5151
println!("Running {command}");
5252

53-
let result = command.call();
53+
let result = command.live().call();
5454

5555
// let command_line = format!(
5656
// "docker run {options} {} {}",
@@ -60,8 +60,8 @@ impl Executor {
6060
// println!("Running {command_line}");
6161
// let result = util::Command::shell(&command_line).try_call();
6262

63-
println!("{}", result.stdout.bright_black());
64-
eprintln!("{}", result.stderr.bright_red());
63+
//println!("{}", result.stdout.bright_black());
64+
//eprintln!("{}", result.stderr.bright_red());
6565

6666
let logs = format!(
6767
"{command}\nprocess success: {}\n{}\n{}\n{}\n",

src/executor/hpc_container.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ impl Executor {
5555

5656
println!("Running {command}");
5757

58-
let result = command.call();
58+
let result = command.live().call();
5959

60-
println!("{}", result.stdout.bright_black());
61-
eprintln!("{}", result.stderr.bright_red());
60+
// println!("{}", result.stdout.bright_black());
61+
// eprintln!("{}", result.stderr.bright_red());
6262

6363
let logs = format!(
6464
"{command}\nprocess success: {}\n{}\n{}\n{}\n",

src/util/command.rs

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::{
22
fmt::{self},
3+
io::{Read, Write},
34
path::PathBuf,
5+
process::Stdio,
6+
thread,
47
};
58

69
use yansi::{Condition, Paint};
@@ -116,7 +119,44 @@ impl Command {
116119
self.execution_mode = ExecutionMode::PrintOutput { live: true };
117120
self
118121
}
122+
119123
// maybe Result<CommandResults, std::io::Error> instead? ie Error::new(ErrorKind::Other, "something went wrong");
124+
// pub fn try_call(&self) -> CommandResults {
125+
// println!("{self:#}");
126+
127+
// let mut cmd = std::process::Command::new(self.command.clone());
128+
// cmd.args(self.args.clone());
129+
// if let Some(dir) = &self.cd {
130+
// cmd.current_dir(dir);
131+
// }
132+
133+
// if let ExecutionMode::PrintOutput { live: true } = self.execution_mode {
134+
// let s = cmd
135+
// .spawn()
136+
// .unwrap_or_else(|_| panic!("command: {} failed to start", self.command.red()))
137+
// .wait()
138+
// .expect("failed to wait on child");
139+
// CommandResults {
140+
// stdout: "".into(),
141+
// stderr: "".into(),
142+
// success: s.success(),
143+
// }
144+
// } else {
145+
// let o = cmd.output().expect("failed to execute process");
146+
// let r = CommandResults {
147+
// stdout: String::from_utf8_lossy(&o.stdout).into(),
148+
// stderr: String::from_utf8_lossy(&o.stderr).into(),
149+
// success: o.status.success(),
150+
// };
151+
// if let ExecutionMode::PrintOutput { live: false } = self.execution_mode {
152+
// println!("{}", r.stdout);
153+
// eprintln!("{}", r.stderr);
154+
// }
155+
// r
156+
// }
157+
// }
158+
159+
/// Execute the command and capture both stdout and stderr while simultaneously printing them live if live is true
120160
pub fn try_call(&self) -> CommandResults {
121161
println!("{self:#}");
122162

@@ -127,15 +167,81 @@ impl Command {
127167
}
128168

129169
if let ExecutionMode::PrintOutput { live: true } = self.execution_mode {
130-
let s = cmd
170+
cmd.stdout(Stdio::piped());
171+
cmd.stderr(Stdio::piped());
172+
173+
let mut child = cmd
131174
.spawn()
132-
.unwrap_or_else(|_| panic!("command: {} failed to start", self.command.red()))
133-
.wait()
134-
.expect("failed to wait on child");
175+
.unwrap_or_else(|_| panic!("command: {} failed to start", self.command.red()));
176+
177+
let stdout = child.stdout.take().expect("Failed to capture stdout");
178+
let stderr = child.stderr.take().expect("Failed to capture stderr");
179+
180+
// Create channels to collect output
181+
let (stdout_tx, stdout_rx) = std::sync::mpsc::channel();
182+
let (stderr_tx, stderr_rx) = std::sync::mpsc::channel();
183+
184+
fn ssd() {}
185+
// Spawn thread to read and print stdout
186+
let stdout_thread = thread::spawn(move || {
187+
let mut reader = stdout;
188+
let mut output = Vec::new();
189+
let mut buf = [0u8; 8192];
190+
191+
let mut out = std::io::stdout().lock();
192+
193+
loop {
194+
let n = match reader.read(&mut buf) {
195+
Ok(0) => break,
196+
Ok(n) => n,
197+
Err(_) => break,
198+
};
199+
200+
output.extend_from_slice(&buf[..n]);
201+
let _ = out.write_all(&buf[..n]); // <-- bytes, not chars
202+
let _ = out.flush();
203+
}
204+
205+
let _ = stdout_tx.send(output);
206+
});
207+
208+
// Spawn thread to read and print stderr
209+
let stderr_thread = thread::spawn(move || {
210+
let mut reader = stderr;
211+
let mut output = Vec::new();
212+
let mut buf = [0u8; 8192];
213+
214+
let mut err = std::io::stderr().lock();
215+
216+
loop {
217+
let n = match reader.read(&mut buf) {
218+
Ok(0) => break,
219+
Ok(n) => n,
220+
Err(_) => break,
221+
};
222+
223+
output.extend_from_slice(&buf[..n]);
224+
let _ = err.write_all(&buf[..n]);
225+
let _ = err.flush();
226+
}
227+
228+
let _ = stderr_tx.send(output);
229+
});
230+
231+
// Wait for the child process to complete
232+
let status = child.wait().expect("Failed to wait on child process");
233+
234+
// Wait for threads to finish and collect output
235+
stdout_thread.join().expect("Failed to join stdout thread");
236+
stderr_thread.join().expect("Failed to join stderr thread");
237+
238+
let stdout_bytes = stdout_rx.recv().unwrap_or_default();
239+
let stderr_bytes = stderr_rx.recv().unwrap_or_default();
240+
135241
CommandResults {
136-
stdout: "".into(),
137-
stderr: "".into(),
138-
success: s.success(),
242+
stdout: String::from_utf8_lossy(&stdout_bytes).into(),
243+
stderr: String::from_utf8_lossy(&stderr_bytes).into(),
244+
success: status.success(),
139245
}
140246
} else {
141247
let o = cmd.output().expect("failed to execute process");
@@ -275,4 +381,39 @@ mod tests {
275381
assert!(alt_output.contains("echo"));
276382
assert!(alt_output.contains("hello world"));
277383
}
384+
385+
#[test]
386+
fn test_call_live() {
387+
let result = Command::shell("echo stdout_test && echo stderr_test >&2")
388+
.live()
389+
.try_call();
390+
391+
assert!(result.success);
392+
assert_eq!(result.stdout.trim(), "stdout_test");
393+
assert_eq!(result.stderr.trim(), "stderr_test");
394+
}
395+
396+
#[test]
397+
fn test_call_live_multiline() {
398+
let result = Command::shell("echo line1 && echo line2 && echo line3")
399+
.live()
400+
.try_call();
401+
assert!(result.success);
402+
assert!(result.stdout.contains("line1"));
403+
assert!(result.stdout.contains("line2"));
404+
assert!(result.stdout.contains("line3"));
405+
}
406+
407+
#[test]
408+
fn test_call_live_utf8() {
409+
// Test with various UTF-8 multi-byte characters
410+
let result = Command::shell("echo こんにちは世界 🌸 おちゃ café")
411+
.live()
412+
.try_call();
413+
414+
assert!(result.success);
415+
assert!(result.stdout.contains("こんにちは世界"));
416+
assert!(result.stdout.contains("🌸"));
417+
assert!(result.stdout.contains("café"));
418+
}
278419
}

0 commit comments

Comments
 (0)