1use std::{
2 collections::HashMap,
3 process::{Command, ExitStatus},
4 time::{Duration, Instant},
5};
6
7use procstream::{Capture, CommandJobExt, Event, Signal, Stream};
8use serde::Serialize;
9use shellish_parse::ParseOptions;
10use termcolor::Color;
11
12use crate::{
13 cwrite, cwriteln,
14 output::Lines,
15 script::{ScriptKillReceiver, ScriptKillSender, ScriptLocation},
16};
17
18#[derive(Copy, Clone, derive_more::Debug, PartialEq, Eq)]
19pub enum CommandResult {
20 #[debug("{_0:?}")]
21 Exit(ExitStatus, bool),
22 #[debug("timed out")]
23 TimedOut,
24}
25
26impl CommandResult {
27 pub fn success(&self) -> bool {
28 match self {
29 CommandResult::Exit(status, _) => status.success(),
30 CommandResult::TimedOut => false,
31 }
32 }
33}
34
35impl std::fmt::Display for CommandResult {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 CommandResult::Exit(status, killed) => {
39 if *killed {
40 write!(f, "killed")?;
41 #[cfg(unix)]
43 {
44 use std::os::unix::process::ExitStatusExt;
45 if status.signal().is_some() {
46 write!(f, "; {status}")?;
47 }
48 }
49 Ok(())
50 } else {
51 write!(f, "{status}")
52 }
53 }
54 CommandResult::TimedOut => write!(f, "timed out"),
55 }
56 }
57}
58
59#[derive(Clone, Debug, Serialize)]
60#[serde(transparent)]
61pub struct CommandLine {
62 pub command: String,
63 #[serde(skip)]
64 pub location: ScriptLocation,
65 #[serde(skip)]
66 pub line_count: usize,
67}
68
69impl CommandLine {
70 pub fn new(command: String, location: ScriptLocation, line_count: usize) -> Self {
71 Self {
72 command,
73 location,
74 line_count,
75 }
76 }
77
78 #[allow(clippy::too_many_arguments)]
79 pub fn run(
80 &self,
81 writer: &mut dyn termcolor::WriteColor,
82 show_line_numbers: bool,
83 runner: Option<String>,
84 timeout: Duration,
85 envs: &HashMap<String, String>,
86 kill_receiver: &ScriptKillReceiver,
87 kill_sender: &ScriptKillSender,
88 ) -> Result<(Lines, CommandResult), std::io::Error> {
89 let start = Instant::now();
90 let warn_time = timeout.saturating_mul(90) / 100;
91 let timeout = timeout.saturating_mul(110) / 100;
92
93 let mut command = if let Some(runner) = runner {
94 let bits = shellish_parse::parse(&runner, ParseOptions::default())
95 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
96 let mut cmd = Command::new(&bits[0]);
97 cmd.args(&bits[1..]);
98 cmd
99 } else {
100 let mut cmd = Command::new("sh");
101 cmd.arg("-c");
102 cmd
103 };
104 command.arg(&self.command);
105 command.envs(envs);
106 if let Some(pwd) = envs.get("PWD") {
107 command.current_dir(pwd);
108 }
109
110 let (mut child, output) = command.spawn_job(Capture::lines())?;
113
114 let job = child.job().clone();
115
116 let result = kill_receiver.run_with(
120 || _ = job.shutdown(Signal::Terminate, Duration::from_millis(250)),
121 move || {
122 let mut line_number = 1;
123 let mut output_lines = vec![];
124 let mut warned = false;
125
126 let mut push_line = |stream: Stream, line: String| {
130 if show_line_numbers {
131 cwrite!(
132 writer,
133 fg = Color::White,
134 dimmed = true,
135 "{line_number:>3} "
136 );
137 }
138
139 let line_out = fast_strip_ansi::strip_ansi_string(&line);
140 if stream == Stream::Stdout {
141 cwriteln!(writer, fg = Color::White, "{line_out}");
142 } else {
143 cwriteln!(writer, fg = Color::Yellow, "{line_out}");
144 }
145
146 output_lines.push(line);
147 line_number += 1;
148 };
149
150 loop {
151 if start.elapsed() >= timeout {
154 cwriteln!(writer, fg = Color::Yellow, "Process took too long!");
155 kill_sender.kill();
156 _ = child.shutdown(Signal::Terminate, Duration::from_millis(250));
157 return Ok((Lines::new(output_lines), CommandResult::TimedOut));
158 }
159
160 let remaining = timeout.saturating_sub(start.elapsed());
163 let wait = if warned {
164 remaining
165 } else {
166 remaining.min(warn_time.saturating_sub(start.elapsed()))
167 };
168
169 match output.recv_timeout(wait) {
170 Ok(Event::Chunk(chunk)) => {
171 let stream = chunk.stream;
172 let line = String::from_utf8(chunk.item.bytes).unwrap_or_else(|e| {
175 String::from_utf8_lossy(e.as_bytes()).into_owned()
176 });
177 push_line(stream, line);
178 }
179 Ok(Event::Exit(_)) => {}
181 Err(procstream::RecvTimeout::Closed) => break,
182 Err(procstream::RecvTimeout::Timeout) => {
183 if !warned && start.elapsed() < timeout {
184 eprintln!("Process #{} taking too long to finish.", child.id());
185 warned = true;
186 }
187 }
188 }
189 }
190
191 let status = child.wait()?;
192 Ok((Lines::new(output_lines), CommandResult::Exit(status, false)))
193 },
194 );
195
196 match result {
199 Ok((lines, CommandResult::Exit(status, _))) => {
200 Ok((lines, CommandResult::Exit(status, job.terminated())))
201 }
202 other => other,
203 }
204 }
205}