1use std::ffi::{OsStr, OsString};
4use std::fs::File;
5use std::path::PathBuf;
6use std::process::{ExitStatus, Stdio};
7
8use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
9
10use crate::error::{ChildSignal, NodeSupervisorError};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum ChildStdio {
14 Null,
15 Piped,
16 Inherit,
17}
18
19impl ChildStdio {
20 fn into_stdio(self) -> Stdio {
21 match self {
22 Self::Null => Stdio::null(),
23 Self::Piped => Stdio::piped(),
24 Self::Inherit => Stdio::inherit(),
25 }
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct ChildProcessSpec {
33 program: OsString,
34 args: Vec<OsString>,
35 envs: Vec<(OsString, OsString)>,
36 current_dir: Option<PathBuf>,
37 stdin: ChildStdio,
38 stdout: ChildStdio,
39 stderr: ChildStdio,
40 kill_on_drop: bool,
41 #[cfg(unix)]
42 process_group: Option<i32>,
43}
44
45impl ChildProcessSpec {
46 pub fn new(program: impl Into<OsString>) -> Self {
48 Self {
49 program: program.into(),
50 args: Vec::new(),
51 envs: Vec::new(),
52 current_dir: None,
53 stdin: ChildStdio::Null,
54 stdout: ChildStdio::Inherit,
55 stderr: ChildStdio::Inherit,
56 kill_on_drop: true,
57 #[cfg(unix)]
58 process_group: None,
59 }
60 }
61
62 #[must_use]
64 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
65 self.args.push(arg.into());
66 self
67 }
68
69 #[must_use]
71 pub fn args<I, S>(mut self, args: I) -> Self
72 where
73 I: IntoIterator<Item = S>,
74 S: Into<OsString>,
75 {
76 self.args.extend(args.into_iter().map(Into::into));
77 self
78 }
79
80 #[must_use]
82 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
83 self.envs.push((key.into(), value.into()));
84 self
85 }
86
87 #[must_use]
89 pub fn current_dir(mut self, current_dir: impl Into<PathBuf>) -> Self {
90 self.current_dir = Some(current_dir.into());
91 self
92 }
93
94 #[must_use]
96 pub fn stdin_piped(mut self) -> Self {
97 self.stdin = ChildStdio::Piped;
98 self
99 }
100
101 #[must_use]
103 pub fn stdout_piped(mut self) -> Self {
104 self.stdout = ChildStdio::Piped;
105 self
106 }
107
108 #[must_use]
110 pub fn stderr_piped(mut self) -> Self {
111 self.stderr = ChildStdio::Piped;
112 self
113 }
114
115 #[must_use]
117 pub fn kill_on_drop(mut self, kill_on_drop: bool) -> Self {
118 self.kill_on_drop = kill_on_drop;
119 self
120 }
121
122 #[cfg(unix)]
124 #[must_use]
125 pub fn process_group(mut self, process_group: i32) -> Self {
126 self.process_group = Some(process_group);
127 self
128 }
129
130 pub fn program(&self) -> &OsStr {
132 &self.program
133 }
134
135 pub fn program_args(&self) -> &[OsString] {
137 &self.args
138 }
139
140 #[cfg(test)]
142 pub(crate) fn program_envs(&self) -> &[(OsString, OsString)] {
143 &self.envs
144 }
145
146 fn program_label(&self) -> String {
147 self.program.to_string_lossy().into_owned()
148 }
149
150 fn command(&self) -> Command {
151 let mut command = Command::new(&self.program);
152 command
153 .args(&self.args)
154 .stdin(self.stdin.into_stdio())
155 .stdout(self.stdout.into_stdio())
156 .stderr(self.stderr.into_stdio())
157 .kill_on_drop(self.kill_on_drop);
158
159 #[cfg(unix)]
160 if let Some(process_group) = self.process_group {
161 command.process_group(process_group);
162 }
163
164 for (key, value) in &self.envs {
165 command.env(key, value);
166 }
167
168 if let Some(current_dir) = &self.current_dir {
169 command.current_dir(current_dir);
170 }
171
172 command
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ChildSupervisor {
179 spec: ChildProcessSpec,
180}
181
182impl ChildSupervisor {
183 pub fn new(spec: ChildProcessSpec) -> Self {
185 Self { spec }
186 }
187
188 #[tracing::instrument(skip_all, fields(program = %self.spec.program_label()))]
190 pub fn spawn(&self) -> Result<SupervisedChild, NodeSupervisorError> {
191 self.spawn_with_inherited_file(None)
192 }
193
194 pub(crate) fn spawn_with_inherited_file(
195 &self,
196 inherited_file: Option<File>,
197 ) -> Result<SupervisedChild, NodeSupervisorError> {
198 self.spawn_with_files(inherited_file, None)
199 }
200
201 pub(crate) fn spawn_with_files(
202 &self,
203 inherited_file: Option<File>,
204 chain_lock: Option<File>,
205 ) -> Result<SupervisedChild, NodeSupervisorError> {
206 let program = self.spec.program_label();
207 let mut command = self.spec.command();
208 if let Some(file) = inherited_file {
209 inherit_file(&mut command, file).map_err(|source| NodeSupervisorError::Spawn {
210 program: program.clone(),
211 source,
212 })?;
213 }
214 if let Some(file) = chain_lock {
215 let fd =
216 inherit_file(&mut command, file).map_err(|source| NodeSupervisorError::Spawn {
217 program: program.clone(),
218 source,
219 })?;
220 command.env("HOG_DEVNODE_DATA_DIR_LOCK_FD", fd.to_string());
221 }
222 let child = command
223 .spawn()
224 .map_err(|source| NodeSupervisorError::Spawn {
225 program: program.clone(),
226 source,
227 })?;
228 #[cfg(unix)]
229 let process_group = resolve_process_group(self.spec.process_group, child.id(), &program)?;
230
231 Ok(SupervisedChild {
232 program,
233 child,
234 #[cfg(unix)]
235 process_group,
236 })
237 }
238}
239
240#[cfg(unix)]
241fn inherit_file(command: &mut Command, file: File) -> std::io::Result<i32> {
242 use std::os::fd::{AsRawFd, FromRawFd};
243
244 let fd = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 3) };
248 if fd == -1 {
249 return Err(std::io::Error::last_os_error());
250 }
251 let inherited = unsafe { File::from_raw_fd(fd) };
253 drop(file);
254
255 unsafe {
260 command.pre_exec(move || {
261 let fd = inherited.as_raw_fd();
262 let flags = libc::fcntl(fd, libc::F_GETFD);
263 if flags == -1 || libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) == -1 {
264 return Err(std::io::Error::last_os_error());
265 }
266 Ok(())
267 });
268 }
269 Ok(fd)
270}
271
272#[cfg(not(unix))]
273fn inherit_file(_command: &mut Command, _file: File) -> std::io::Result<i32> {
274 Err(std::io::Error::new(
275 std::io::ErrorKind::Unsupported,
276 "inherited installation leases require Unix file descriptors",
277 ))
278}
279
280#[cfg(unix)]
281fn resolve_process_group(
282 configured_process_group: Option<i32>,
283 child_id: Option<u32>,
284 program: &str,
285) -> Result<Option<i32>, NodeSupervisorError> {
286 match configured_process_group {
287 Some(0) => child_id_to_i32(child_id, program).map(Some),
288 Some(process_group) => Ok(Some(process_group)),
289 None => Ok(None),
290 }
291}
292
293#[cfg(unix)]
294fn child_id_to_i32(child_id: Option<u32>, program: &str) -> Result<i32, NodeSupervisorError> {
295 let Some(child_id) = child_id else {
296 return Err(NodeSupervisorError::Status {
297 program: program.to_string(),
298 source: std::io::Error::new(
299 std::io::ErrorKind::InvalidInput,
300 "child process id is unavailable",
301 ),
302 });
303 };
304
305 i32::try_from(child_id).map_err(|_| NodeSupervisorError::Status {
306 program: program.to_string(),
307 source: std::io::Error::new(
308 std::io::ErrorKind::InvalidInput,
309 "child pid does not fit in pid_t",
310 ),
311 })
312}
313
314#[derive(Debug)]
316pub struct SupervisedChild {
317 program: String,
318 child: Child,
319 #[cfg(unix)]
320 process_group: Option<i32>,
321}
322
323impl Drop for SupervisedChild {
324 fn drop(&mut self) {
325 #[cfg(unix)]
326 if let Err(error) = self.terminate_process_group() {
327 tracing::debug!(
328 program = %self.program,
329 %error,
330 "failed to SIGTERM supervised child process group during drop"
331 );
332 }
333 }
334}
335
336impl SupervisedChild {
337 pub fn program(&self) -> &str {
339 &self.program
340 }
341
342 pub fn id(&self) -> Option<u32> {
344 self.child.id()
345 }
346
347 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
349 self.child.stdin.take()
350 }
351
352 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
354 self.child.stdout.take()
355 }
356
357 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
359 self.child.stderr.take()
360 }
361
362 pub fn try_wait(&mut self) -> Result<Option<ExitStatus>, NodeSupervisorError> {
364 self.child
365 .try_wait()
366 .map_err(|source| NodeSupervisorError::Status {
367 program: self.program.clone(),
368 source,
369 })
370 }
371
372 #[tracing::instrument(skip_all, fields(program = %self.program))]
374 pub async fn wait(&mut self) -> Result<ExitStatus, NodeSupervisorError> {
375 self.child
376 .wait()
377 .await
378 .map_err(|source| NodeSupervisorError::Wait {
379 program: self.program.clone(),
380 source,
381 })
382 }
383
384 #[tracing::instrument(skip_all, fields(program = %self.program))]
389 pub async fn kill_and_wait(&mut self) -> Result<ExitStatus, NodeSupervisorError> {
390 if let Some(status) = self.try_wait()? {
391 return Ok(status);
392 }
393
394 self.child
395 .start_kill()
396 .map_err(|source| NodeSupervisorError::Stop {
397 program: self.program.clone(),
398 source,
399 })?;
400 self.wait().await
401 }
402
403 #[cfg(unix)]
405 pub fn terminate_process_group(&self) -> Result<(), NodeSupervisorError> {
406 self.signal_process_group(libc::SIGTERM)
407 }
408
409 #[cfg(unix)]
411 pub fn kill_process_group(&self) -> Result<(), NodeSupervisorError> {
412 self.signal_process_group(libc::SIGKILL)
413 }
414
415 #[cfg(unix)]
417 pub fn process_group_exists(&self) -> Result<bool, NodeSupervisorError> {
418 let Some(target) = self.process_group_signal_target()? else {
419 return Ok(false);
420 };
421
422 process_group_has_live_member(-target).map_err(|source| NodeSupervisorError::Signal {
423 program: self.program.clone(),
424 signal: ChildSignal::from_raw(0),
425 source,
426 })
427 }
428
429 #[cfg(unix)]
431 pub(crate) fn process_group_id(&self) -> Option<i32> {
432 self.process_group
433 }
434
435 #[cfg(unix)]
436 fn signal_process_group(&self, signal: i32) -> Result<(), NodeSupervisorError> {
437 let Some(target) = self.process_group_signal_target()? else {
438 return Ok(());
439 };
440
441 let result = unsafe { libc::kill(target, signal) };
444 if result == 0 {
445 return Ok(());
446 }
447
448 let source = std::io::Error::last_os_error();
449 if source.raw_os_error() == Some(libc::ESRCH) {
450 return Ok(());
451 }
452
453 Err(NodeSupervisorError::Signal {
454 program: self.program.clone(),
455 signal: ChildSignal::from_raw(signal),
456 source,
457 })
458 }
459
460 #[cfg(unix)]
461 fn process_group_signal_target(&self) -> Result<Option<i32>, NodeSupervisorError> {
462 let process_group = match self.process_group {
463 Some(process_group) => process_group,
464 None => match self.id() {
465 Some(_) => child_id_to_i32(self.id(), &self.program)?,
466 None => return Ok(None),
467 },
468 };
469
470 Ok(Some(-process_group))
471 }
472}
473
474#[cfg(unix)]
483pub(crate) fn process_group_has_live_member(process_group: i32) -> std::io::Result<bool> {
484 let result = unsafe { libc::kill(-process_group, 0) };
487 if result != 0 {
488 let source = std::io::Error::last_os_error();
489 return match source.raw_os_error() {
490 Some(libc::ESRCH) => Ok(false),
491 Some(libc::EPERM) => Ok(true),
492 _ => Err(source),
493 };
494 }
495
496 #[cfg(target_os = "linux")]
497 return Ok(linux_process_group_has_running_member(process_group));
498
499 #[cfg(not(target_os = "linux"))]
500 Ok(true)
501}
502
503#[cfg(target_os = "linux")]
513fn linux_process_group_has_running_member(process_group: i32) -> bool {
514 let Ok(entries) = std::fs::read_dir("/proc") else {
515 return true;
516 };
517
518 entries.into_iter().any(|entry| {
519 let Ok(entry) = entry else {
520 return true;
521 };
522 let is_pid = entry
523 .file_name()
524 .to_str()
525 .is_some_and(|name| !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()));
526 if !is_pid {
527 return false;
528 }
529
530 match read_linux_stat(&entry.path().join("stat")) {
531 LinuxStat::Vanished => false,
532 LinuxStat::Unreadable => true,
533 LinuxStat::Read {
534 process_group: member_group,
535 ..
536 } if member_group != process_group => false,
537 LinuxStat::Read { state, .. } if !linux_state_has_exited(state) => true,
538 LinuxStat::Read { .. } => linux_exited_leader_has_running_thread(&entry.path()),
539 }
540 })
541}
542
543#[cfg(target_os = "linux")]
544fn linux_exited_leader_has_running_thread(process_dir: &std::path::Path) -> bool {
545 let tasks = match std::fs::read_dir(process_dir.join("task")) {
546 Ok(tasks) => tasks,
547 Err(error) => return !linux_read_error_means_vanished(&error),
548 };
549
550 tasks.into_iter().any(|task| {
551 let Ok(task) = task else {
552 return true;
553 };
554 match read_linux_stat(&task.path().join("stat")) {
555 LinuxStat::Vanished => false,
556 LinuxStat::Unreadable => true,
557 LinuxStat::Read { state, .. } => !linux_state_has_exited(state),
558 }
559 })
560}
561
562#[cfg(target_os = "linux")]
564#[derive(Debug, PartialEq, Eq)]
565enum LinuxStat {
566 Vanished,
568 Unreadable,
570 Read {
571 state: char,
572 process_group: i32,
573 },
574}
575
576#[cfg(target_os = "linux")]
577fn read_linux_stat(path: &std::path::Path) -> LinuxStat {
578 classify_linux_stat_read(std::fs::read_to_string(path))
579}
580
581#[cfg(target_os = "linux")]
582fn classify_linux_stat_read(read: std::io::Result<String>) -> LinuxStat {
583 match read {
584 Ok(stat) => parse_linux_stat_state_and_group(&stat).map_or(
585 LinuxStat::Unreadable,
586 |(state, process_group)| LinuxStat::Read {
587 state,
588 process_group,
589 },
590 ),
591 Err(error) if linux_read_error_means_vanished(&error) => LinuxStat::Vanished,
592 Err(_) => LinuxStat::Unreadable,
593 }
594}
595
596#[cfg(target_os = "linux")]
597fn linux_read_error_means_vanished(error: &std::io::Error) -> bool {
598 error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(libc::ESRCH)
599}
600
601#[cfg(target_os = "linux")]
606fn parse_linux_stat_state_and_group(stat: &str) -> Option<(char, i32)> {
607 let mut fields = stat.get(stat.rfind(')')? + 1..)?.split_whitespace();
608 let state = fields.next()?.chars().next()?;
609 let _parent_pid = fields.next()?;
610 let process_group = fields.next()?.parse().ok()?;
611 Some((state, process_group))
612}
613
614#[cfg(target_os = "linux")]
615fn linux_state_has_exited(state: char) -> bool {
616 matches!(state, 'Z' | 'X' | 'x')
617}
618
619#[cfg(all(test, unix))]
620mod tests;