1use std::process::{Command, Stdio};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::time::Duration;
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 loop {
84 if cancelled.load(Ordering::SeqCst) {
85 children.terminate();
86 return Ok(Outcome::Cancelled);
87 }
88 if children.0.is_empty() || task.parallel {
89 for (index, command) in pending.by_ref() {
90 let interactive = task.interactive && task.commands.len() == 1;
91 let mut child = spawn(task, command, index, capture.is_some(), interactive)
92 .with_context(|| format!("cannot start command for {}", task.id))?;
93 if let Some(capture) = capture {
94 let stdout =
95 child.inner().stdout.take().context("missing stdout pipe")?;
96 let stderr =
97 child.inner().stderr.take().context("missing stderr pipe")?;
98 let decoration = task.decorations.get(index);
99 readers
100 .push(scope.spawn(move || capture.copy(stdout, false, decoration)));
101 readers
102 .push(scope.spawn(move || capture.copy(stderr, true, decoration)));
103 }
104 children.0.push(child);
105 if !task.parallel {
106 break;
107 }
108 }
109 }
110 if children.0.is_empty() {
111 return Ok(Outcome::Success);
112 }
113 let mut index = 0;
114 while index < children.0.len() {
115 match children.0[index]
116 .try_wait()
117 .context("cannot wait for command")?
118 {
119 Some(status) => {
120 let mut child = children.0.swap_remove(index);
121 let _ = child.kill();
123 if !status.success() {
124 let code = if task.parallel
127 && task.commands.len() > 1
128 && task.ready_when.is_empty()
129 {
130 1
131 } else {
132 exit_code(status)
133 };
134 return Ok(Outcome::Failed(code));
135 }
136 }
137 None => index += 1,
138 }
139 }
140 std::thread::sleep(Duration::from_millis(20));
141 }
142 })();
143 for reader in readers {
144 reader
145 .join()
146 .map_err(|_| anyhow::anyhow!("output capture thread panicked"))??;
147 }
148 if let (Some(capture), Ok(outcome)) = (capture, &outcome) {
149 capture.finish(if *outcome == Outcome::Success {
150 Shown::Success
151 } else {
152 Shown::Failure
153 })?;
154 }
155 outcome
156 })
157}
158
159pub fn shell(text: &str) -> Command {
162 #[cfg(windows)]
163 {
164 use std::os::windows::process::CommandExt;
165 let mut command =
166 Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
167 command
168 .args(["/D", "/S", "/C"])
169 .raw_arg(format!("\"{text}\""));
170 command
171 }
172 #[cfg(not(windows))]
173 {
174 let mut command = Command::new("/bin/sh");
175 command.arg("-c").arg(text);
176 command
177 }
178}
179
180fn spawn(
182 task: &PreparedTask,
183 text: &str,
184 index: usize,
185 capture: bool,
186 interactive: bool,
187) -> Result<GroupChild> {
188 #[cfg(windows)]
189 let mut command = shell(text);
190 #[cfg(not(windows))]
191 let mut command = match &task.sandbox {
192 Some(crate::Confinement::Seatbelt(profile)) => {
193 let mut command = Command::new("sandbox-exec");
194 command.arg("-f").arg(profile).args(["/bin/sh", "-c", text]);
195 command
196 }
197 _ => shell(text),
198 };
199 if let Some(recording) = &task.recording {
200 command = recording.command(index, text);
201 }
202 #[cfg(target_os = "linux")]
203 if let Some(crate::Confinement::Landlock(ruleset)) = task.sandbox {
204 use std::os::unix::process::CommandExt;
205 unsafe {
207 command.pre_exec(move || {
208 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
209 || libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
210 {
211 return Err(std::io::Error::last_os_error());
212 }
213 Ok(())
214 });
215 }
216 }
217 command
218 .current_dir(&task.cwd)
219 .env_clear()
220 .envs(&task.env)
221 .envs(&task.execution)
222 .stdin(if interactive {
223 Stdio::inherit()
224 } else {
225 Stdio::null()
226 })
227 .stdout(if capture {
228 Stdio::piped()
229 } else {
230 Stdio::inherit()
231 })
232 .stderr(if capture {
233 Stdio::piped()
234 } else {
235 Stdio::inherit()
236 });
237 command.env_remove("QK_TASK_SANDBOX");
240 if task.sandbox.is_some() {
241 command.env("QK_TASK_SANDBOX", "1");
242 }
243 #[cfg(unix)]
244 if interactive && terminal::in_foreground() {
245 use std::os::unix::process::CommandExt;
246 unsafe {
250 command.pre_exec(|| {
251 terminal::claim();
252 Ok(())
253 });
254 }
255 }
256 Ok(command.group_spawn()?)
257}
258
259mod terminal {
261 #[cfg_attr(not(unix), allow(dead_code))]
263 pub fn in_foreground() -> bool {
264 #[cfg(unix)]
265 unsafe {
266 libc::isatty(0) == 1 && libc::tcgetpgrp(0) == libc::getpgrp()
267 }
268 #[cfg(not(unix))]
269 false
270 }
271
272 #[cfg(unix)]
275 pub fn claim() {
276 unsafe { foreground(libc::getpgrp()) }
277 }
278
279 #[cfg(unix)]
282 unsafe fn foreground(group: libc::pid_t) {
283 unsafe {
284 let previous = libc::signal(libc::SIGTTOU, libc::SIG_IGN);
285 libc::tcsetpgrp(0, group);
286 libc::signal(libc::SIGTTOU, previous);
287 }
288 }
289
290 pub struct Foreground {
292 #[cfg(unix)]
293 group: Option<libc::pid_t>,
294 }
295
296 impl Foreground {
297 pub fn take_back_on_drop() -> Self {
298 Self {
299 #[cfg(unix)]
300 group: in_foreground().then(|| unsafe { libc::getpgrp() }),
301 }
302 }
303 }
304
305 impl Drop for Foreground {
306 fn drop(&mut self) {
307 #[cfg(unix)]
308 if let Some(group) = self.group {
309 unsafe { foreground(group) }
310 }
311 }
312 }
313}
314
315fn exit_code(status: std::process::ExitStatus) -> i32 {
316 #[cfg(unix)]
317 {
318 use std::os::unix::process::ExitStatusExt;
319 status
320 .code()
321 .unwrap_or_else(|| 128 + status.signal().unwrap_or(1))
322 }
323 #[cfg(not(unix))]
324 {
325 status.code().unwrap_or(1)
326 }
327}