1use std::{
2 collections::HashMap,
3 io::{BufRead, BufReader},
4 process::{Command, ExitStatus, Stdio},
5 thread,
6 time::{Duration, Instant},
7};
8
9use serde::Serialize;
10use shellish_parse::ParseOptions;
11use termcolor::Color;
12
13use crate::{
14 cwrite, cwriteln,
15 output::Lines,
16 script::{ScriptKillReceiver, ScriptKillSender, ScriptLocation},
17};
18
19#[derive(Copy, Clone, derive_more::Debug, derive_more::Display, PartialEq, Eq)]
20pub enum CommandResult {
21 #[debug("{_0:?}")]
22 #[display("{_0}")]
23 Exit(ExitStatus),
24 #[debug("timed out")]
25 #[display("timed out")]
26 TimedOut,
27}
28
29impl CommandResult {
30 pub fn success(&self) -> bool {
31 match self {
32 CommandResult::Exit(status) => status.success(),
33 CommandResult::TimedOut => false,
34 }
35 }
36}
37
38#[derive(Clone, Debug, Serialize)]
39#[serde(transparent)]
40pub struct CommandLine {
41 pub command: String,
42 #[serde(skip)]
43 pub location: ScriptLocation,
44 #[serde(skip)]
45 pub line_count: usize,
46}
47
48impl CommandLine {
49 pub fn new(command: String, location: ScriptLocation, line_count: usize) -> Self {
50 Self {
51 command,
52 location,
53 line_count,
54 }
55 }
56
57 #[allow(clippy::too_many_arguments)]
58 pub fn run(
59 &self,
60 writer: &mut dyn termcolor::WriteColor,
61 show_line_numbers: bool,
62 runner: Option<String>,
63 timeout: Duration,
64 envs: &HashMap<String, String>,
65 kill_receiver: &ScriptKillReceiver,
66 kill_sender: &ScriptKillSender,
67 ) -> Result<(Lines, CommandResult), std::io::Error> {
68 let start = Instant::now();
69 let warn_time = timeout.saturating_mul(90) / 100;
70 let timeout = timeout.saturating_mul(110) / 100;
71
72 thread::scope(|s| {
74 let mut command = if let Some(runner) = runner {
75 let bits = shellish_parse::parse(&runner, ParseOptions::default())
76 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
77 let mut cmd = Command::new(&bits[0]);
78 cmd.args(&bits[1..]);
79 cmd
80 } else {
81 let mut cmd = Command::new("sh");
82 cmd.arg("-c");
83 cmd
84 };
85 command.arg(&self.command);
86 command.envs(envs);
87 if let Some(pwd) = envs.get("PWD") {
88 command.current_dir(pwd);
89 }
90 #[cfg(unix)]
91 {
92 use std::os::unix::process::CommandExt;
93 command.process_group(0);
94 }
95 #[cfg(windows)]
96 {
97 use std::os::windows::process::CommandExt;
98 const CREATE_SUSPENDED: u32 = 0x00000004;
99 command.creation_flags(CREATE_SUSPENDED);
100 }
101 command.stdout(Stdio::piped());
102 command.stderr(Stdio::piped());
103 command.stdin(Stdio::null());
104 let mut output = command.spawn().map_err(|e| {
105 std::io::Error::new(
106 e.kind(),
107 format!("failed to spawn command {command:?}: {e}"),
108 )
109 })?;
110 let (tx, rx) = std::sync::mpsc::channel();
111
112 let stdout_lines = tx.clone();
114 let stdout = output.stdout.take().unwrap();
115 let stdout = s.spawn(move || {
116 let mut reader = BufReader::new(stdout);
117 let mut line = String::new();
118 while reader.read_line(&mut line).unwrap() > 0 {
119 if line.is_empty() {
120 continue;
121 }
122 if line.ends_with('\n') {
123 line.pop();
124 }
125 _ = stdout_lines.send((true, std::mem::take(&mut line)));
126 }
127 });
128
129 let stderr_lines = tx;
130 let stderr = output.stderr.take().unwrap();
131 let stderr = s.spawn(move || {
132 let mut reader = BufReader::new(stderr);
133 let mut line = String::new();
134 while reader.read_line(&mut line).unwrap() > 0 {
135 if line.is_empty() {
136 continue;
137 }
138 if line.ends_with('\n') {
139 line.pop();
140 }
141 _ = stderr_lines.send((false, std::mem::take(&mut line)));
142 }
143 });
144
145 let runner = s.spawn(move || kill_receiver.run_cmd(output, warn_time));
146
147 let mut line_number = 1;
148 let mut output_lines = vec![];
149
150 while let Ok((is_stdout, line)) = rx.recv_timeout(timeout) {
151 if show_line_numbers {
152 cwrite!(
153 writer,
154 fg = Color::White,
155 dimmed = true,
156 "{line_number:>3} "
157 );
158 }
159
160 let line_out = fast_strip_ansi::strip_ansi_string(&line);
162 if is_stdout {
163 cwriteln!(writer, fg = Color::White, "{line_out}");
164 } else {
165 cwriteln!(writer, fg = Color::Yellow, "{line_out}");
166 }
167
168 output_lines.push(line);
169 line_number += 1;
170 }
171
172 let mut handles = vec![stdout, stderr];
173 while !handles.is_empty() {
174 if start.elapsed() > timeout {
175 cwriteln!(writer, fg = Color::Yellow, "Process took too long!");
176 kill_sender.kill();
177
178 return Ok((Lines::new(output_lines), CommandResult::TimedOut));
179 }
180
181 let mut new_handles = vec![];
182 for handle in handles.drain(..) {
183 if handle.is_finished() {
184 handle
185 .join()
186 .map_err(|_| std::io::Error::other("thread panicked"))?;
187 } else {
188 new_handles.push(handle);
189 }
190 }
191 handles = new_handles;
192 std::thread::sleep(std::time::Duration::from_millis(10));
193 }
194
195 Ok((
196 Lines::new(output_lines),
197 CommandResult::Exit(runner.join().unwrap()?),
198 ))
199 })
200 }
201}