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