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 pub fn run(
58 &self,
59 writer: &mut dyn termcolor::WriteColor,
60 show_line_numbers: bool,
61 runner: Option<String>,
62 timeout: Duration,
63 envs: &HashMap<String, String>,
64 kill_receiver: &ScriptKillReceiver,
65 kill_sender: &ScriptKillSender,
66 ) -> Result<(Lines, CommandResult), std::io::Error> {
67 let start = Instant::now();
68 let warn_time = timeout.saturating_mul(90) / 100;
69 let timeout = timeout.saturating_mul(110) / 100;
70
71 thread::scope(|s| {
73 let mut command = if let Some(runner) = runner {
74 let bits = shellish_parse::parse(&runner, ParseOptions::default())
75 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
76 let mut cmd = Command::new(&bits[0]);
77 cmd.args(&bits[1..]);
78 cmd
79 } else {
80 let mut cmd = Command::new("sh");
81 cmd.arg("-c");
82 cmd
83 };
84 command.arg(&self.command);
85 command.envs(envs);
86 if let Some(pwd) = envs.get("PWD") {
87 command.current_dir(pwd);
88 }
89 #[cfg(unix)]
90 {
91 use std::os::unix::process::CommandExt;
92 command.process_group(0);
93 }
94 #[cfg(windows)]
95 {
96 use std::os::windows::process::CommandExt;
97 const CREATE_SUSPENDED: u32 = 0x00000004;
98 command.creation_flags(CREATE_SUSPENDED);
99 }
100 command.stdout(Stdio::piped());
101 command.stderr(Stdio::piped());
102 command.stdin(Stdio::null());
103 let mut output = command.spawn().map_err(|e| {
104 std::io::Error::new(
105 e.kind(),
106 format!("failed to spawn command {command:?}: {e}"),
107 )
108 })?;
109 let (tx, rx) = std::sync::mpsc::channel();
110
111 let stdout_lines = tx.clone();
113 let stdout = output.stdout.take().unwrap();
114 let stdout = s.spawn(move || {
115 let mut reader = BufReader::new(stdout);
116 let mut line = String::new();
117 while reader.read_line(&mut line).unwrap() > 0 {
118 if line.is_empty() {
119 continue;
120 }
121 if line.ends_with('\n') {
122 line.pop();
123 }
124 _ = stdout_lines.send((true, std::mem::take(&mut line)));
125 }
126 });
127
128 let stderr_lines = tx;
129 let stderr = output.stderr.take().unwrap();
130 let stderr = s.spawn(move || {
131 let mut reader = BufReader::new(stderr);
132 let mut line = String::new();
133 while reader.read_line(&mut line).unwrap() > 0 {
134 if line.is_empty() {
135 continue;
136 }
137 if line.ends_with('\n') {
138 line.pop();
139 }
140 _ = stderr_lines.send((false, std::mem::take(&mut line)));
141 }
142 });
143
144 let runner = s.spawn(move || kill_receiver.run_cmd(output, warn_time));
145
146 let mut line_number = 1;
147 let mut output_lines = vec![];
148
149 while let Ok((is_stdout, line)) = rx.recv_timeout(timeout) {
150 if show_line_numbers {
151 cwrite!(
152 writer,
153 fg = Color::White,
154 dimmed = true,
155 "{line_number:>3} "
156 );
157 }
158
159 let line_out = fast_strip_ansi::strip_ansi_string(&line);
161 if is_stdout {
162 cwriteln!(writer, fg = Color::White, "{line_out}");
163 } else {
164 cwriteln!(writer, fg = Color::Yellow, "{line_out}");
165 }
166
167 output_lines.push(line);
168 line_number += 1;
169 }
170
171 let mut handles = vec![stdout, stderr];
172 while !handles.is_empty() {
173 if start.elapsed() > timeout {
174 cwriteln!(writer, fg = Color::Yellow, "Process took too long!");
175 kill_sender.kill();
176
177 return Ok((Lines::new(output_lines), CommandResult::TimedOut));
178 }
179
180 let mut new_handles = vec![];
181 for handle in handles.drain(..) {
182 if handle.is_finished() {
183 handle
184 .join()
185 .map_err(|_| std::io::Error::other("thread panicked"))?;
186 } else {
187 new_handles.push(handle);
188 }
189 }
190 handles = new_handles;
191 std::thread::sleep(std::time::Duration::from_millis(10));
192 }
193
194 Ok((
195 Lines::new(output_lines),
196 CommandResult::Exit(runner.join().unwrap()?),
197 ))
198 })
199 }
200}