1use std::convert::Infallible;
53use std::ffi::OsString;
54use std::fs::File;
55use std::io;
56use std::path::{Path, PathBuf};
57use std::process::{Child, Command, ExitStatus, Stdio};
58use std::time::{Duration, Instant};
59
60use crate::local::ProcessHandle;
61use crate::readiness;
62use crate::readiness::channel::{Channel, ENV};
63
64pub const POLL: Duration = readiness::POLL_INTERVAL;
66
67#[derive(Debug)]
72#[non_exhaustive]
73pub struct SpawnWait {
74 pub live: bool,
76 pub spawn_error: Option<io::Error>,
78}
79
80pub fn spawn(command: &mut Command) -> io::Result<Child> {
87 #[cfg(unix)]
88 {
89 crate::local::unix::spawn_with_child_waiting(command)
90 }
91 #[cfg(windows)]
92 {
93 let _spawn = crate::spawn_lock::spawning();
94 let _guard = crate::local::windows::StdioInheritGuard::suppress()?;
95 command.spawn()
96 }
97}
98
99pub fn spawn_detached(command: &mut Command) -> io::Result<Child> {
104 command
105 .stdin(Stdio::null())
106 .stdout(Stdio::null())
107 .stderr(Stdio::null());
108 spawn(command)
109}
110
111#[derive(Debug, Default)]
113pub enum DaemonOutput {
114 #[default]
116 Null,
117 File(File),
119}
120
121#[derive(Clone, Debug, PartialEq)]
122enum EnvChange {
123 Set(OsString, OsString),
124 Remove(OsString),
125}
126
127#[derive(Debug)]
156pub struct DaemonCommand {
157 program: PathBuf,
158 args: Vec<OsString>,
159 env: Vec<EnvChange>,
160 current_dir: Option<PathBuf>,
161 stdout: DaemonOutput,
162 stderr: DaemonOutput,
163 readiness: bool,
164}
165
166impl DaemonCommand {
167 pub fn new(program: impl AsRef<Path>) -> Self {
169 Self {
170 program: program.as_ref().to_path_buf(),
171 args: Vec::new(),
172 env: Vec::new(),
173 current_dir: None,
174 stdout: DaemonOutput::Null,
175 stderr: DaemonOutput::Null,
176 readiness: false,
177 }
178 }
179
180 pub fn arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
182 self.args.push(arg.into());
183 self
184 }
185
186 pub fn args<I, S>(&mut self, args: I) -> &mut Self
188 where
189 I: IntoIterator<Item = S>,
190 S: Into<OsString>,
191 {
192 self.args.extend(args.into_iter().map(Into::into));
193 self
194 }
195
196 pub fn env(&mut self, name: impl Into<OsString>, value: impl Into<OsString>) -> &mut Self {
201 self.env.push(EnvChange::Set(name.into(), value.into()));
202 self
203 }
204
205 pub fn env_remove(&mut self, name: impl Into<OsString>) -> &mut Self {
207 self.env.push(EnvChange::Remove(name.into()));
208 self
209 }
210
211 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
213 self.current_dir = Some(dir.as_ref().to_path_buf());
214 self
215 }
216
217 pub fn stdout(&mut self, target: DaemonOutput) -> &mut Self {
219 self.stdout = target;
220 self
221 }
222
223 pub fn stderr(&mut self, target: DaemonOutput) -> &mut Self {
225 self.stderr = target;
226 self
227 }
228
229 pub fn readiness_channel(&mut self) -> &mut Self {
266 self.readiness = true;
267 self
268 }
269
270 pub fn spawn(&self) -> io::Result<DaemonChild> {
275 #[cfg(unix)]
276 {
277 use std::os::fd::AsRawFd;
278 let mut command = Command::new(&self.program);
279 command.args(&self.args).stdin(Stdio::null());
280 for change in &self.env {
281 match change {
282 EnvChange::Set(name, value) => command.env(name, value),
283 EnvChange::Remove(name) => command.env_remove(name),
284 };
285 }
286 if let Some(dir) = &self.current_dir {
287 command.current_dir(dir);
288 }
289 command.stdout(unix_stdio(&self.stdout)?);
290 command.stderr(unix_stdio(&self.stderr)?);
291 let readiness = if self.readiness {
292 let (reader, child_end) = crate::local::unix::ready_pipe()?;
293 command.env(ENV, child_end.as_raw_fd().to_string());
294 Some((Channel::start(reader)?, child_end))
295 } else {
296 command.env_remove(ENV);
297 None
298 };
299 let keep = readiness
300 .as_ref()
301 .map(|(_, child_end)| child_end.as_raw_fd());
302 let (inner, exit) =
303 crate::local::unix::spawn_in_new_session_watched(&mut command, keep)?;
304 Ok(DaemonChild {
307 inner: crate::candidate::Reaped::new(inner),
308 exit,
309 readiness: readiness.map(|(channel, _child_end)| channel),
310 })
311 }
312 #[cfg(windows)]
313 {
314 use crate::local::command_line::EnvChange as WideChange;
315 use crate::local::windows::ready;
316 use crate::local::windows_spawn::{Spawn, Target, spawn};
317 use std::os::windows::ffi::OsStrExt;
318 let wide = |text: &OsString| text.encode_wide().collect::<Vec<u16>>();
319 let mut env: Vec<WideChange> = self
320 .env
321 .iter()
322 .map(|change| match change {
323 EnvChange::Set(name, value) => WideChange::Set(wide(name), wide(value)),
324 EnvChange::Remove(name) => WideChange::Remove(wide(name)),
325 })
326 .collect();
327 let name: Vec<u16> = ENV.encode_utf16().collect();
328 let readiness = if self.readiness {
331 let (pipe_name, pipe) = ready::create()?;
332 env.push(WideChange::Set(name, pipe_name.encode_utf16().collect()));
333 let (started, process) = std::sync::mpsc::channel();
334 let channel = Channel::start(ready::Reader::new(pipe, process))?;
335 Some((channel, started))
336 } else {
337 env.push(WideChange::Remove(name));
338 None
339 };
340 fn target(output: &DaemonOutput) -> Target<'_> {
341 match output {
342 DaemonOutput::Null => Target::Null,
343 DaemonOutput::File(file) => Target::File(file),
344 }
345 }
346 let mut inner = spawn(&Spawn {
347 program: &self.program,
348 args: &self.args,
349 env: &env,
350 current_dir: self.current_dir.as_deref(),
351 stdout: target(&self.stdout),
352 stderr: target(&self.stderr),
353 })?;
354 let handles = (|| {
355 let exit = ProcessHandle::open(
356 crate::ProcessId::new(inner.id()).expect("spawned daemon has a nonzero PID"),
357 )?;
358 let readiness = match readiness {
361 Some((channel, started)) => {
362 let _ = started.send(inner.watch()?);
363 Some(channel)
364 }
365 None => None,
366 };
367 Ok((exit, readiness))
368 })();
369 match handles {
370 Ok((exit, readiness)) => Ok(DaemonChild {
371 inner,
372 exit,
373 readiness,
374 }),
375 Err(error) => {
376 let _ = inner.kill();
377 Err(error)
378 }
379 }
380 }
381 }
382}
383
384impl From<&Command> for DaemonCommand {
391 fn from(command: &Command) -> Self {
392 let mut daemon = Self::new(command.get_program());
393 daemon.args(command.get_args());
394 for (name, value) in command.get_envs() {
395 match value {
396 Some(value) => daemon.env(name, value),
397 None => daemon.env_remove(name),
398 };
399 }
400 if let Some(dir) = command.get_current_dir() {
401 daemon.current_dir(dir);
402 }
403 daemon
404 }
405}
406
407#[cfg(unix)]
408fn unix_stdio(target: &DaemonOutput) -> io::Result<Stdio> {
409 Ok(match target {
410 DaemonOutput::Null => Stdio::null(),
411 DaemonOutput::File(file) => Stdio::from(file.try_clone()?),
412 })
413}
414
415#[derive(Debug)]
428pub struct DaemonChild {
429 #[cfg(unix)]
430 inner: crate::candidate::Reaped,
431 #[cfg(windows)]
432 inner: crate::local::windows_spawn::WindowsChild,
433 exit: ProcessHandle,
434 readiness: Option<Channel>,
435}
436
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439#[non_exhaustive]
440pub enum Startup {
441 Live,
443 Exited,
448 TimedOut,
451}
452
453impl DaemonChild {
454 pub fn id(&self) -> crate::ProcessId {
456 crate::ProcessId::new(self.inner.id()).expect("spawned daemon has a nonzero PID")
457 }
458
459 pub fn take_readiness(&mut self) -> Option<Channel> {
462 self.readiness.take()
463 }
464
465 pub fn in_callers_job(&self) -> bool {
475 #[cfg(unix)]
476 {
477 false
478 }
479 #[cfg(windows)]
480 {
481 self.inner.in_callers_job()
482 }
483 }
484
485 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
487 self.inner.try_wait()
488 }
489
490 pub fn wait(&mut self) -> io::Result<ExitStatus> {
492 self.inner.wait()
493 }
494
495 pub fn wait_until(&mut self, deadline: Instant) -> io::Result<Option<ExitStatus>> {
502 if self.exits_by(deadline)? {
503 self.inner.wait().map(Some)
504 } else {
505 Ok(None)
506 }
507 }
508
509 fn exits_by(&mut self, deadline: Instant) -> io::Result<bool> {
513 crate::local::wait_child(|| self.inner.try_wait(), Some(&mut self.exit), deadline)
514 }
515
516 pub fn exit_handle(&mut self) -> &mut ProcessHandle {
519 &mut self.exit
520 }
521
522 pub fn wait_until_live(
531 &mut self,
532 deadline: Instant,
533 mut is_live: impl FnMut() -> bool,
534 ) -> io::Result<Startup> {
535 loop {
536 if is_live() {
537 return Ok(Startup::Live);
538 }
539 let now = Instant::now();
540 if now >= deadline {
541 return Ok(Startup::TimedOut);
542 }
543 if self.exits_by(deadline.min(now + POLL))? {
544 return Ok(if is_live() {
545 Startup::Live
546 } else {
547 Startup::Exited
548 });
549 }
550 }
551 }
552
553 pub fn kill(&mut self) -> io::Result<()> {
556 self.inner.kill()
557 }
558}
559
560pub fn wait_until_live(deadline: Instant, mut is_live: impl FnMut() -> bool) -> bool {
566 readiness::wait_until(deadline, |_| Ok::<_, Infallible>(is_live().then_some(())))
567 .ok()
568 .flatten()
569 .is_some()
570}
571
572pub fn spawn_and_wait(
580 command: &mut Command,
581 budget: Duration,
582 is_live: impl FnMut() -> bool,
583) -> SpawnWait {
584 let spawn_error = spawn_detached(command).err();
585 let live = wait_until_live(Instant::now() + budget, is_live);
586 SpawnWait { live, spawn_error }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn wait_until_live_fails_when_the_probe_never_succeeds() {
595 let started = Instant::now();
596 assert!(!wait_until_live(
597 Instant::now() + Duration::from_millis(300),
598 || false,
599 ));
600 assert!(started.elapsed() >= Duration::from_millis(250));
601 assert!(started.elapsed() < Duration::from_secs(3));
602 }
603
604 #[test]
605 fn wait_until_live_returns_as_soon_as_the_probe_succeeds() {
606 let mut n = 0;
607 assert!(wait_until_live(
608 Instant::now() + Duration::from_secs(2),
609 || {
610 n += 1;
611 n >= 2
612 }
613 ));
614 assert!(n >= 2);
615 }
616
617 #[test]
618 fn a_missing_binary_is_reported_on_spawn_error() {
619 let mut command = Command::new("/this/does/not/exist-kunobi-daemon-launch");
620 let outcome = spawn_and_wait(&mut command, Duration::from_millis(80), || false);
621 assert!(!outcome.live);
622 assert!(outcome.spawn_error.is_some());
623 }
624
625 #[cfg(unix)]
626 #[test]
627 fn a_stale_socket_file_is_not_live() {
628 let dir = tempfile::tempdir().unwrap();
629 let sock = dir.path().join("b.sock");
630 {
631 let listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
632 drop(listener);
633 }
634 assert!(sock.exists());
635 let stale_deadline = Instant::now() + Duration::from_secs(1);
636 while std::os::unix::net::UnixStream::connect(&sock).is_ok() {
637 assert!(
638 Instant::now() < stale_deadline,
639 "closed listener stayed live"
640 );
641 #[expect(
642 clippy::disallowed_methods,
643 reason = "Bounded test probe waits for the closed Unix listener to stop accepting."
644 )]
645 std::thread::sleep(Duration::from_millis(10));
646 }
647 assert!(!wait_until_live(
648 Instant::now() + Duration::from_millis(300),
649 || std::os::unix::net::UnixStream::connect(&sock).is_ok(),
650 ));
651 }
652
653 #[cfg(unix)]
654 #[test]
655 fn spawn_and_wait_still_sees_a_peer_another_client_started() {
656 let dir = tempfile::tempdir().unwrap();
657 let sock = dir.path().join("b.sock");
658 let listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
659 let mut command = Command::new("/this/does/not/exist-kunobi-daemon-launch");
660 let outcome = spawn_and_wait(&mut command, Duration::from_secs(2), || {
661 std::os::unix::net::UnixStream::connect(&sock).is_ok()
662 });
663 assert!(outcome.live);
664 assert!(outcome.spawn_error.is_some());
665 drop(listener);
666 }
667
668 #[cfg(unix)]
669 #[test]
670 fn spawn_and_wait_waits_for_a_peer_that_binds_later() {
671 let dir = tempfile::tempdir().unwrap();
672 let sock = dir.path().join("b.sock");
673 let delayed_socket = sock.clone();
674 let (stop_tx, stop_rx) = std::sync::mpsc::channel();
675 let delayed = std::thread::spawn(move || {
676 #[expect(
677 clippy::disallowed_methods,
678 reason = "The fixture binds after startup to exercise retrying a failed liveness probe."
679 )]
680 std::thread::sleep(Duration::from_millis(250));
681 let listener = std::os::unix::net::UnixListener::bind(delayed_socket).unwrap();
682 let _ = stop_rx.recv_timeout(Duration::from_secs(2));
683 listener
684 });
685 let mut command = Command::new("/this/does/not/exist-kunobi-daemon-launch");
686 let outcome = spawn_and_wait(&mut command, Duration::from_secs(2), || {
687 std::os::unix::net::UnixStream::connect(&sock).is_ok()
688 });
689 stop_tx.send(()).unwrap();
690 drop(delayed.join().unwrap());
691 assert!(outcome.live);
692 }
693
694 #[cfg(unix)]
695 fn sleeper() -> DaemonChild {
696 let mut command = DaemonCommand::new("/bin/sleep");
697 command.arg("30");
698 command.spawn().unwrap()
699 }
700
701 #[cfg(unix)]
704 #[test]
705 #[ignore = "helper process for the SIGINT test"]
706 fn detach_sigint_helper() {
707 let Some(out) = std::env::var_os("KDAEMON_DETACH_HELPER_OUT") else {
708 return;
709 };
710 let child = sleeper();
711 std::fs::write(&out, child.id().to_string()).unwrap();
712 #[expect(
714 clippy::disallowed_methods,
715 reason = "The signal-test helper must stay alive until the parent sends SIGINT."
716 )]
717 std::thread::sleep(Duration::from_secs(30));
718 }
719
720 #[cfg(unix)]
721 #[test]
722 fn a_daemon_survives_sigint_to_its_callers_process_group() {
723 use std::os::unix::process::CommandExt;
724 let dir = tempfile::tempdir().unwrap();
725 let out = dir.path().join("pid");
726 let mut helper = Command::new(std::env::current_exe().unwrap())
729 .args([
730 "--exact",
731 "launch::tests::detach_sigint_helper",
732 "--ignored",
733 "--nocapture",
734 "--test-threads=1",
735 ])
736 .env("KDAEMON_DETACH_HELPER_OUT", &out)
737 .stdout(Stdio::null())
738 .process_group(0)
739 .spawn()
740 .unwrap();
741 let deadline = Instant::now() + Duration::from_secs(20);
742 let daemon: u32 = loop {
743 if let Ok(pid) = std::fs::read_to_string(&out)
744 && let Ok(pid) = pid.trim().parse()
745 {
746 break pid;
747 }
748 assert!(Instant::now() < deadline, "helper never started the daemon");
749 #[expect(
750 clippy::disallowed_methods,
751 reason = "The cross-process fixture reports its daemon PID through a file, with a deadline."
752 )]
753 std::thread::sleep(Duration::from_millis(20));
754 };
755 let mut daemon_exit = ProcessHandle::open(crate::ProcessId::new(daemon).unwrap()).unwrap();
757 crate::local::unix::interrupt_process_group(helper.id()).unwrap();
758 let status = helper.wait().unwrap();
759 let died = daemon_exit
762 .wait_until(Instant::now() + Duration::from_secs(2))
763 .unwrap();
764 let _ = Command::new("kill")
765 .args(["-KILL", &daemon.to_string()])
766 .status();
767 assert!(!status.success(), "SIGINT did not reach the helper's group");
768 assert!(!died, "the daemon died with its caller's process group");
769 }
770
771 #[cfg(unix)]
773 fn reaches_eof_within(read: File, timeout: Duration) -> bool {
774 use std::io::Read;
775 let (done, finished) = std::sync::mpsc::channel();
776 std::thread::spawn(move || {
777 let mut read = read;
778 let mut buffer = [0u8; 64];
779 while matches!(read.read(&mut buffer), Ok(n) if n > 0) {}
780 let _ = done.send(());
781 });
782 finished.recv_timeout(timeout).is_ok()
783 }
784
785 #[cfg(unix)]
786 #[test]
787 fn a_daemon_does_not_keep_the_callers_pipe_open() {
788 let (read, write) = crate::local::unix::inheritable_pipe();
789 let mut child = sleeper();
790 drop(write);
791 let eof = reaches_eof_within(read, Duration::from_secs(5));
792 child.kill().unwrap();
793 child.wait().unwrap();
794 assert!(eof, "the daemon kept the caller's pipe open");
795 }
796
797 #[cfg(unix)]
800 #[test]
801 fn a_plain_spawn_would_keep_the_callers_pipe_open() {
802 let (read, write) = crate::local::unix::inheritable_pipe();
803 let mut child = Command::new("/bin/sleep")
804 .arg("30")
805 .stdin(Stdio::null())
806 .stdout(Stdio::null())
807 .stderr(Stdio::null())
808 .spawn()
809 .unwrap();
810 drop(write);
811 let eof = reaches_eof_within(read, Duration::from_secs(1));
812 child.kill().unwrap();
813 child.wait().unwrap();
814 assert!(!eof, "a plain spawn is expected to inherit the write end");
815 }
816
817 #[cfg(unix)]
818 #[test]
819 fn a_daemon_gets_its_output_file_env_and_directory() {
820 let dir = tempfile::tempdir().unwrap();
821 let log = dir.path().join("daemon.log");
822 let file = File::create(&log).unwrap();
823 assert!(std::env::var_os("CARGO_MANIFEST_DIR").is_some());
826 let mut command = DaemonCommand::new("/bin/sh");
827 command
828 .args([
829 "-c",
830 "echo \"$KDAEMON_SET|${CARGO_MANIFEST_DIR:-unset}|$(pwd)\" >&2",
831 ])
832 .env("KDAEMON_SET", "set-by-builder")
833 .env_remove("CARGO_MANIFEST_DIR")
834 .current_dir(dir.path())
835 .stderr(DaemonOutput::File(file));
836 let mut child = command.spawn().unwrap();
837 assert!(child.wait().unwrap().success());
838 let written = std::fs::read_to_string(&log).unwrap();
839 let expected_dir = dir.path().canonicalize().unwrap();
840 assert_eq!(
841 written.trim(),
842 format!("set-by-builder|unset|{}", expected_dir.display())
843 );
844 }
845
846 #[test]
847 fn a_daemon_command_takes_a_command_s_program_arguments_environment_and_directory() {
848 let mut command = Command::new("exampled");
849 command
850 .args(["serve", "--quiet"])
851 .env("KDAEMON_SET", "set-by-command")
852 .env_remove("KDAEMON_REMOVED")
853 .current_dir("daemon-dir");
854 let daemon = DaemonCommand::from(&command);
855 assert_eq!(daemon.program, Path::new("exampled"));
856 assert_eq!(daemon.args, ["serve", "--quiet"]);
857 assert_eq!(daemon.env.len(), 2);
859 assert!(daemon.env.contains(&EnvChange::Set(
860 "KDAEMON_SET".into(),
861 "set-by-command".into()
862 )));
863 assert!(
864 daemon
865 .env
866 .contains(&EnvChange::Remove("KDAEMON_REMOVED".into()))
867 );
868 assert_eq!(daemon.current_dir.as_deref(), Some(Path::new("daemon-dir")));
869 }
870
871 #[test]
872 fn a_missing_daemon_binary_is_a_spawn_error() {
873 let error = DaemonCommand::new("/this/does/not/exist-kunobi-daemon")
874 .spawn()
875 .unwrap_err();
876 assert_eq!(error.kind(), io::ErrorKind::NotFound);
877 }
878
879 #[cfg(unix)]
881 struct Held {
882 fifo: PathBuf,
883 _dir: tempfile::TempDir,
884 }
885
886 #[cfg(unix)]
887 impl Held {
888 fn release(&self) {
890 std::fs::write(&self.fifo, b"go\n").unwrap();
891 }
892 }
893
894 #[cfg(unix)]
895 impl Drop for Held {
896 fn drop(&mut self) {
898 use std::io::Write;
899 use std::os::unix::fs::OpenOptionsExt;
900 if let Ok(mut fifo) = std::fs::OpenOptions::new()
901 .write(true)
902 .custom_flags(libc::O_NONBLOCK)
903 .open(&self.fifo)
904 {
905 let _ = fifo.write_all(b"go\n");
906 }
907 }
908 }
909
910 #[cfg(unix)]
914 fn held_daemon() -> (DaemonChild, Held) {
915 let dir = tempfile::tempdir().unwrap();
916 let fifo = dir.path().join("release");
917 assert!(
918 Command::new("mkfifo")
919 .arg(&fifo)
920 .status()
921 .unwrap()
922 .success()
923 );
924 let mut command = DaemonCommand::new("/bin/sh");
925 command
926 .args(["-c", "read line < \"$1\"; exit 3", "sh"])
927 .arg(&fifo);
928 (command.spawn().unwrap(), Held { fifo, _dir: dir })
929 }
930
931 #[cfg(unix)]
932 #[test]
933 fn a_daemon_that_exits_before_it_is_live_ends_the_wait() {
934 let (mut child, held) = held_daemon();
935 held.release();
936 let started = Instant::now();
937 let outcome = child
938 .wait_until_live(started + Duration::from_secs(60), || false)
939 .unwrap();
940 assert_eq!(outcome, Startup::Exited);
941 assert!(started.elapsed() < Duration::from_secs(30));
942 assert_eq!(child.wait().unwrap().code(), Some(3));
943 }
944
945 #[cfg(unix)]
946 #[test]
947 fn a_live_daemon_is_reported_as_soon_as_a_probe_succeeds() {
948 let (mut child, held) = held_daemon();
949 let mut probes = 0;
950 let started = Instant::now();
951 let outcome = child
952 .wait_until_live(started + Duration::from_secs(60), || {
953 probes += 1;
954 probes == 3
955 })
956 .unwrap();
957 assert_eq!(outcome, Startup::Live);
958 assert_eq!(probes, 3);
959 assert!(started.elapsed() < Duration::from_secs(30));
960
961 assert!(child.wait_until(Instant::now()).unwrap().is_none());
962 held.release();
963 let status = child
964 .wait_until(Instant::now() + Duration::from_secs(30))
965 .unwrap()
966 .expect("the daemon exited once released");
967 assert_eq!(status.code(), Some(3));
968 }
969
970 #[cfg(unix)]
971 #[test]
972 fn a_daemon_that_never_becomes_live_times_out_without_busy_probing() {
973 let (mut child, held) = held_daemon();
974 let mut probes = 0;
975 let deadline = Instant::now() + Duration::from_millis(200);
976 let outcome = child
977 .wait_until_live(deadline, || {
978 probes += 1;
979 false
980 })
981 .unwrap();
982 assert_eq!(outcome, Startup::TimedOut);
983 assert!(Instant::now() >= deadline);
984 assert!(probes <= 20, "{probes} probes in 200 ms");
986 held.release();
987 child.wait().unwrap();
988 }
989
990 #[cfg(unix)]
991 #[test]
992 fn a_daemon_that_exits_while_another_serves_is_live() {
993 let (mut child, held) = held_daemon();
996 held.release();
997 child.exit_handle().wait().unwrap();
998 assert_eq!(child.exit_handle().pid(), child.id());
999 let mut probes = 0;
1000 let outcome = child
1001 .wait_until_live(Instant::now() + Duration::from_secs(60), || {
1002 probes += 1;
1003 probes == 2
1004 })
1005 .unwrap();
1006 assert_eq!(outcome, Startup::Live);
1007 assert_eq!(child.wait().unwrap().code(), Some(3));
1008 }
1009
1010 #[cfg(unix)]
1012 #[allow(unsafe_code)]
1013 fn reaped(pid: crate::ProcessId) -> bool {
1014 let pid = libc::pid_t::try_from(pid.get()).unwrap();
1015 let result = unsafe { libc::kill(pid, 0) };
1018 result == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
1019 }
1020
1021 #[cfg(unix)]
1023 fn reaped_soon(pid: crate::ProcessId) -> bool {
1024 let deadline = Instant::now() + Duration::from_secs(10);
1025 crate::backoff::poll(|| Ok(reaped(pid)), Some(deadline)).unwrap()
1026 }
1027
1028 #[cfg(unix)]
1029 #[test]
1030 fn a_dropped_daemon_keeps_running_and_is_reaped_once_it_exits() {
1031 let (child, held) = held_daemon();
1032 let pid = child.id();
1033 let mut exit = ProcessHandle::open(pid).unwrap();
1034 drop(child);
1035 assert!(
1036 !exit
1037 .wait_until(Instant::now() + Duration::from_millis(200))
1038 .unwrap(),
1039 "dropping the daemon's handle stopped it"
1040 );
1041 held.release();
1042 assert!(exit.wait_until(Instant::now() + SILENCE).unwrap());
1043 assert!(reaped_soon(pid), "the exited daemon was left a zombie");
1044 }
1045
1046 #[test]
1047 fn a_daemon_taken_as_a_candidate_is_stopped_when_dropped_unselected() {
1048 let child = silent_daemon().spawn().unwrap();
1049 let pid = child.id();
1050 let mut exit = ProcessHandle::open(pid).unwrap();
1051 let mut candidate = crate::Candidate::from(child);
1052 assert_eq!(candidate.id(), pid);
1053 assert_eq!(candidate.exit_handle().unwrap().pid(), pid);
1054 assert!(candidate.try_wait().unwrap().is_none());
1055 assert!(candidate.wait_until(Instant::now()).unwrap().is_none());
1056 drop(candidate);
1057 assert!(
1058 exit.has_exited().unwrap(),
1059 "the unselected daemon was not stopped"
1060 );
1061 }
1062
1063 #[cfg(unix)]
1064 #[test]
1065 fn a_selected_daemon_candidate_keeps_running_and_is_reaped_once_it_exits() {
1066 let (child, held) = held_daemon();
1067 let pid = child.id();
1068 let mut exit = ProcessHandle::open(pid).unwrap();
1069 let mut candidate = crate::Candidate::from(child);
1070 candidate.selected();
1071 drop(candidate);
1072 assert!(
1073 !exit
1074 .wait_until(Instant::now() + Duration::from_millis(200))
1075 .unwrap(),
1076 "dropping a selected candidate stopped it"
1077 );
1078 held.release();
1079 assert!(exit.wait_until(Instant::now() + SILENCE).unwrap());
1080 assert!(reaped_soon(pid), "the exited daemon was left a zombie");
1081 }
1082
1083 #[cfg(unix)]
1084 #[test]
1085 fn spawn_detached_execs() {
1086 let bin = ["/usr/bin/true", "/bin/true"]
1087 .into_iter()
1088 .find(|path| std::path::Path::new(path).exists());
1089 let Some(bin) = bin else {
1090 return;
1091 };
1092 let mut command = Command::new(bin);
1093 let mut child = spawn_detached(&mut command).unwrap();
1094 assert!(child.wait().unwrap().success());
1095 }
1096
1097 use crate::readiness::channel::{NotReady, Notifier};
1100
1101 const SILENCE: Duration = Duration::from_secs(60);
1103
1104 fn progress_then_ready(mut notifier: Notifier) -> io::Result<()> {
1105 notifier.progress("")?;
1106 notifier.progress("loading")?;
1107 notifier.ready()
1108 }
1109
1110 #[test]
1113 #[ignore = "helper process for the readiness tests"]
1114 #[allow(unsafe_code)]
1115 fn readiness_helper() {
1116 let (Some(mode), Some(out)) = (
1117 std::env::var_os("KDAEMON_READY_HELPER"),
1118 std::env::var_os("KDAEMON_READY_HELPER_OUT"),
1119 ) else {
1120 return;
1121 };
1122 let taken = unsafe { Notifier::from_env() };
1126 let left = std::env::var_os(ENV).is_some();
1128 let again = unsafe { Notifier::from_env() };
1130 let report = match (mode.to_str(), taken) {
1131 _ if left => "the variable was left in the environment".to_owned(),
1132 _ if !matches!(again, Ok(None)) => format!("taken twice: {again:?}"),
1133 (Some("progress-then-ready"), Ok(Some(notifier))) => {
1134 match progress_then_ready(notifier) {
1135 Ok(()) => "sent".to_owned(),
1136 Err(error) => error.to_string(),
1137 }
1138 }
1139 (Some("exit-before-ready"), Ok(Some(_notifier))) => "exiting".to_owned(),
1140 (Some("no-channel"), Ok(None)) => "no channel".to_owned(),
1141 (mode, taken) => format!("unexpected: {mode:?} {taken:?}"),
1142 };
1143 std::fs::write(out, report).unwrap();
1144 }
1145
1146 fn readiness_helper_daemon(mode: &str, out: &Path) -> DaemonCommand {
1148 let mut command = DaemonCommand::new(std::env::current_exe().unwrap());
1149 command
1150 .args([
1151 "--exact",
1152 "launch::tests::readiness_helper",
1153 "--ignored",
1154 "--nocapture",
1155 "--test-threads=1",
1156 ])
1157 .env("KDAEMON_READY_HELPER", mode)
1158 .env("KDAEMON_READY_HELPER_OUT", out.as_os_str());
1159 command
1160 }
1161
1162 fn silent_daemon() -> DaemonCommand {
1164 #[cfg(unix)]
1165 {
1166 let mut command = DaemonCommand::new("/bin/sleep");
1167 command.arg("30");
1168 command
1169 }
1170 #[cfg(windows)]
1171 {
1172 let root = std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into());
1173 let mut command =
1174 DaemonCommand::new(PathBuf::from(root).join("System32").join("PING.EXE"));
1175 command.args(["-n", "30", "127.0.0.1"]);
1176 command
1177 }
1178 }
1179
1180 #[test]
1181 fn a_daemon_reports_progress_then_ready() {
1182 let dir = tempfile::tempdir().unwrap();
1183 let out = dir.path().join("report");
1184 let mut child = readiness_helper_daemon("progress-then-ready", &out)
1185 .readiness_channel()
1186 .spawn()
1187 .unwrap();
1188 let mut channel = child.take_readiness().expect("the channel was asked for");
1189 assert!(
1190 child.take_readiness().is_none(),
1191 "the channel is taken once"
1192 );
1193 assert_eq!(channel.wait(SILENCE), Ok(()));
1194 assert_eq!(channel.progress_reports(), 2);
1195 assert_eq!(channel.last_progress().as_deref(), Some("loading"));
1196 assert!(child.wait().unwrap().success());
1197 assert_eq!(std::fs::read_to_string(&out).unwrap(), "sent");
1198 }
1199
1200 #[test]
1201 fn a_daemon_that_exits_before_ready_is_noticed_at_once() {
1202 let dir = tempfile::tempdir().unwrap();
1203 let out = dir.path().join("report");
1204 let mut child = readiness_helper_daemon("exit-before-ready", &out)
1205 .readiness_channel()
1206 .spawn()
1207 .unwrap();
1208 let mut channel = child.take_readiness().unwrap();
1209 assert!(
1212 child
1213 .exit_handle()
1214 .wait_until(Instant::now() + SILENCE)
1215 .unwrap()
1216 );
1217 assert_eq!(channel.wait(SILENCE), Err(NotReady::Died));
1218 assert_eq!(
1219 child
1220 .wait_until_live(Instant::now() + SILENCE, || false)
1221 .unwrap(),
1222 Startup::Exited,
1223 );
1224 child.wait().unwrap();
1225 assert_eq!(std::fs::read_to_string(&out).unwrap(), "exiting");
1226 }
1227
1228 #[test]
1229 fn a_killed_daemon_ends_its_channel() {
1230 let mut child = silent_daemon().readiness_channel().spawn().unwrap();
1231 let mut channel = child.take_readiness().unwrap();
1232 assert_eq!(channel.wait(Duration::ZERO), Err(NotReady::Silent));
1234 child.kill().unwrap();
1235 child.wait().unwrap();
1236 assert_eq!(channel.wait(SILENCE), Err(NotReady::Died));
1238 }
1239
1240 #[test]
1241 fn without_opting_in_the_daemon_gets_no_channel() {
1242 let dir = tempfile::tempdir().unwrap();
1243 let out = dir.path().join("report");
1244 let mut command = readiness_helper_daemon("no-channel", &out);
1245 command.env(ENV, "5");
1247 let mut child = command.spawn().unwrap();
1248 assert!(child.take_readiness().is_none());
1249 assert!(child.wait().unwrap().success());
1250 assert_eq!(std::fs::read_to_string(&out).unwrap(), "no channel");
1251 }
1252
1253 #[cfg(unix)]
1254 #[test]
1255 fn a_daemon_with_a_readiness_channel_still_does_not_keep_the_callers_pipe_open() {
1256 let (read, write) = crate::local::unix::inheritable_pipe();
1257 let mut command = DaemonCommand::new("/bin/sleep");
1258 command.arg("30").readiness_channel();
1259 let mut child = command.spawn().unwrap();
1260 drop(write);
1261 let eof = reaches_eof_within(read, Duration::from_secs(5));
1262 child.kill().unwrap();
1263 child.wait().unwrap();
1264 assert!(eof, "the channel's exception let the caller's pipe through");
1265 }
1266
1267 #[cfg(feature = "async")]
1268 #[tokio::test]
1269 async fn a_daemon_s_readiness_can_be_awaited() {
1270 let dir = tempfile::tempdir().unwrap();
1271 let out = dir.path().join("report");
1272 let mut child = readiness_helper_daemon("progress-then-ready", &out)
1273 .readiness_channel()
1274 .spawn()
1275 .unwrap();
1276 let mut channel = child.take_readiness().unwrap();
1277 assert_eq!(channel.wait_async(SILENCE).await, Ok(()));
1278 assert_eq!(channel.progress_reports(), 2);
1279 assert!(child.wait().unwrap().success());
1280 }
1281}