1use std::cfg_select;
9pub mod foreground;
11mod semantic_priority;
12pub use semantic_priority::ProcessPriority;
13#[cfg(feature = "async-process")]
14mod spawn_admission;
15#[cfg(feature = "async-process")]
16pub use spawn_admission::SpawnAdmission;
17#[cfg(feature = "async-process")]
18use std::ffi::{OsStr, OsString};
19#[cfg(feature = "async-process")]
20use std::io;
21#[cfg(feature = "async-process")]
22use std::path::PathBuf;
23#[cfg(feature = "async-process")]
24use std::process::{ExitStatus, Output, Stdio};
25
26#[cfg(feature = "async-process")]
27use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
28#[cfg(feature = "async-process")]
29use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
30
31pub mod platform;
36
37#[cfg(all(feature = "independent-spawn", test, not(windows)))]
38#[path = "platform_win/scheduler_error.rs"]
39mod scheduler_error;
40#[cfg(feature = "independent-spawn")]
41pub(crate) use platform_imp::spawn_sync_owned_daemon;
42#[cfg(feature = "independent-spawn")]
43pub use platform_imp::{
44 independent_broker_run, independent_broker_spawn, independent_spawn, IndependentChild,
45};
46#[cfg(feature = "independent-spawn")]
47pub(crate) use platform_imp::{independent_open_regular, INDEPENDENT_ZERO_WRITE_PENDING};
48
49#[cfg(feature = "pty")]
55#[doc(hidden)]
56pub use portable_pty as portable_pty_compat;
57
58cfg_select! {
61 target_os = "windows" => {
62 mod platform_win;
63 pub(crate) use platform_win as platform_imp;
64 }
65 target_os = "linux" => {
66 mod platform_linux;
67 pub(crate) use platform_linux as platform_imp;
68 }
69 target_os = "macos" => {
70 mod platform_macos;
71 pub(crate) use platform_macos as platform_imp;
72 }
73}
74
75pub use platform_imp::{
79 assign_child_to_windows_job, cancel_capture_reader, canonical_environment_pairs,
80 capture_reader_done, compat_shell_command, configure_exact_trace, configure_process_command,
81 configure_process_command_for_bounded_owner_death, configure_sync_contained_command,
82 configure_sync_daemon_command, configure_sync_daemon_command_with_inheritance,
83 configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
84 monitor_console_windows, parent_has_console, prepare_capture_reader, set_process_name,
85 shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
86 spawn_sync_daemon_with_inheritance, start_descendant_monitor, start_exact_trace,
87 sync_child_native_handle, trampoline_exit_code, unix_mark_extra_fds_close_on_exec,
88 unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
89 CaptureCancellation, TracedChild, WindowsJobHandle,
90};
91
92#[cfg(feature = "terminal-graphics")]
93pub use platform_imp::active_graphics_probe;
94
95#[cfg(feature = "window-icon")]
96pub use platform_imp::{set_window_icon_impl, window_icon_support_impl};
97
98#[cfg(feature = "async-process")]
99pub(crate) use platform_imp::{
100 async_child_cpu_time, async_child_identity, signal_async_child, signal_async_child_group,
101 AsyncChildIdentity,
102};
103
104#[cfg(feature = "process-inspection")]
105pub use platform_imp::{kill_tree, process_snapshot, process_snapshot_for_pid};
106
107pub use platform_imp::{autostart_register, autostart_render_registration, autostart_unregister};
108
109pub use platform_imp::{process_install_owner_death_cleanup, process_owner_death_cleanup_target};
110
111pub use platform_imp::process_install_shutdown_request_handler;
112
113pub use platform_imp::fs_write_all_to_descriptor;
114
115pub use platform_imp::{process_can_replace_current_image, process_replace_current_image};
116
117pub use platform_imp::{
118 process_executable_path, process_force_kill, process_same_executable_path,
119 process_signal_terminate, ProcessLiveness,
120};
121
122pub use platform_imp::{
123 resources_fd_exhaustion_error, resources_inode_capacity, resources_signals_fd_exhaustion,
124 resources_signals_storage_exhaustion, resources_storage_exhaustion_error,
125};
126
127pub use platform_imp::{
128 executable_file_name, executable_sibling_of_current_image, EXECUTABLE_EXTENSION,
129};
130
131#[cfg(feature = "fs")]
132pub use platform_imp::{
133 fs_create_private_file, fs_decode_path_bytes, fs_encode_path_bytes, fs_file_identity,
134 fs_is_lock_conflict, fs_open_lock_file, fs_path_identity, fs_replace_file, fs_sync_directory,
135 fs_try_lock_exclusive, fs_unlock, fs_user_config_dir, fs_user_data_dir, fs_user_run_data_root,
136 fs_user_runtime_dir, fs_user_state_dir, FsFileIdentity,
137};
138
139pub use platform_imp::{
140 host_boot_id, host_current_process_privilege, host_environment_keys_are_case_insensitive,
141 host_filesystem_device_id, host_hostname, host_login_environment, host_machine_id,
142 host_namespace_id, host_user_machine_identity, HostPrivilegedIdentity,
143};
144
145pub use platform_imp::host_login_environment_block;
146
147pub use platform_imp::terminal_input;
148
149#[cfg(feature = "ipc")]
150pub use platform_imp::{
151 ipc_broker_endpoint_name as IpcBrokerEndpointName, ipc_broker_v1_endpoint_path,
152 ipc_broker_v2_runtime_dir, ipc_current_user_id, ipc_endpoint_is_filesystem_backed,
153 ipc_endpoint_name_limit, ipc_endpoint_scope_bytes, ipc_nonblocking_zero_read_is_pending,
154 ipc_select_endpoint_address, IpcEndpoint, IpcInheritedListener, IpcListener,
155 IpcListenerNonblockingMode, IpcPeerIdentity, IpcPeerIdentitySource, IpcStream,
156};
157
158#[cfg(feature = "private-dir")]
159pub use platform_imp::{
160 private_dir_ensure_owner_private_directory, private_dir_owner_private_directory,
161};
162
163#[cfg(feature = "ipc")]
166pub use platform_imp::{
167 private_dir_ensure_owner_private_directory as ipc_ensure_owner_private_directory,
168 private_dir_owner_private_directory as ipc_owner_private_directory,
169};
170
171#[cfg(feature = "ipc")]
176#[doc(hidden)]
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct LegacyHandoffError {
179 kind: platform::ipc::HandoffTransferErrorKind,
180 raw_os_error: Option<i32>,
181 transferred_bytes: Option<usize>,
182 expected_bytes: Option<usize>,
183 detail: Option<String>,
184}
185
186#[cfg(feature = "ipc")]
187impl LegacyHandoffError {
188 pub(crate) fn new(
189 kind: platform::ipc::HandoffTransferErrorKind,
190 raw_os_error: Option<i32>,
191 ) -> Self {
192 Self {
193 kind,
194 raw_os_error,
195 transferred_bytes: None,
196 expected_bytes: None,
197 detail: None,
198 }
199 }
200
201 pub(crate) fn with_detail(
202 kind: platform::ipc::HandoffTransferErrorKind,
203 raw_os_error: Option<i32>,
204 detail: impl Into<String>,
205 ) -> Self {
206 Self {
207 kind,
208 raw_os_error,
209 transferred_bytes: None,
210 expected_bytes: None,
211 detail: Some(detail.into()),
212 }
213 }
214
215 #[doc(hidden)]
216 pub fn partial(transferred_bytes: usize, expected_bytes: usize) -> Self {
217 Self {
218 kind: platform::ipc::HandoffTransferErrorKind::Failed,
219 raw_os_error: None,
220 transferred_bytes: Some(transferred_bytes),
221 expected_bytes: Some(expected_bytes),
222 detail: Some(format!(
223 "SCM_RIGHTS connection transfer was partial ({transferred_bytes}/{expected_bytes} bytes)"
224 )),
225 }
226 }
227
228 pub fn kind(&self) -> platform::ipc::HandoffTransferErrorKind {
230 self.kind
231 }
232
233 pub fn raw_os_error(&self) -> Option<i32> {
235 self.raw_os_error
236 }
237
238 pub fn partial_counts(&self) -> Option<(usize, usize)> {
240 self.transferred_bytes.zip(self.expected_bytes)
241 }
242
243 pub(crate) fn detail(&self) -> Option<&str> {
244 self.detail.as_deref()
245 }
246}
247
248#[cfg(feature = "ipc")]
250#[doc(hidden)]
251pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
252 platform_imp::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
253
254#[cfg(feature = "ipc")]
256#[doc(hidden)]
257pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool =
258 platform_imp::LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED;
259
260#[cfg(feature = "ipc")]
262#[doc(hidden)]
263pub fn legacy_send_fd_to(
264 socket: &std::path::Path,
265 sent_fd: i32,
266 payload: &[u8],
267) -> Result<(), LegacyHandoffError> {
268 platform_imp::legacy_send_fd_to(socket, sent_fd, payload)
269}
270
271#[cfg(feature = "ipc")]
273#[doc(hidden)]
274pub fn legacy_send_fd_over(
275 socket_fd: i32,
276 sent_fd: i32,
277 payload: &[u8],
278) -> Result<(), LegacyHandoffError> {
279 platform_imp::legacy_send_fd_over(socket_fd, sent_fd, payload)
280}
281
282#[cfg(feature = "ipc")]
284#[doc(hidden)]
285pub fn legacy_duplicate_handle(
286 source_handle: usize,
287 backend_pid: u32,
288) -> Result<usize, LegacyHandoffError> {
289 platform_imp::legacy_duplicate_handle(source_handle, backend_pid)
290}
291
292#[cfg(feature = "ipc")]
300#[doc(hidden)]
301pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
302 platform_imp::into_legacy_ipc_stream(stream)
303}
304
305#[cfg(feature = "ipc")]
307#[doc(hidden)]
308pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
309 platform_imp::from_legacy_ipc_stream(stream)
310}
311
312#[cfg(feature = "ipc")]
315#[doc(hidden)]
316pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
317 platform_imp::legacy_ipc_name(path)
318}
319
320#[cfg(feature = "ipc-async")]
321pub use platform_imp::{
322 IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
323};
324
325#[cfg(feature = "pty")]
326pub use platform_imp::terminal::{
327 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
328 input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
329 query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
330 wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
331 OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
332};
333
334#[cfg(feature = "session-relay")]
335pub use platform_imp::relay_local_socket_session;
336
337#[cfg(feature = "async-process")]
342pub fn configure_compat_tokio_command(
343 command: &mut Command,
344 show_console: bool,
345 kill_when_owner_dies: bool,
346) -> io::Result<()> {
347 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
348}
349
350#[cfg(feature = "async-process")]
352pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) -> io::Result<()> {
353 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
354}
355
356#[cfg(feature = "async-process")]
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum StreamMode {
360 Inherit,
362 Piped,
364 Null,
366}
367
368#[cfg(feature = "async-process")]
369impl StreamMode {
370 fn apply(self) -> Stdio {
371 match self {
372 Self::Inherit => Stdio::inherit(),
373 Self::Piped => Stdio::piped(),
374 Self::Null => Stdio::null(),
375 }
376 }
377}
378
379#[cfg(feature = "async-process")]
381#[derive(Debug, Clone)]
382pub struct SpawnSpec {
383 program: OsString,
384 args: Vec<OsString>,
385 current_dir: Option<PathBuf>,
386 env: Vec<(OsString, OsString)>,
387 clear_env: bool,
388 stdin: StreamMode,
389 stdout: StreamMode,
390 stderr: StreamMode,
391 create_process_group: bool,
392 kill_when_owner_dies: bool,
393 nice: Option<i32>,
394 admission: Option<SpawnAdmission>,
395}
396
397#[cfg(feature = "async-process")]
398impl SpawnSpec {
399 pub fn new(program: impl Into<OsString>) -> Self {
401 Self {
402 program: program.into(),
403 args: Vec::new(),
404 current_dir: None,
405 env: Vec::new(),
406 clear_env: false,
407 stdin: StreamMode::Inherit,
408 stdout: StreamMode::Inherit,
409 stderr: StreamMode::Inherit,
410 create_process_group: false,
411 kill_when_owner_dies: false,
412 nice: None,
413 admission: None,
414 }
415 }
416
417 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
419 self.args.push(arg.into());
420 self
421 }
422
423 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
425 self.current_dir = Some(path.into());
426 self
427 }
428
429 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
431 self.env.push((key.into(), value.into()));
432 self
433 }
434
435 pub fn clear_env(mut self, clear: bool) -> Self {
437 self.clear_env = clear;
438 self
439 }
440
441 pub fn stdin(mut self, mode: StreamMode) -> Self {
443 self.stdin = mode;
444 self
445 }
446
447 pub fn stdout(mut self, mode: StreamMode) -> Self {
449 self.stdout = mode;
450 self
451 }
452
453 pub fn stderr(mut self, mode: StreamMode) -> Self {
455 self.stderr = mode;
456 self
457 }
458
459 pub fn create_process_group(mut self, create: bool) -> Self {
468 self.create_process_group = create;
469 self
470 }
471
472 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
481 self.kill_when_owner_dies = kill;
482 self
483 }
484
485 pub fn nice(mut self, nice: Option<i32>) -> Self {
492 self.nice = nice;
493 self
494 }
495
496 pub fn priority(self, priority: ProcessPriority) -> Self {
498 self.nice(priority.nice_value())
499 }
500
501 pub fn priority_best_effort(self, priority: ProcessPriority) -> Self {
506 self.priority(priority)
507 }
508
509 pub fn spawn_admission(mut self, admission: SpawnAdmission) -> Self {
511 self.admission = Some(admission);
512 self
513 }
514
515 pub async fn spawn(self) -> io::Result<PlatformChild> {
517 let mut command = Command::new(&self.program);
518 command.args(&self.args);
519 if let Some(current_dir) = self.current_dir.as_deref() {
520 command.current_dir(current_dir);
521 }
522 if self.clear_env {
523 command.env_clear();
524 }
525 for (key, value) in &self.env {
526 command.env(key, value);
527 }
528 command
529 .stdin(self.stdin.apply())
530 .stdout(self.stdout.apply())
531 .stderr(self.stderr.apply());
532 platform_imp::configure_command(
533 &mut command,
534 self.create_process_group,
535 self.kill_when_owner_dies,
536 self.nice,
537 )?;
538
539 let mut spawn = || command.spawn();
540 let child = match self.admission.as_ref() {
541 Some(admission) => admission.run(spawn)?,
542 None => spawn()?,
543 };
544 platform_imp::after_spawn(&child, self.kill_when_owner_dies)?;
545 Ok(PlatformChild::new(child, self.create_process_group))
546 }
547}
548
549#[cfg(feature = "async-process")]
551pub struct PlatformChild {
552 child: Child,
553 stdin: Option<ChildStdin>,
554 stdout: Option<ChildStdout>,
555 stderr: Option<ChildStderr>,
556 signal: PlatformEmergencySignal,
557}
558
559#[cfg(feature = "async-process")]
560impl PlatformChild {
561 fn new(mut child: Child, own_process_group: bool) -> Self {
562 let signal = PlatformEmergencySignal {
563 identity: async_child_identity(&child),
564 own_process_group,
565 legacy_group_pid: child.id(),
570 };
571 Self {
572 stdin: child.stdin.take(),
573 stdout: child.stdout.take(),
574 stderr: child.stderr.take(),
575 child,
576 signal,
577 }
578 }
579
580 pub fn id(&self) -> Option<u32> {
582 self.child.id()
583 }
584
585 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
587 self.child.wait().await
588 }
589
590 pub async fn kill(&mut self) -> io::Result<()> {
592 self.child.kill().await
593 }
594
595 pub async fn wait_with_output(self) -> io::Result<Output> {
597 let Self {
598 mut child,
599 stdin,
600 stdout,
601 stderr,
602 ..
603 } = self;
604 drop(stdin);
607 let (status, stdout, stderr) = tokio::try_join!(
608 child.wait(),
609 read_owned_to_end(stdout),
610 read_owned_to_end(stderr),
611 )?;
612 Ok(Output {
613 status,
614 stdout,
615 stderr,
616 })
617 }
618
619 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
621 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
622 stdin.write_all(bytes).await?;
623 stdin.flush().await
624 }
625
626 pub fn close_stdin(&mut self) {
631 drop(self.stdin.take());
632 }
633
634 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
636 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
637 let mut bytes = Vec::new();
638 stdout.read_to_end(&mut bytes).await?;
639 Ok(bytes)
640 }
641
642 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
644 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
645 let mut bytes = Vec::new();
646 stderr.read_to_end(&mut bytes).await?;
647 Ok(bytes)
648 }
649
650 pub fn into_actor_parts(
656 self,
657 ) -> (
658 PlatformLifecycle,
659 PlatformEmergencySignal,
660 Option<PlatformStdin>,
661 Option<PlatformOutput>,
662 Option<PlatformOutput>,
663 ) {
664 (
665 PlatformLifecycle { child: self.child },
666 self.signal,
667 self.stdin.map(|stdin| PlatformStdin { stdin }),
668 self.stdout.map(PlatformOutput::stdout),
669 self.stderr.map(PlatformOutput::stderr),
670 )
671 }
672}
673
674#[cfg(feature = "async-process")]
676pub struct PlatformLifecycle {
677 child: Child,
678}
679
680#[cfg(feature = "async-process")]
681impl PlatformLifecycle {
682 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
684 self.child.wait().await
685 }
686
687 pub fn start_kill(&mut self) -> io::Result<()> {
695 self.child.start_kill()
696 }
697}
698
699#[cfg(feature = "async-process")]
704pub struct PlatformEmergencySignal {
705 identity: Option<AsyncChildIdentity>,
706 own_process_group: bool,
707 legacy_group_pid: Option<u32>,
708}
709
710#[cfg(feature = "async-process")]
711impl PlatformEmergencySignal {
712 pub fn kill(&self) -> io::Result<()> {
714 let Some(identity) = self.identity.as_ref() else {
715 return Err(signal_target_unavailable());
716 };
717 signal_async_child(identity)
718 }
719
720 pub fn terminate_group_soft(&self) -> io::Result<bool> {
729 if !self.own_process_group {
730 return Ok(false);
731 }
732 let Some(identity) = self.identity.as_ref() else {
733 return Err(signal_target_unavailable());
734 };
735 signal_async_child_group(identity).map(|()| true)
736 }
737
738 pub fn terminate_group_soft_legacy(&self) -> io::Result<bool> {
747 if !self.own_process_group {
748 return Ok(false);
749 }
750 if let Some(identity) = self.identity.as_ref() {
751 return signal_async_child_group(identity).map(|()| true);
752 }
753 let pid = self
754 .legacy_group_pid
755 .ok_or_else(signal_target_unavailable)?;
756 crate::platform::process::soft_terminate_process_group(pid).map(|()| true)
757 }
758
759 pub fn cpu_time(&self) -> io::Result<Option<std::time::Duration>> {
763 self.identity
764 .as_ref()
765 .map_or(Ok(None), async_child_cpu_time)
766 }
767}
768
769#[cfg(feature = "async-process")]
770fn signal_target_unavailable() -> io::Error {
771 io::Error::new(
772 io::ErrorKind::BrokenPipe,
773 "child process launch identity is no longer available",
774 )
775}
776
777#[cfg(feature = "async-process")]
779pub struct PlatformStdin {
780 stdin: ChildStdin,
781}
782
783#[cfg(feature = "async-process")]
784impl PlatformStdin {
785 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
787 self.stdin.write_all(bytes).await?;
788 self.stdin.flush().await
789 }
790}
791
792#[cfg(feature = "async-process")]
794pub struct PlatformOutput {
795 reader: OutputReader,
796 read_pending: bool,
797}
798
799#[cfg(feature = "async-process")]
800enum OutputReader {
801 Stdout(ChildStdout),
802 Stderr(ChildStderr),
803}
804
805#[cfg(feature = "async-process")]
806impl PlatformOutput {
807 fn stdout(stdout: ChildStdout) -> Self {
808 Self {
809 reader: OutputReader::Stdout(stdout),
810 read_pending: false,
811 }
812 }
813
814 fn stderr(stderr: ChildStderr) -> Self {
815 Self {
816 reader: OutputReader::Stderr(stderr),
817 read_pending: false,
818 }
819 }
820
821 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
823 match self.reader {
824 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
825 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
826 }
827 }
828
829 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
834 self.read_pending = true;
835 let result = match &mut self.reader {
836 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
837 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
838 };
839 self.read_pending = false;
840 result
841 }
842
843 pub async fn shutdown(self) -> io::Result<()> {
850 match self.reader {
851 OutputReader::Stdout(stdout) => {
852 platform_imp::shutdown_output_reader(stdout, self.read_pending).await
853 }
854 OutputReader::Stderr(stderr) => {
855 platform_imp::shutdown_output_reader(stderr, self.read_pending).await
856 }
857 }
858 }
859}
860
861#[cfg(all(test, feature = "async-process"))]
862mod output_shutdown_tests {
863 use super::*;
864 use std::time::Duration;
865
866 #[test]
867 fn silent_output_fixture() {
868 if std::env::var_os("RUNNING_PROCESS_OUTPUT_SHUTDOWN_FIXTURE").is_some() {
869 std::thread::sleep(Duration::from_secs(30));
870 }
871 }
872
873 #[tokio::test]
874 async fn shutdown_finishes_a_cancelled_pending_read_before_child_exit() {
875 let child = SpawnSpec::new(std::env::current_exe().expect("test executable"))
876 .arg("--exact")
877 .arg("output_shutdown_tests::silent_output_fixture")
878 .env("RUNNING_PROCESS_OUTPUT_SHUTDOWN_FIXTURE", "1")
879 .stdin(StreamMode::Null)
880 .stdout(StreamMode::Piped)
881 .stderr(StreamMode::Null)
882 .spawn()
883 .await
884 .expect("spawn silent fixture");
885 let (mut lifecycle, _, _, stdout, _) = child.into_actor_parts();
886 let mut stdout = stdout.expect("stdout pipe");
887 let mut bytes = [0; 1024];
889 loop {
890 match tokio::time::timeout(Duration::from_millis(20), stdout.read_chunk(&mut bytes))
891 .await
892 {
893 Ok(Ok(0)) => panic!("fixture exited before pending read"),
894 Ok(Ok(_)) => {}
895 Ok(Err(error)) => panic!("fixture read failed: {error}"),
896 Err(_) => break,
897 }
898 }
899 let shutdown = tokio::time::timeout(Duration::from_secs(2), stdout.shutdown()).await;
900 lifecycle.start_kill().expect("kill fixture");
903 lifecycle.wait().await.expect("reap fixture");
904 shutdown
905 .expect("pending read shutdown must finish")
906 .expect("output shutdown");
907 }
908}
909
910#[cfg(feature = "async-process")]
911fn stdin_not_piped() -> io::Error {
912 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
913}
914
915#[cfg(feature = "async-process")]
916fn stdout_not_piped() -> io::Error {
917 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
918}
919
920#[cfg(feature = "async-process")]
921fn stderr_not_piped() -> io::Error {
922 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
923}
924
925#[cfg(feature = "async-process")]
926async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
927where
928 R: AsyncRead + Unpin,
929{
930 let Some(mut reader) = reader else {
931 return Ok(Vec::new());
932 };
933 let mut bytes = Vec::new();
934 reader.read_to_end(&mut bytes).await?;
935 Ok(bytes)
936}
937
938#[cfg(feature = "async-process")]
940pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
941 platform_imp::shell_spec(command.as_ref())
942}
943
944#[cfg(all(test, feature = "async-process"))]
945mod tests {
946 use super::{shell_spec, SpawnSpec, StreamMode};
947
948 fn fixture_command() -> SpawnSpec {
949 #[cfg(windows)]
950 {
951 shell_spec("echo async-platform-internal")
952 }
953 #[cfg(not(windows))]
954 {
955 shell_spec("printf async-platform-internal")
956 }
957 }
958
959 #[tokio::test]
960 async fn blessed_spawn_captures_output_without_sync_wait() {
961 let output = fixture_command()
962 .stdout(StreamMode::Piped)
963 .stderr(StreamMode::Piped)
964 .spawn()
965 .await
966 .expect("spawn")
967 .wait_with_output()
968 .await
969 .expect("wait with output");
970
971 assert!(output.status.success());
972 let expected = if cfg!(windows) {
973 b"async-platform-internal\r\n".as_slice()
974 } else {
975 b"async-platform-internal".as_slice()
976 };
977 assert_eq!(output.stdout, expected);
978 assert!(output.stderr.is_empty());
979 }
980
981 #[tokio::test]
982 async fn blessed_spawn_reports_missing_program() {
983 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
984 .spawn()
985 .await;
986 assert!(result.is_err());
987 }
988
989 #[tokio::test]
990 async fn one_shot_output_closes_owned_stdin() {
991 #[cfg(windows)]
992 let spec = shell_spec("more > nul & echo done");
993 #[cfg(not(windows))]
994 let spec = shell_spec("cat > /dev/null; printf done");
995
996 let output = tokio::time::timeout(
997 std::time::Duration::from_secs(2),
998 spec.stdin(StreamMode::Piped)
999 .stdout(StreamMode::Piped)
1000 .stderr(StreamMode::Piped)
1001 .spawn()
1002 .await
1003 .expect("spawn")
1004 .wait_with_output(),
1005 )
1006 .await
1007 .expect("stdin is closed for one-shot output")
1008 .expect("output succeeds");
1009
1010 let expected = if cfg!(windows) {
1011 b"done\r\n".as_slice()
1012 } else {
1013 b"done".as_slice()
1014 };
1015 assert_eq!(output.stdout, expected);
1016 }
1017}