1use std::{
2 io::{self, BufRead},
3 process::{Child, ChildStderr, ChildStdout, Command, ExitStatus, Stdio},
4 sync::mpsc,
5 thread,
6 time::{Duration, Instant}
7};
8
9use super::{CommandResult, ShellError, overlay};
10use crate::output::{Output, OutputMode};
11
12const FRAME_INTERVAL: Duration = Duration::from_millis(80);
13
14pub(super) enum Line {
15 Stdout(String),
16 StdoutCr(String),
19 Stderr(String)
20}
21
22impl Line {
23 fn text(&self) -> &str {
24 match self {
25 Line::Stdout(s) | Line::StdoutCr(s) | Line::Stderr(s) => s
26 }
27 }
28}
29
30pub(super) struct RenderedOverlay {
31 pub viewport: Vec<String>,
32 pub stderr_lines: Vec<String>,
33 pub elapsed: Duration
34}
35
36struct SpawnedCommand {
37 child: Child,
38 lines: mpsc::Receiver<Line>,
39 readers: Vec<thread::JoinHandle<()>>
40}
41
42pub fn run_command(
47 label: &str,
48 program: &str,
49 args: &[&str],
50 output: &mut dyn Output,
51 mode: OutputMode,
52 viewport_size: usize
53) -> Result<CommandResult, ShellError> {
54 if mode.is_quiet() {
55 return run_quiet(program, args);
56 }
57 if mode.is_verbose() {
58 return run_verbose(label, program, args, output, mode);
59 }
60 run_overlay(label, program, args, output, viewport_size)
61}
62
63pub fn run_passthrough(
69 program: &str,
70 args: &[&str],
71 output: &mut dyn Output,
72 mode: OutputMode
73) -> Result<CommandResult, ShellError> {
74 if mode.is_dry_run() {
75 output.dry_run_shell(&super::format_command(program, args));
76 return Ok(CommandResult { success: true, stderr: String::new() });
77 }
78
79 let status = Command::new(program)
80 .args(args)
81 .stdout(Stdio::inherit())
82 .stderr(Stdio::inherit())
83 .spawn()
84 .map_err(|e| ShellError::Spawn(program.to_string(), e))?
85 .wait()
86 .map_err(|e| ShellError::Wait(program.to_string(), e))?;
87
88 Ok(CommandResult { success: status.success(), stderr: String::new() })
89}
90
91pub(super) fn run_quiet(program: &str, args: &[&str]) -> Result<CommandResult, ShellError> {
93 let SpawnedCommand { child, lines, readers } = spawn_command_with_lines(program, args)?;
94 let stderr_lines = collect_stderr_lines(lines, |_| {});
95 let status = wait_and_join(program, child, readers)?;
96
97 Ok(CommandResult { success: status.success(), stderr: stderr_lines.join("\n") })
98}
99
100fn run_verbose(
102 label: &str,
103 program: &str,
104 args: &[&str],
105 output: &mut dyn Output,
106 mode: OutputMode
107) -> Result<CommandResult, ShellError> {
108 output.log(mode, &format!("{label}..."));
109 output.shell_command(&super::format_command(program, args));
110
111 let SpawnedCommand { child, lines, readers } = spawn_command_with_lines(program, args)?;
112 let stderr_lines = collect_stderr_lines(lines, |line| output.shell_line(line));
113 let status = wait_and_join(program, child, readers)?;
114
115 Ok(CommandResult { success: status.success(), stderr: stderr_lines.join("\n") })
116}
117
118fn run_overlay(
120 label: &str,
121 program: &str,
122 args: &[&str],
123 output: &mut dyn Output,
124 viewport_size: usize
125) -> Result<CommandResult, ShellError> {
126 let SpawnedCommand { child, lines, readers } = spawn_command_with_lines(program, args)?;
127 let rendered = render_overlay_lines(label, lines, viewport_size);
128 let status = wait_and_join(program, child, readers)?;
129 output.step_result(label, status.success(), rendered.elapsed.as_millis(), &rendered.viewport);
130 Ok(CommandResult { success: status.success(), stderr: rendered.stderr_lines.join("\n") })
131}
132
133pub(super) fn render_overlay_lines(label: &str, lines: mpsc::Receiver<Line>, viewport_size: usize) -> RenderedOverlay {
137 let mut stderr_lines: Vec<String> = Vec::new();
138 let mut viewport: Vec<String> = Vec::new();
139 let start = Instant::now();
140
141 {
142 let stdout_handle = io::stdout();
143 let mut out = stdout_handle.lock();
144 let mut frame = 0usize;
145 let mut last_rows = overlay::render_frame(&mut out, label, &[], 0, 0, viewport_size);
146
147 loop {
148 match lines.recv_timeout(FRAME_INTERVAL) {
149 Ok(line) => {
150 let text = line.text().to_string();
151 match line {
152 Line::StdoutCr(_) => {
153 if let Some(last) = viewport.last_mut() {
155 *last = text;
156 } else {
157 viewport.push(text);
158 }
159 }
160 Line::Stderr(s) => {
161 stderr_lines.push(s.clone());
162 viewport.push(s);
163 }
164 Line::Stdout(_) => {
165 viewport.push(text);
166 }
167 }
168 frame += 1;
169 last_rows = overlay::render_frame(&mut out, label, &viewport, frame, last_rows, viewport_size);
170 }
171 Err(mpsc::RecvTimeoutError::Timeout) => {
172 frame += 1;
173 last_rows = overlay::render_frame(&mut out, label, &viewport, frame, last_rows, viewport_size);
174 }
175 Err(mpsc::RecvTimeoutError::Disconnected) => break
176 }
177 }
178
179 overlay::clear_lines(&mut out, last_rows);
180 }
181
182 RenderedOverlay { viewport, stderr_lines, elapsed: start.elapsed() }
183}
184
185fn spawn_command_with_lines(program: &str, args: &[&str]) -> Result<SpawnedCommand, ShellError> {
186 let mut child = Command::new(program)
187 .args(args)
188 .stdout(Stdio::piped())
189 .stderr(Stdio::piped())
190 .spawn()
191 .map_err(|e| ShellError::Spawn(program.to_string(), e))?;
192
193 let child_stdout = child
194 .stdout
195 .take()
196 .ok_or_else(|| ShellError::Spawn(program.to_string(), io::Error::other("stdout is not piped")))?;
197 let child_stderr = child
198 .stderr
199 .take()
200 .ok_or_else(|| ShellError::Spawn(program.to_string(), io::Error::other("stderr is not piped")))?;
201
202 let (tx, rx) = mpsc::channel::<Line>();
203 let readers = spawn_line_readers(child_stdout, child_stderr, tx);
204
205 Ok(SpawnedCommand { child, lines: rx, readers })
206}
207
208fn spawn_line_readers(stdout: ChildStdout, stderr: ChildStderr, tx: mpsc::Sender<Line>) -> Vec<thread::JoinHandle<()>> {
209 let tx_stderr = tx.clone();
210
211 let stdout_reader = thread::spawn(move || {
212 use std::io::Read;
213 let mut buf = String::new();
214 let mut raw = String::new();
215 let mut reader = io::BufReader::new(stdout);
216 let mut byte = [0u8; 1];
217 while reader.read(&mut byte).unwrap_or(0) > 0 {
218 let ch = byte[0] as char;
219 if ch == '\n' {
220 let line = std::mem::take(&mut buf);
221 raw.clear();
223 let _ = tx.send(Line::Stdout(line));
224 } else if ch == '\r' {
225 let segment = std::mem::take(&mut buf);
226 raw.clear();
227 let _ = tx.send(Line::StdoutCr(segment));
228 } else {
229 buf.push(ch);
230 raw.push(ch);
231 }
232 }
233 if !buf.is_empty() {
235 let _ = tx.send(Line::Stdout(buf));
236 }
237 });
238
239 let stderr_reader = thread::spawn(move || {
240 for line in io::BufReader::new(stderr).lines().map_while(Result::ok) {
241 let _ = tx_stderr.send(Line::Stderr(line));
242 }
243 });
244
245 vec![stdout_reader, stderr_reader]
246}
247
248fn collect_stderr_lines(lines: mpsc::Receiver<Line>, mut on_line: impl FnMut(&str)) -> Vec<String> {
249 let mut stderr_lines = Vec::new();
250
251 for line in lines {
252 let text = line.text().to_string();
253 on_line(&text);
254 if let Line::Stderr(stderr) = line {
255 stderr_lines.push(stderr);
256 }
257 }
258
259 stderr_lines
260}
261
262fn wait_and_join(
263 program: &str,
264 mut child: Child,
265 readers: Vec<thread::JoinHandle<()>>
266) -> Result<ExitStatus, ShellError> {
267 let status = child.wait().map_err(|e| ShellError::Wait(program.to_string(), e))?;
268
269 for reader in readers {
270 reader.join().unwrap();
271 }
272
273 Ok(status)
274}