1use std::collections::BTreeSet;
2use std::process::{Command, Stdio};
3use std::sync::Mutex;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::{Duration, Instant};
6
7use anyhow::{Context, Result};
8use command_group::{CommandGroup, GroupChild};
9
10use crate::{Capture, Decoration, Display, PreparedTask, Shown};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Outcome {
14 Success,
15 Failed(i32),
16 Cancelled,
17}
18
19struct Children<'a>(Vec<GroupChild>, &'a Mutex<BTreeSet<u32>>);
21
22impl Children<'_> {
23 fn push(&mut self, child: GroupChild) {
24 self.1.lock().unwrap().insert(child.id());
25 self.0.push(child);
26 }
27
28 fn remove(&mut self, index: usize) -> GroupChild {
29 let child = self.0.swap_remove(index);
30 self.1.lock().unwrap().remove(&child.id());
31 child
32 }
33}
34
35impl Drop for Children<'_> {
36 fn drop(&mut self) {
37 for child in &mut self.0 {
39 let _ = child.kill();
40 }
41 for child in &mut self.0 {
42 let _ = child.wait();
43 }
44 let mut ids = self.1.lock().unwrap();
45 for child in &self.0 {
46 ids.remove(&child.id());
47 }
48 }
49}
50
51#[cfg(unix)]
53const GRACE: Duration = Duration::from_secs(5);
54
55impl Children<'_> {
56 fn terminate(&mut self) {
58 #[cfg(unix)]
59 {
60 use command_group::{Signal, UnixChildExt};
61 for child in &self.0 {
62 let _ = child.signal(Signal::SIGTERM);
63 }
64 let deadline = std::time::Instant::now() + GRACE;
65 while std::time::Instant::now() < deadline
66 && self
67 .0
68 .iter_mut()
69 .any(|child| matches!(child.try_wait(), Ok(None)))
70 {
71 std::thread::sleep(Duration::from_millis(20));
72 }
73 }
74 }
75}
76
77pub fn execute(task: &PreparedTask, cancelled: &AtomicBool) -> Result<Outcome> {
78 if task.display == Display::Stream
79 && task.ready_when.is_empty()
80 && task.decorations.iter().all(Decoration::is_empty)
81 {
82 return execute_captured(task, cancelled, None);
84 }
85 let capture =
86 Capture::new(None, task.display.clone()).ready_when(&task.ready_when, task.ready.clone());
87 execute_captured(task, cancelled, Some(&capture))
88}
89
90pub fn execute_captured(
91 task: &PreparedTask,
92 cancelled: &AtomicBool,
93 capture: Option<&Capture>,
94) -> Result<Outcome> {
95 let _terminal = (task.interactive && task.commands.len() == 1)
97 .then(terminal::Foreground::take_back_on_drop);
98 std::thread::scope(|scope| {
99 let mut readers = Vec::new();
100 let outcome = (|| {
101 let mut pending = task.commands.iter().enumerate();
102 let mut children = Children(Vec::new(), &task.processes);
103 let mut launched = Instant::now();
104 loop {
105 if cancelled.load(Ordering::SeqCst) {
106 children.terminate();
107 return Ok(Outcome::Cancelled);
108 }
109 if children.0.is_empty() || task.parallel {
110 for (index, command) in pending.by_ref() {
111 let interactive = task.interactive && task.commands.len() == 1;
112 let mut child = spawn(task, command, index, capture.is_some(), interactive)
113 .with_context(|| format!("cannot start command for {}", task.id))?;
114 if let Some(capture) = capture {
115 let stdout =
116 child.inner().stdout.take().context("missing stdout pipe")?;
117 let stderr =
118 child.inner().stderr.take().context("missing stderr pipe")?;
119 let decoration = task.decorations.get(index);
120 readers
121 .push(scope.spawn(move || capture.copy(stdout, false, decoration)));
122 readers
123 .push(scope.spawn(move || capture.copy(stderr, true, decoration)));
124 }
125 children.push(child);
126 launched = Instant::now();
127 if !task.parallel {
128 break;
129 }
130 }
131 }
132 if children.0.is_empty() {
133 return Ok(Outcome::Success);
134 }
135 let mut index = 0;
136 while index < children.0.len() {
137 match children.0[index]
138 .try_wait()
139 .context("cannot wait for command")?
140 {
141 Some(status) => {
142 let mut child = children.remove(index);
143 let _ = child.kill();
145 if !status.success() {
146 let code = if task.parallel
149 && task.commands.len() > 1
150 && task.ready_when.is_empty()
151 {
152 1
153 } else {
154 exit_code(status)
155 };
156 return Ok(Outcome::Failed(code));
157 }
158 }
159 None => index += 1,
160 }
161 }
162 if !children.0.is_empty() {
163 let interval = if launched.elapsed() < Duration::from_millis(250) {
166 1
167 } else {
168 20
169 };
170 std::thread::sleep(Duration::from_millis(interval));
171 }
172 }
173 })();
174 for reader in readers {
175 reader
176 .join()
177 .map_err(|_| anyhow::anyhow!("output capture thread panicked"))??;
178 }
179 if let (Some(capture), Ok(outcome)) = (capture, &outcome) {
180 capture.finish(if *outcome == Outcome::Success {
181 Shown::Success
182 } else {
183 Shown::Failure
184 })?;
185 }
186 outcome
187 })
188}
189
190pub fn shell(text: &str) -> Command {
193 #[cfg(windows)]
194 {
195 use std::os::windows::process::CommandExt;
196 let mut command =
197 Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
198 command
199 .args(["/D", "/S", "/C"])
200 .raw_arg(format!("\"{text}\""));
201 command
202 }
203 #[cfg(not(windows))]
204 {
205 let mut command = Command::new("/bin/sh");
206 command.arg("-c").arg(text);
207 command
208 }
209}
210
211fn spawn(
213 task: &PreparedTask,
214 text: &str,
215 index: usize,
216 capture: bool,
217 interactive: bool,
218) -> Result<GroupChild> {
219 if !task.cwd.is_dir() {
223 anyhow::bail!("working directory does not exist: {}", task.cwd.display());
224 }
225 #[cfg(windows)]
226 let mut command = shell(text);
227 #[cfg(not(windows))]
228 let mut command = match &task.sandbox {
229 Some(crate::Confinement::Seatbelt(profile)) => {
230 let mut command = Command::new("sandbox-exec");
231 command.arg("-f").arg(profile).args(["/bin/sh", "-c", text]);
232 command
233 }
234 _ => shell(text),
235 };
236 if let Some(recording) = &task.recording {
237 command = recording.command(index, text);
238 }
239 #[cfg(target_os = "linux")]
240 if let Some(crate::Confinement::Landlock(ruleset)) = task.sandbox {
241 use std::os::unix::process::CommandExt;
242 unsafe {
244 command.pre_exec(move || {
245 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
246 || libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
247 {
248 return Err(std::io::Error::last_os_error());
249 }
250 Ok(())
251 });
252 }
253 }
254 command
255 .current_dir(&task.cwd)
256 .env_clear()
257 .envs(&task.env)
258 .envs(&task.execution)
259 .stdin(if interactive {
260 Stdio::inherit()
261 } else {
262 Stdio::null()
263 })
264 .stdout(if capture {
265 Stdio::piped()
266 } else {
267 Stdio::inherit()
268 })
269 .stderr(if capture {
270 Stdio::piped()
271 } else {
272 Stdio::inherit()
273 });
274 if let Some(node) = &task.node_path
275 && let Some((key, path)) = node.path(&task.env)?
276 {
277 command.env(key, path);
278 }
279 command.env_remove("QK_TASK_SANDBOX");
282 if task.sandbox.is_some() {
283 command.env("QK_TASK_SANDBOX", "1");
284 }
285 #[cfg(unix)]
286 if interactive && terminal::in_foreground() {
287 use std::os::unix::process::CommandExt;
288 unsafe {
292 command.pre_exec(|| {
293 terminal::claim();
294 Ok(())
295 });
296 }
297 }
298 Ok(command.group_spawn()?)
299}
300
301mod terminal {
303 #[cfg_attr(not(unix), allow(dead_code))]
305 pub fn in_foreground() -> bool {
306 #[cfg(unix)]
307 unsafe {
308 libc::isatty(0) == 1 && libc::tcgetpgrp(0) == libc::getpgrp()
309 }
310 #[cfg(not(unix))]
311 false
312 }
313
314 #[cfg(unix)]
317 pub fn claim() {
318 unsafe { foreground(libc::getpgrp()) }
319 }
320
321 #[cfg(unix)]
324 unsafe fn foreground(group: libc::pid_t) {
325 unsafe {
326 let previous = libc::signal(libc::SIGTTOU, libc::SIG_IGN);
327 libc::tcsetpgrp(0, group);
328 libc::signal(libc::SIGTTOU, previous);
329 }
330 }
331
332 pub struct Foreground {
334 #[cfg(unix)]
335 group: Option<libc::pid_t>,
336 }
337
338 impl Foreground {
339 pub fn take_back_on_drop() -> Self {
340 Self {
341 #[cfg(unix)]
342 group: in_foreground().then(|| unsafe { libc::getpgrp() }),
343 }
344 }
345 }
346
347 impl Drop for Foreground {
348 fn drop(&mut self) {
349 #[cfg(unix)]
350 if let Some(group) = self.group {
351 unsafe { foreground(group) }
352 }
353 }
354 }
355}
356
357fn exit_code(status: std::process::ExitStatus) -> i32 {
358 #[cfg(unix)]
359 {
360 use std::os::unix::process::ExitStatusExt;
361 status
362 .code()
363 .unwrap_or_else(|| 128 + status.signal().unwrap_or(1))
364 }
365 #[cfg(not(unix))]
366 {
367 status.code().unwrap_or(1)
368 }
369}