1use std::collections::BTreeSet;
2use std::process::{Command, ExitStatus, 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
19enum Running {
22 Group(GroupChild),
23 #[cfg(unix)]
24 Session(crate::pty::Session),
25}
26
27impl Running {
28 fn id(&self) -> u32 {
29 match self {
30 Self::Group(child) => child.id(),
31 #[cfg(unix)]
32 Self::Session(child) => child.id(),
33 }
34 }
35
36 fn inner(&mut self) -> &mut std::process::Child {
37 match self {
38 Self::Group(child) => child.inner(),
39 #[cfg(unix)]
40 Self::Session(child) => child.inner(),
41 }
42 }
43
44 fn kill(&mut self) -> std::io::Result<()> {
45 match self {
46 Self::Group(child) => child.kill(),
47 #[cfg(unix)]
48 Self::Session(child) => child.kill(),
49 }
50 }
51
52 fn try_wait(&mut self) -> std::io::Result<Option<ExitStatus>> {
53 match self {
54 Self::Group(child) => child.try_wait(),
55 #[cfg(unix)]
56 Self::Session(child) => child.try_wait(),
57 }
58 }
59
60 fn wait(&mut self) -> std::io::Result<ExitStatus> {
61 match self {
62 Self::Group(child) => child.wait(),
63 #[cfg(unix)]
64 Self::Session(child) => child.wait(),
65 }
66 }
67
68 #[cfg(unix)]
69 fn terminate(&self) {
70 use command_group::{Signal, UnixChildExt};
71 let _ = match self {
72 Self::Group(child) => child.signal(Signal::SIGTERM),
73 Self::Session(child) => child.signal(libc::SIGTERM),
74 };
75 }
76}
77
78struct Children<'a>(Vec<Running>, &'a Mutex<BTreeSet<u32>>);
80
81impl Children<'_> {
82 fn push(&mut self, child: Running) {
83 self.1.lock().unwrap().insert(child.id());
84 self.0.push(child);
85 }
86
87 fn remove(&mut self, index: usize) -> Running {
88 let child = self.0.swap_remove(index);
89 self.1.lock().unwrap().remove(&child.id());
90 child
91 }
92}
93
94impl Drop for Children<'_> {
95 fn drop(&mut self) {
96 for child in &mut self.0 {
98 let _ = child.kill();
99 }
100 for child in &mut self.0 {
101 let _ = child.wait();
102 }
103 let mut ids = self.1.lock().unwrap();
104 for child in &self.0 {
105 ids.remove(&child.id());
106 }
107 }
108}
109
110#[cfg(unix)]
113fn grace() -> Duration {
114 grace_period(
115 std::env::var("NX_PROCESS_KILL_GRACE_PERIOD")
116 .ok()
117 .as_deref(),
118 )
119}
120
121#[cfg(unix)]
122fn grace_period(value: Option<&str>) -> Duration {
123 Duration::from_millis(
124 value
125 .and_then(|value| value.parse::<u32>().ok())
126 .map_or(5000, u64::from),
127 )
128}
129
130impl Children<'_> {
131 fn terminate(&mut self) {
133 #[cfg(unix)]
134 {
135 for child in &self.0 {
136 child.terminate();
137 }
138 let deadline = std::time::Instant::now() + grace();
139 while std::time::Instant::now() < deadline
140 && self
141 .0
142 .iter_mut()
143 .any(|child| matches!(child.try_wait(), Ok(None)))
144 {
145 std::thread::sleep(Duration::from_millis(20));
146 }
147 }
148 }
149}
150
151pub fn execute(task: &PreparedTask, cancelled: &AtomicBool) -> Result<Outcome> {
152 let log_matches = match &task.ready_check {
153 Some(Ok(crate::readiness::ReadyCheck {
154 probe: crate::readiness::Probe::LogMatches(patterns),
155 ..
156 })) => Some(patterns),
157 _ => None,
158 };
159 if task.display == Display::Stream
160 && task.ready_when.is_empty()
161 && log_matches.is_none()
162 && task.decorations.iter().all(Decoration::is_empty)
163 {
164 return execute_captured(task, cancelled, None);
166 }
167 let mut capture =
168 Capture::new(None, task.display.clone()).ready_when(&task.ready_when, task.ready.clone());
169 if let Some(patterns) = log_matches {
170 capture = capture.log_matches(patterns, task.verdict.clone());
171 }
172 execute_captured(task, cancelled, Some(&capture))
173}
174
175pub fn execute_captured(
176 task: &PreparedTask,
177 cancelled: &AtomicBool,
178 capture: Option<&Capture>,
179) -> Result<Outcome> {
180 let _terminal = (task.interactive && task.commands.len() == 1)
182 .then(terminal::Foreground::take_back_on_drop);
183 std::thread::scope(|scope| {
184 let mut readers = Vec::new();
185 let outcome = (|| {
186 #[cfg(unix)]
188 let mut forwarders = Vec::new();
189 let mut pending = task.commands.iter().enumerate();
190 let mut children = Children(Vec::new(), &task.processes);
191 let mut launched = Instant::now();
192 loop {
193 if cancelled.load(Ordering::SeqCst) {
194 children.terminate();
195 return Ok(Outcome::Cancelled);
196 }
197 if children.0.is_empty() || task.parallel {
198 for (index, command) in pending.by_ref() {
199 let interactive = task.interactive && task.commands.len() == 1;
200 let (mut child, leader) =
201 spawn(task, command, index, capture.is_some(), interactive)
202 .with_context(|| format!("cannot start command for {}", task.id))?;
203 if let Some(capture) = capture {
204 let decoration = task.decorations.get(index);
205 match leader {
206 #[cfg(unix)]
208 Some(leader) => {
209 if interactive && crate::pty::forwards_input() {
210 forwarders.push(crate::pty::Forwarder::start(
211 leader.try_clone()?,
212 task.tty,
213 ));
214 }
215 let output = crate::pty::Output(leader);
216 readers
217 .push(scope.spawn(move || {
218 capture.copy(output, false, decoration)
219 }));
220 }
221 _ => {
222 let stdout = child
223 .inner()
224 .stdout
225 .take()
226 .context("missing stdout pipe")?;
227 let stderr = child
228 .inner()
229 .stderr
230 .take()
231 .context("missing stderr pipe")?;
232 readers
233 .push(scope.spawn(move || {
234 capture.copy(stdout, false, decoration)
235 }));
236 readers.push(
237 scope.spawn(move || capture.copy(stderr, true, decoration)),
238 );
239 }
240 }
241 }
242 children.push(child);
243 launched = Instant::now();
244 if !task.parallel {
245 break;
246 }
247 }
248 }
249 if children.0.is_empty() {
250 return Ok(Outcome::Success);
251 }
252 let mut index = 0;
253 while index < children.0.len() {
254 match children.0[index]
255 .try_wait()
256 .context("cannot wait for command")?
257 {
258 Some(status) => {
259 let mut child = children.remove(index);
260 let _ = child.kill();
262 if !status.success() {
263 let code = if task.parallel
266 && task.commands.len() > 1
267 && task.ready_when.is_empty()
268 {
269 1
270 } else {
271 exit_code(status)
272 };
273 return Ok(Outcome::Failed(code));
274 }
275 }
276 None => index += 1,
277 }
278 }
279 if !children.0.is_empty() {
280 let interval = if launched.elapsed() < Duration::from_millis(250) {
283 1
284 } else {
285 20
286 };
287 std::thread::sleep(Duration::from_millis(interval));
288 }
289 }
290 })();
291 for reader in readers {
292 reader
293 .join()
294 .map_err(|_| anyhow::anyhow!("output capture thread panicked"))??;
295 }
296 if let (Some(capture), Ok(outcome)) = (capture, &outcome) {
297 capture.finish(if *outcome == Outcome::Success {
298 Shown::Success
299 } else {
300 Shown::Failure
301 })?;
302 }
303 outcome
304 })
305}
306
307pub fn shell(text: &str) -> Command {
310 #[cfg(windows)]
311 {
312 use std::os::windows::process::CommandExt;
313 let mut command =
314 Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
315 command
316 .args(["/D", "/S", "/C"])
317 .raw_arg(format!("\"{text}\""));
318 command
319 }
320 #[cfg(not(windows))]
321 {
322 let mut command = Command::new("/bin/sh");
323 command.arg("-c").arg(text);
324 command
325 }
326}
327
328fn spawn(
332 task: &PreparedTask,
333 text: &str,
334 index: usize,
335 capture: bool,
336 interactive: bool,
337) -> Result<(Running, Option<std::fs::File>)> {
338 if !task.cwd.is_dir() {
342 anyhow::bail!("working directory does not exist: {}", task.cwd.display());
343 }
344 #[cfg(windows)]
345 let mut command = shell(text);
346 #[cfg(not(windows))]
347 let mut command = match &task.sandbox {
348 Some(crate::Confinement::Seatbelt(profile)) => {
349 let mut command = Command::new("sandbox-exec");
350 command.arg("-f").arg(profile).args(["/bin/sh", "-c", text]);
351 command
352 }
353 _ => shell(text),
354 };
355 if let Some(recording) = &task.recording {
356 command = recording.command(index, text);
357 }
358 #[cfg(target_os = "linux")]
359 if let Some(crate::Confinement::Landlock(ruleset)) = task.sandbox {
360 use std::os::unix::process::CommandExt;
361 unsafe {
363 command.pre_exec(move || {
364 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
365 || libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
366 {
367 return Err(std::io::Error::last_os_error());
368 }
369 Ok(())
370 });
371 }
372 }
373 command
374 .current_dir(&task.cwd)
375 .env_clear()
376 .envs(&task.env)
377 .envs(&task.execution)
378 .stdin(if interactive {
379 Stdio::inherit()
380 } else {
381 Stdio::null()
382 })
383 .stdout(if capture {
384 Stdio::piped()
385 } else {
386 Stdio::inherit()
387 })
388 .stderr(if capture {
389 Stdio::piped()
390 } else {
391 Stdio::inherit()
392 });
393 if let Some(node) = &task.node_path
394 && let Some((key, path)) = node.path(&task.env)?
395 {
396 command.env(key, path);
397 }
398 command.env_remove("QK_TASK_SANDBOX");
401 if task.sandbox.is_some() {
402 command.env("QK_TASK_SANDBOX", "1");
403 }
404 #[cfg(unix)]
405 if capture
406 && task.pseudo_terminal
407 && task
409 .decorations
410 .get(index)
411 .is_none_or(|decoration| decoration.prefix.is_none())
412 && std::io::IsTerminal::is_terminal(&std::io::stdout())
413 {
414 let input = interactive && crate::pty::forwards_input();
415 let (session, leader) = crate::pty::spawn(&mut command, input)?;
416 return Ok((Running::Session(session), Some(leader)));
417 }
418 #[cfg(unix)]
419 if interactive && terminal::in_foreground() {
420 use std::os::unix::process::CommandExt;
421 unsafe {
425 command.pre_exec(|| {
426 terminal::claim();
427 Ok(())
428 });
429 }
430 }
431 Ok((Running::Group(command.group_spawn()?), None))
432}
433
434pub(crate) mod terminal {
436 #[cfg_attr(not(unix), allow(dead_code))]
438 pub fn in_foreground() -> bool {
439 #[cfg(unix)]
440 unsafe {
441 libc::isatty(0) == 1 && libc::tcgetpgrp(0) == libc::getpgrp()
442 }
443 #[cfg(not(unix))]
444 false
445 }
446
447 #[cfg(unix)]
450 pub fn claim() {
451 unsafe { foreground(libc::getpgrp()) }
452 }
453
454 #[cfg(unix)]
457 unsafe fn foreground(group: libc::pid_t) {
458 unsafe {
459 let previous = libc::signal(libc::SIGTTOU, libc::SIG_IGN);
460 libc::tcsetpgrp(0, group);
461 libc::signal(libc::SIGTTOU, previous);
462 }
463 }
464
465 pub struct Foreground {
467 #[cfg(unix)]
468 group: Option<libc::pid_t>,
469 }
470
471 impl Foreground {
472 pub fn take_back_on_drop() -> Self {
473 Self {
474 #[cfg(unix)]
475 group: in_foreground().then(|| unsafe { libc::getpgrp() }),
476 }
477 }
478 }
479
480 impl Drop for Foreground {
481 fn drop(&mut self) {
482 #[cfg(unix)]
483 if let Some(group) = self.group {
484 unsafe { foreground(group) }
485 }
486 }
487 }
488}
489
490fn exit_code(status: std::process::ExitStatus) -> i32 {
491 #[cfg(unix)]
492 {
493 use std::os::unix::process::ExitStatusExt;
494 status
495 .code()
496 .unwrap_or_else(|| 128 + status.signal().unwrap_or(1))
497 }
498 #[cfg(not(unix))]
499 {
500 status.code().unwrap_or(1)
501 }
502}
503
504#[cfg(all(test, unix))]
505mod grace_tests {
506 use super::*;
507
508 #[test]
509 fn the_kill_grace_period_is_read_as_nx_reads_it() {
510 assert_eq!(grace_period(None), Duration::from_millis(5000));
511 assert_eq!(grace_period(Some("250")), Duration::from_millis(250));
512 assert_eq!(grace_period(Some("soon")), Duration::from_millis(5000));
513 }
514}