1use std::ffi::{OsStr, OsString};
9use std::io;
10use std::path::PathBuf;
11use std::process::{ExitStatus, Output, Stdio};
12
13use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
14use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
15
16#[cfg(windows)]
19const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum StreamMode {
24 Inherit,
26 Piped,
28 Null,
30}
31
32impl StreamMode {
33 fn apply(self) -> Stdio {
34 match self {
35 Self::Inherit => Stdio::inherit(),
36 Self::Piped => Stdio::piped(),
37 Self::Null => Stdio::null(),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct SpawnSpec {
45 program: OsString,
46 args: Vec<OsString>,
47 current_dir: Option<PathBuf>,
48 env: Vec<(OsString, OsString)>,
49 clear_env: bool,
50 stdin: StreamMode,
51 stdout: StreamMode,
52 stderr: StreamMode,
53 create_process_group: bool,
54 kill_when_owner_dies: bool,
55}
56
57impl SpawnSpec {
58 pub fn new(program: impl Into<OsString>) -> Self {
60 Self {
61 program: program.into(),
62 args: Vec::new(),
63 current_dir: None,
64 env: Vec::new(),
65 clear_env: false,
66 stdin: StreamMode::Inherit,
67 stdout: StreamMode::Inherit,
68 stderr: StreamMode::Inherit,
69 create_process_group: false,
70 kill_when_owner_dies: false,
71 }
72 }
73
74 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
76 self.args.push(arg.into());
77 self
78 }
79
80 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
82 self.current_dir = Some(path.into());
83 self
84 }
85
86 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
88 self.env.push((key.into(), value.into()));
89 self
90 }
91
92 pub fn clear_env(mut self, clear: bool) -> Self {
94 self.clear_env = clear;
95 self
96 }
97
98 pub fn stdin(mut self, mode: StreamMode) -> Self {
100 self.stdin = mode;
101 self
102 }
103
104 pub fn stdout(mut self, mode: StreamMode) -> Self {
106 self.stdout = mode;
107 self
108 }
109
110 pub fn stderr(mut self, mode: StreamMode) -> Self {
112 self.stderr = mode;
113 self
114 }
115
116 pub fn create_process_group(mut self, create: bool) -> Self {
125 self.create_process_group = create;
126 self
127 }
128
129 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
136 self.kill_when_owner_dies = kill;
137 self
138 }
139
140 pub async fn spawn(self) -> io::Result<PlatformChild> {
142 let mut command = Command::new(&self.program);
143 command.args(&self.args);
144 if let Some(current_dir) = self.current_dir.as_deref() {
145 command.current_dir(current_dir);
146 }
147 if self.clear_env {
148 command.env_clear();
149 }
150 for (key, value) in &self.env {
151 command.env(key, value);
152 }
153 command
154 .stdin(self.stdin.apply())
155 .stdout(self.stdout.apply())
156 .stderr(self.stderr.apply());
157 if self.create_process_group {
158 #[cfg(unix)]
159 command.process_group(0);
160 #[cfg(windows)]
161 command.creation_flags(CREATE_NEW_PROCESS_GROUP);
162 }
163 #[cfg(target_os = "linux")]
164 if self.kill_when_owner_dies {
165 let owner_pid = unsafe { libc::getpid() };
166 unsafe {
168 command.pre_exec(move || {
169 if libc::prctl(
170 libc::PR_SET_PDEATHSIG,
171 libc::SIGTERM as libc::c_ulong,
172 0,
173 0,
174 0,
175 ) == -1
176 {
177 return Err(io::Error::last_os_error());
178 }
179 if libc::getppid() != owner_pid {
182 libc::kill(libc::getpid(), libc::SIGTERM);
183 }
184 Ok(())
185 });
186 }
187 }
188 #[cfg(target_os = "macos")]
189 if self.kill_when_owner_dies {
190 let owner_pid = unsafe { libc::getpid() };
191 unsafe {
195 command.pre_exec(move || {
196 let supervisor = libc::fork();
197 if supervisor < 0 {
198 return Err(io::Error::last_os_error());
199 }
200 if supervisor == 0 {
201 macos_owner_death_supervisor(owner_pid);
202 }
203 Ok(())
204 });
205 }
206 }
207 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
208 let _ = self.kill_when_owner_dies;
209
210 let child = command.spawn()?;
211 #[cfg(windows)]
212 if self.kill_when_owner_dies {
213 windows_owner_death_job::assign(child.raw_handle());
214 }
215 Ok(PlatformChild::new(child, self.create_process_group))
216 }
217}
218
219#[cfg(target_os = "macos")]
227fn macos_owner_death_supervisor(owner_pid: libc::pid_t) -> ! {
228 let target_pid = unsafe { libc::getppid() };
229 unsafe {
230 for fd in 3..1024 {
234 libc::close(fd);
235 }
236 }
237
238 let queue = unsafe { libc::kqueue() };
239 if queue < 0 {
240 unsafe { libc::_exit(127) };
241 }
242
243 let mut watches = [
244 libc::kevent {
245 ident: owner_pid as libc::uintptr_t,
246 filter: libc::EVFILT_PROC,
247 flags: libc::EV_ADD | libc::EV_ONESHOT,
248 fflags: libc::NOTE_EXIT,
249 data: 0,
250 udata: std::ptr::null_mut(),
251 },
252 libc::kevent {
253 ident: target_pid as libc::uintptr_t,
254 filter: libc::EVFILT_PROC,
255 flags: libc::EV_ADD | libc::EV_ONESHOT,
256 fflags: libc::NOTE_EXIT,
257 data: 0,
258 udata: std::ptr::null_mut(),
259 },
260 ];
261 let registered = unsafe {
262 libc::kevent(
263 queue,
264 watches.as_mut_ptr(),
265 watches.len() as i32,
266 std::ptr::null_mut(),
267 0,
268 std::ptr::null(),
269 )
270 };
271 if registered < 0 {
272 unsafe {
273 libc::close(queue);
274 libc::_exit(127);
275 }
276 }
277
278 if unsafe { libc::kill(owner_pid, 0) } < 0
281 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
282 {
283 unsafe {
284 libc::kill(target_pid, libc::SIGTERM);
285 libc::close(queue);
286 libc::_exit(0);
287 }
288 }
289
290 let mut events = [unsafe { std::mem::zeroed::<libc::kevent>() }];
291 loop {
292 let count = unsafe {
293 libc::kevent(
294 queue,
295 std::ptr::null(),
296 0,
297 events.as_mut_ptr(),
298 1,
299 std::ptr::null(),
300 )
301 };
302 if count <= 0 {
303 if count < 0 && io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
304 continue;
305 }
306 break;
307 }
308 if events[0].ident == owner_pid as libc::uintptr_t {
309 unsafe {
310 libc::kill(target_pid, libc::SIGTERM);
311 }
312 }
313 break;
314 }
315 unsafe {
316 libc::close(queue);
317 libc::_exit(0);
318 }
319}
320
321#[cfg(windows)]
322mod windows_owner_death_job {
323 use std::sync::OnceLock;
324 use windows_sys::Win32::Foundation::HANDLE;
325 use windows_sys::Win32::System::JobObjects::{
326 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
327 SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
328 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
329 };
330
331 struct Job(HANDLE);
332 unsafe impl Send for Job {}
333 unsafe impl Sync for Job {}
334
335 static JOB: OnceLock<Option<Job>> = OnceLock::new();
336
337 fn create() -> Option<Job> {
338 unsafe {
339 let handle = CreateJobObjectW(std::ptr::null(), std::ptr::null());
340 if handle.is_null() {
341 return None;
342 }
343 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
344 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
345 if SetInformationJobObject(
346 handle,
347 JobObjectExtendedLimitInformation,
348 &info as *const _ as *const _,
349 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
350 ) == 0
351 {
352 return None;
353 }
354 Some(Job(handle))
355 }
356 }
357
358 pub(super) fn assign(child: Option<HANDLE>) {
359 let Some(child) = child else { return };
360 let Some(job) = JOB.get_or_init(create).as_ref() else {
361 return;
362 };
363 unsafe {
364 AssignProcessToJobObject(job.0, child);
365 }
366 }
367}
368
369pub struct PlatformChild {
371 child: Child,
372 stdin: Option<ChildStdin>,
373 stdout: Option<ChildStdout>,
374 stderr: Option<ChildStderr>,
375 signal: PlatformEmergencySignal,
376}
377
378impl PlatformChild {
379 fn new(mut child: Child, own_process_group: bool) -> Self {
380 let signal = PlatformEmergencySignal {
381 pid: child.id(),
382 own_process_group,
383 };
384 Self {
385 stdin: child.stdin.take(),
386 stdout: child.stdout.take(),
387 stderr: child.stderr.take(),
388 child,
389 signal,
390 }
391 }
392
393 pub fn id(&self) -> Option<u32> {
395 self.child.id()
396 }
397
398 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
400 self.child.wait().await
401 }
402
403 pub async fn kill(&mut self) -> io::Result<()> {
405 self.child.kill().await
406 }
407
408 pub async fn wait_with_output(self) -> io::Result<Output> {
410 let Self {
411 mut child,
412 stdin,
413 stdout,
414 stderr,
415 ..
416 } = self;
417 drop(stdin);
420 let (status, stdout, stderr) = tokio::try_join!(
421 child.wait(),
422 read_owned_to_end(stdout),
423 read_owned_to_end(stderr),
424 )?;
425 Ok(Output {
426 status,
427 stdout,
428 stderr,
429 })
430 }
431
432 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
434 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
435 stdin.write_all(bytes).await?;
436 stdin.flush().await
437 }
438
439 pub fn close_stdin(&mut self) {
444 drop(self.stdin.take());
445 }
446
447 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
449 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
450 let mut bytes = Vec::new();
451 stdout.read_to_end(&mut bytes).await?;
452 Ok(bytes)
453 }
454
455 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
457 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
458 let mut bytes = Vec::new();
459 stderr.read_to_end(&mut bytes).await?;
460 Ok(bytes)
461 }
462
463 pub fn into_actor_parts(
469 self,
470 ) -> (
471 PlatformLifecycle,
472 PlatformEmergencySignal,
473 Option<PlatformStdin>,
474 Option<PlatformOutput>,
475 Option<PlatformOutput>,
476 ) {
477 (
478 PlatformLifecycle { child: self.child },
479 self.signal,
480 self.stdin.map(|stdin| PlatformStdin { stdin }),
481 self.stdout.map(PlatformOutput::stdout),
482 self.stderr.map(PlatformOutput::stderr),
483 )
484 }
485}
486
487pub struct PlatformLifecycle {
489 child: Child,
490}
491
492impl PlatformLifecycle {
493 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
495 self.child.wait().await
496 }
497}
498
499pub struct PlatformEmergencySignal {
504 pid: Option<u32>,
505 own_process_group: bool,
506}
507
508impl PlatformEmergencySignal {
509 pub fn kill(&self) -> io::Result<()> {
511 signal_process(self.target()?)
512 }
513
514 pub fn terminate_group_soft(&self) -> io::Result<bool> {
523 if !self.own_process_group {
524 return Ok(false);
525 }
526 signal_process_group(self.target()?).map(|()| true)
527 }
528
529 fn target(&self) -> io::Result<u32> {
530 self.pid.ok_or_else(|| {
531 io::Error::new(
532 io::ErrorKind::BrokenPipe,
533 "child process no longer has an emergency signal target",
534 )
535 })
536 }
537}
538
539pub struct PlatformStdin {
541 stdin: ChildStdin,
542}
543
544impl PlatformStdin {
545 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
547 self.stdin.write_all(bytes).await?;
548 self.stdin.flush().await
549 }
550}
551
552pub struct PlatformOutput {
554 reader: OutputReader,
555}
556
557enum OutputReader {
558 Stdout(ChildStdout),
559 Stderr(ChildStderr),
560}
561
562impl PlatformOutput {
563 fn stdout(stdout: ChildStdout) -> Self {
564 Self {
565 reader: OutputReader::Stdout(stdout),
566 }
567 }
568
569 fn stderr(stderr: ChildStderr) -> Self {
570 Self {
571 reader: OutputReader::Stderr(stderr),
572 }
573 }
574
575 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
577 match self.reader {
578 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
579 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
580 }
581 }
582
583 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
588 match &mut self.reader {
589 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
590 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
591 }
592 }
593}
594
595fn stdin_not_piped() -> io::Error {
596 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
597}
598
599fn stdout_not_piped() -> io::Error {
600 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
601}
602
603fn stderr_not_piped() -> io::Error {
604 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
605}
606
607async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
608where
609 R: AsyncRead + Unpin,
610{
611 let Some(mut reader) = reader else {
612 return Ok(Vec::new());
613 };
614 let mut bytes = Vec::new();
615 reader.read_to_end(&mut bytes).await?;
616 Ok(bytes)
617}
618
619#[cfg(unix)]
620fn signal_process(pid: u32) -> io::Result<()> {
621 unix_kill(pid as i32, libc::SIGKILL)
622}
623
624#[cfg(unix)]
627fn signal_process_group(pid: u32) -> io::Result<()> {
628 unix_kill(-(pid as i32), libc::SIGTERM)
629}
630
631#[cfg(unix)]
632fn unix_kill(target: i32, signal: i32) -> io::Result<()> {
633 let result = unsafe { libc::kill(target, signal) };
635 if result == 0 {
636 return Ok(());
637 }
638 let error = io::Error::last_os_error();
639 if error.raw_os_error() == Some(libc::ESRCH) {
640 Ok(())
641 } else {
642 Err(error)
643 }
644}
645
646#[cfg(windows)]
647fn signal_process(pid: u32) -> io::Result<()> {
648 use windows_sys::Win32::Foundation::{CloseHandle, ERROR_INVALID_PARAMETER};
649 use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
650
651 let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
652 if handle.is_null() {
653 let error = io::Error::last_os_error();
654 return if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
655 Ok(())
656 } else {
657 Err(error)
658 };
659 }
660 let terminated = unsafe { TerminateProcess(handle, 1) };
661 let termination_error = if terminated == 0 {
662 Some(io::Error::last_os_error())
663 } else {
664 None
665 };
666 unsafe { CloseHandle(handle) };
667 termination_error.map_or(Ok(()), Err)
668}
669
670#[cfg(windows)]
675fn signal_process_group(pid: u32) -> io::Result<()> {
676 use windows_sys::Win32::Foundation::ERROR_INVALID_HANDLE;
677 use windows_sys::Win32::System::Console::{GenerateConsoleCtrlEvent, CTRL_BREAK_EVENT};
678
679 if unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) } != 0 {
681 return Ok(());
682 }
683 let error = io::Error::last_os_error();
684 if error.raw_os_error() == Some(ERROR_INVALID_HANDLE as i32) {
687 Ok(())
688 } else {
689 Err(error)
690 }
691}
692
693pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
695 #[cfg(windows)]
696 {
697 SpawnSpec::new("cmd.exe").arg("/C").arg(command.as_ref())
698 }
699 #[cfg(not(windows))]
700 {
701 SpawnSpec::new("/bin/sh").arg("-c").arg(command.as_ref())
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::{shell_spec, SpawnSpec, StreamMode};
708
709 fn fixture_command() -> SpawnSpec {
710 #[cfg(windows)]
711 {
712 shell_spec("echo async-platform-internal")
713 }
714 #[cfg(not(windows))]
715 {
716 shell_spec("printf async-platform-internal")
717 }
718 }
719
720 #[tokio::test]
721 async fn blessed_spawn_captures_output_without_sync_wait() {
722 let output = fixture_command()
723 .stdout(StreamMode::Piped)
724 .stderr(StreamMode::Piped)
725 .spawn()
726 .await
727 .expect("spawn")
728 .wait_with_output()
729 .await
730 .expect("wait with output");
731
732 assert!(output.status.success());
733 let expected = if cfg!(windows) {
734 b"async-platform-internal\r\n".as_slice()
735 } else {
736 b"async-platform-internal".as_slice()
737 };
738 assert_eq!(output.stdout, expected);
739 assert!(output.stderr.is_empty());
740 }
741
742 #[tokio::test]
743 async fn blessed_spawn_reports_missing_program() {
744 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
745 .spawn()
746 .await;
747 assert!(result.is_err());
748 }
749
750 #[tokio::test]
751 async fn one_shot_output_closes_owned_stdin() {
752 #[cfg(windows)]
753 let spec = shell_spec("more > nul & echo done");
754 #[cfg(not(windows))]
755 let spec = shell_spec("cat > /dev/null; printf done");
756
757 let output = tokio::time::timeout(
758 std::time::Duration::from_secs(2),
759 spec.stdin(StreamMode::Piped)
760 .stdout(StreamMode::Piped)
761 .stderr(StreamMode::Piped)
762 .spawn()
763 .await
764 .expect("spawn")
765 .wait_with_output(),
766 )
767 .await
768 .expect("stdin is closed for one-shot output")
769 .expect("output succeeds");
770
771 let expected = if cfg!(windows) {
772 b"done\r\n".as_slice()
773 } else {
774 b"done".as_slice()
775 };
776 assert_eq!(output.stdout, expected);
777 }
778}