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 if line.ends_with('\r') {
126 line.pop();
127 }
128 _ = stdout_lines.send((true, std::mem::take(&mut line)));
129 }
130 });
131
132 let stderr_lines = tx;
133 let stderr = output.stderr.take().unwrap();
134 let stderr = s.spawn(move || {
135 let mut reader = BufReader::new(stderr);
136 let mut line = String::new();
137 while reader.read_line(&mut line).unwrap() > 0 {
138 if line.is_empty() {
139 continue;
140 }
141 if line.ends_with('\n') {
142 line.pop();
143 }
144 if line.ends_with('\r') {
145 line.pop();
146 }
147 _ = stderr_lines.send((false, std::mem::take(&mut line)));
148 }
149 });
150
151 let runner = s.spawn(move || kill_receiver.run_cmd(output, warn_time));
152
153 let mut line_number = 1;
154 let mut output_lines = vec![];
155
156 while let Ok((is_stdout, line)) = rx.recv_timeout(timeout) {
157 if show_line_numbers {
158 cwrite!(
159 writer,
160 fg = Color::White,
161 dimmed = true,
162 "{line_number:>3} "
163 );
164 }
165
166 let line_out = fast_strip_ansi::strip_ansi_string(&line);
168 if is_stdout {
169 cwriteln!(writer, fg = Color::White, "{line_out}");
170 } else {
171 cwriteln!(writer, fg = Color::Yellow, "{line_out}");
172 }
173
174 output_lines.push(line);
175 line_number += 1;
176 }
177
178 let mut handles = vec![stdout, stderr];
179 while !handles.is_empty() {
180 if start.elapsed() > timeout {
181 cwriteln!(writer, fg = Color::Yellow, "Process took too long!");
182 kill_sender.kill();
183
184 return Ok((Lines::new(output_lines), CommandResult::TimedOut));
185 }
186
187 let mut new_handles = vec![];
188 for handle in handles.drain(..) {
189 if handle.is_finished() {
190 handle
191 .join()
192 .map_err(|_| std::io::Error::other("thread panicked"))?;
193 } else {
194 new_handles.push(handle);
195 }
196 }
197 handles = new_handles;
198 std::thread::sleep(std::time::Duration::from_millis(10));
199 }
200
201 Ok((
202 Lines::new(output_lines),
203 CommandResult::Exit(runner.join().unwrap()?),
204 ))
205 })
206 }
207}