1use std::cfg_select;
9#[cfg(feature = "async-process")]
10use std::ffi::{OsStr, OsString};
11#[cfg(feature = "async-process")]
12use std::io;
13#[cfg(feature = "async-process")]
14use std::path::PathBuf;
15#[cfg(feature = "async-process")]
16use std::process::{ExitStatus, Output, Stdio};
17
18#[cfg(feature = "async-process")]
19use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
20#[cfg(feature = "async-process")]
21use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
22
23pub mod platform;
28
29#[cfg(feature = "pty")]
35#[doc(hidden)]
36pub use portable_pty as portable_pty_compat;
37
38cfg_select! {
41 target_os = "windows" => {
42 mod platform_win;
43 pub(crate) use platform_win as platform_imp;
44 }
45 target_os = "linux" => {
46 mod platform_linux;
47 pub(crate) use platform_linux as platform_imp;
48 }
49 target_os = "macos" => {
50 mod platform_macos;
51 pub(crate) use platform_macos as platform_imp;
52 }
53}
54
55pub use platform_imp::{
59 assign_child_to_windows_job, cancel_capture_reader, canonical_environment_pairs,
60 capture_reader_done, compat_shell_command, configure_exact_trace, configure_process_command,
61 configure_process_command_for_bounded_owner_death, configure_sync_contained_command,
62 configure_sync_daemon_command, configure_sync_daemon_command_with_inheritance,
63 configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
64 monitor_console_windows, parent_has_console, prepare_capture_reader, set_process_name,
65 set_window_icon_impl, shell_command, soft_terminate_process_group, spawn_sync,
66 spawn_sync_daemon, spawn_sync_daemon_with_inheritance, start_descendant_monitor,
67 start_exact_trace, sync_child_native_handle, trampoline_exit_code,
68 unix_mark_extra_fds_close_on_exec, unix_set_priority, unix_signal_process,
69 unix_signal_process_group, unix_signal_raw, window_icon_support_impl, CaptureCancellation,
70 TracedChild, WindowsJobHandle,
71};
72
73#[cfg(feature = "terminal-graphics")]
74pub use platform_imp::active_graphics_probe;
75
76#[cfg(feature = "async-process")]
77pub(crate) use platform_imp::{
78 async_child_cpu_time, async_child_identity, signal_async_child, signal_async_child_group,
79 AsyncChildIdentity,
80};
81
82#[cfg(feature = "process-inspection")]
83pub use platform_imp::{kill_tree, process_snapshot, process_snapshot_for_pid};
84
85pub use platform_imp::{autostart_register, autostart_render_registration, autostart_unregister};
86
87pub use platform_imp::{process_install_owner_death_cleanup, process_owner_death_cleanup_target};
88
89pub use platform_imp::process_install_shutdown_request_handler;
90
91pub use platform_imp::fs_write_all_to_descriptor;
92
93pub use platform_imp::{process_can_replace_current_image, process_replace_current_image};
94
95pub use platform_imp::{
96 process_executable_path, process_force_kill, process_same_executable_path,
97 process_signal_terminate, ProcessLiveness,
98};
99
100pub use platform_imp::{
101 resources_fd_exhaustion_error, resources_inode_capacity, resources_signals_fd_exhaustion,
102 resources_signals_storage_exhaustion, resources_storage_exhaustion_error,
103};
104
105pub use platform_imp::{
106 executable_file_name, executable_sibling_of_current_image, EXECUTABLE_EXTENSION,
107};
108
109#[cfg(feature = "fs")]
110pub use platform_imp::{
111 fs_create_private_file, fs_decode_path_bytes, fs_encode_path_bytes, fs_file_identity,
112 fs_is_lock_conflict, fs_open_lock_file, fs_path_identity, fs_replace_file, fs_sync_directory,
113 fs_try_lock_exclusive, fs_unlock, fs_user_config_dir, fs_user_data_dir, fs_user_run_data_root,
114 fs_user_runtime_dir, fs_user_state_dir, FsFileIdentity,
115};
116
117pub use platform_imp::{
118 host_boot_id, host_current_process_privilege, host_environment_keys_are_case_insensitive,
119 host_filesystem_device_id, host_hostname, host_login_environment, host_machine_id,
120 host_namespace_id, host_user_machine_identity, HostPrivilegedIdentity,
121};
122
123pub use platform_imp::host_login_environment_block;
124
125pub use platform_imp::terminal_input;
126
127#[cfg(feature = "ipc")]
128pub use platform_imp::{
129 ipc_broker_endpoint_name as IpcBrokerEndpointName, ipc_broker_v1_endpoint_path,
130 ipc_broker_v2_runtime_dir, ipc_current_user_id, ipc_endpoint_is_filesystem_backed,
131 ipc_endpoint_name_limit, ipc_endpoint_scope_bytes, ipc_nonblocking_zero_read_is_pending,
132 ipc_select_endpoint_address, IpcEndpoint, IpcInheritedListener, IpcListener,
133 IpcListenerNonblockingMode, IpcPeerIdentity, IpcPeerIdentitySource, IpcStream,
134};
135
136#[cfg(feature = "private-dir")]
137pub use platform_imp::{
138 private_dir_ensure_owner_private_directory, private_dir_owner_private_directory,
139};
140
141#[cfg(feature = "ipc")]
144pub use platform_imp::{
145 private_dir_ensure_owner_private_directory as ipc_ensure_owner_private_directory,
146 private_dir_owner_private_directory as ipc_owner_private_directory,
147};
148
149#[cfg(feature = "ipc")]
154#[doc(hidden)]
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct LegacyHandoffError {
157 kind: platform::ipc::HandoffTransferErrorKind,
158 raw_os_error: Option<i32>,
159 transferred_bytes: Option<usize>,
160 expected_bytes: Option<usize>,
161 detail: Option<String>,
162}
163
164#[cfg(feature = "ipc")]
165impl LegacyHandoffError {
166 pub(crate) fn new(
167 kind: platform::ipc::HandoffTransferErrorKind,
168 raw_os_error: Option<i32>,
169 ) -> Self {
170 Self {
171 kind,
172 raw_os_error,
173 transferred_bytes: None,
174 expected_bytes: None,
175 detail: None,
176 }
177 }
178
179 pub(crate) fn with_detail(
180 kind: platform::ipc::HandoffTransferErrorKind,
181 raw_os_error: Option<i32>,
182 detail: impl Into<String>,
183 ) -> Self {
184 Self {
185 kind,
186 raw_os_error,
187 transferred_bytes: None,
188 expected_bytes: None,
189 detail: Some(detail.into()),
190 }
191 }
192
193 #[doc(hidden)]
194 pub fn partial(transferred_bytes: usize, expected_bytes: usize) -> Self {
195 Self {
196 kind: platform::ipc::HandoffTransferErrorKind::Failed,
197 raw_os_error: None,
198 transferred_bytes: Some(transferred_bytes),
199 expected_bytes: Some(expected_bytes),
200 detail: Some(format!(
201 "SCM_RIGHTS connection transfer was partial ({transferred_bytes}/{expected_bytes} bytes)"
202 )),
203 }
204 }
205
206 pub fn kind(&self) -> platform::ipc::HandoffTransferErrorKind {
208 self.kind
209 }
210
211 pub fn raw_os_error(&self) -> Option<i32> {
213 self.raw_os_error
214 }
215
216 pub fn partial_counts(&self) -> Option<(usize, usize)> {
218 self.transferred_bytes.zip(self.expected_bytes)
219 }
220
221 pub(crate) fn detail(&self) -> Option<&str> {
222 self.detail.as_deref()
223 }
224}
225
226#[cfg(feature = "ipc")]
228#[doc(hidden)]
229pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
230 platform_imp::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
231
232#[cfg(feature = "ipc")]
234#[doc(hidden)]
235pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool =
236 platform_imp::LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED;
237
238#[cfg(feature = "ipc")]
240#[doc(hidden)]
241pub fn legacy_send_fd_to(
242 socket: &std::path::Path,
243 sent_fd: i32,
244 payload: &[u8],
245) -> Result<(), LegacyHandoffError> {
246 platform_imp::legacy_send_fd_to(socket, sent_fd, payload)
247}
248
249#[cfg(feature = "ipc")]
251#[doc(hidden)]
252pub fn legacy_send_fd_over(
253 socket_fd: i32,
254 sent_fd: i32,
255 payload: &[u8],
256) -> Result<(), LegacyHandoffError> {
257 platform_imp::legacy_send_fd_over(socket_fd, sent_fd, payload)
258}
259
260#[cfg(feature = "ipc")]
262#[doc(hidden)]
263pub fn legacy_duplicate_handle(
264 source_handle: usize,
265 backend_pid: u32,
266) -> Result<usize, LegacyHandoffError> {
267 platform_imp::legacy_duplicate_handle(source_handle, backend_pid)
268}
269
270#[cfg(feature = "ipc")]
278#[doc(hidden)]
279pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
280 platform_imp::into_legacy_ipc_stream(stream)
281}
282
283#[cfg(feature = "ipc")]
285#[doc(hidden)]
286pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
287 platform_imp::from_legacy_ipc_stream(stream)
288}
289
290#[cfg(feature = "ipc")]
293#[doc(hidden)]
294pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
295 platform_imp::legacy_ipc_name(path)
296}
297
298#[cfg(feature = "ipc-async")]
299pub use platform_imp::{
300 IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
301};
302
303#[cfg(feature = "pty")]
304pub use platform_imp::terminal::{
305 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
306 input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
307 query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
308 wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
309 OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
310};
311
312#[cfg(feature = "session-relay")]
313pub use platform_imp::relay_local_socket_session;
314
315#[cfg(feature = "async-process")]
320pub fn configure_compat_tokio_command(
321 command: &mut Command,
322 show_console: bool,
323 kill_when_owner_dies: bool,
324) -> io::Result<()> {
325 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
326}
327
328#[cfg(feature = "async-process")]
330pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) -> io::Result<()> {
331 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
332}
333
334#[cfg(feature = "async-process")]
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub enum StreamMode {
338 Inherit,
340 Piped,
342 Null,
344}
345
346#[cfg(feature = "async-process")]
347impl StreamMode {
348 fn apply(self) -> Stdio {
349 match self {
350 Self::Inherit => Stdio::inherit(),
351 Self::Piped => Stdio::piped(),
352 Self::Null => Stdio::null(),
353 }
354 }
355}
356
357#[cfg(feature = "async-process")]
359#[derive(Debug, Clone)]
360pub struct SpawnSpec {
361 program: OsString,
362 args: Vec<OsString>,
363 current_dir: Option<PathBuf>,
364 env: Vec<(OsString, OsString)>,
365 clear_env: bool,
366 stdin: StreamMode,
367 stdout: StreamMode,
368 stderr: StreamMode,
369 create_process_group: bool,
370 kill_when_owner_dies: bool,
371 nice: Option<i32>,
372}
373
374#[cfg(feature = "async-process")]
375impl SpawnSpec {
376 pub fn new(program: impl Into<OsString>) -> Self {
378 Self {
379 program: program.into(),
380 args: Vec::new(),
381 current_dir: None,
382 env: Vec::new(),
383 clear_env: false,
384 stdin: StreamMode::Inherit,
385 stdout: StreamMode::Inherit,
386 stderr: StreamMode::Inherit,
387 create_process_group: false,
388 kill_when_owner_dies: false,
389 nice: None,
390 }
391 }
392
393 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
395 self.args.push(arg.into());
396 self
397 }
398
399 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
401 self.current_dir = Some(path.into());
402 self
403 }
404
405 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
407 self.env.push((key.into(), value.into()));
408 self
409 }
410
411 pub fn clear_env(mut self, clear: bool) -> Self {
413 self.clear_env = clear;
414 self
415 }
416
417 pub fn stdin(mut self, mode: StreamMode) -> Self {
419 self.stdin = mode;
420 self
421 }
422
423 pub fn stdout(mut self, mode: StreamMode) -> Self {
425 self.stdout = mode;
426 self
427 }
428
429 pub fn stderr(mut self, mode: StreamMode) -> Self {
431 self.stderr = mode;
432 self
433 }
434
435 pub fn create_process_group(mut self, create: bool) -> Self {
444 self.create_process_group = create;
445 self
446 }
447
448 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
457 self.kill_when_owner_dies = kill;
458 self
459 }
460
461 pub fn nice(mut self, nice: Option<i32>) -> Self {
468 self.nice = nice;
469 self
470 }
471
472 pub async fn spawn(self) -> io::Result<PlatformChild> {
474 let mut command = Command::new(&self.program);
475 command.args(&self.args);
476 if let Some(current_dir) = self.current_dir.as_deref() {
477 command.current_dir(current_dir);
478 }
479 if self.clear_env {
480 command.env_clear();
481 }
482 for (key, value) in &self.env {
483 command.env(key, value);
484 }
485 command
486 .stdin(self.stdin.apply())
487 .stdout(self.stdout.apply())
488 .stderr(self.stderr.apply());
489 platform_imp::configure_command(
490 &mut command,
491 self.create_process_group,
492 self.kill_when_owner_dies,
493 self.nice,
494 )?;
495
496 let child = command.spawn()?;
497 platform_imp::after_spawn(&child, self.kill_when_owner_dies)?;
498 Ok(PlatformChild::new(child, self.create_process_group))
499 }
500}
501
502#[cfg(feature = "async-process")]
504pub struct PlatformChild {
505 child: Child,
506 stdin: Option<ChildStdin>,
507 stdout: Option<ChildStdout>,
508 stderr: Option<ChildStderr>,
509 signal: PlatformEmergencySignal,
510}
511
512#[cfg(feature = "async-process")]
513impl PlatformChild {
514 fn new(mut child: Child, own_process_group: bool) -> Self {
515 let signal = PlatformEmergencySignal {
516 identity: async_child_identity(&child),
517 own_process_group,
518 legacy_group_pid: child.id(),
523 };
524 Self {
525 stdin: child.stdin.take(),
526 stdout: child.stdout.take(),
527 stderr: child.stderr.take(),
528 child,
529 signal,
530 }
531 }
532
533 pub fn id(&self) -> Option<u32> {
535 self.child.id()
536 }
537
538 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
540 self.child.wait().await
541 }
542
543 pub async fn kill(&mut self) -> io::Result<()> {
545 self.child.kill().await
546 }
547
548 pub async fn wait_with_output(self) -> io::Result<Output> {
550 let Self {
551 mut child,
552 stdin,
553 stdout,
554 stderr,
555 ..
556 } = self;
557 drop(stdin);
560 let (status, stdout, stderr) = tokio::try_join!(
561 child.wait(),
562 read_owned_to_end(stdout),
563 read_owned_to_end(stderr),
564 )?;
565 Ok(Output {
566 status,
567 stdout,
568 stderr,
569 })
570 }
571
572 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
574 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
575 stdin.write_all(bytes).await?;
576 stdin.flush().await
577 }
578
579 pub fn close_stdin(&mut self) {
584 drop(self.stdin.take());
585 }
586
587 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
589 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
590 let mut bytes = Vec::new();
591 stdout.read_to_end(&mut bytes).await?;
592 Ok(bytes)
593 }
594
595 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
597 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
598 let mut bytes = Vec::new();
599 stderr.read_to_end(&mut bytes).await?;
600 Ok(bytes)
601 }
602
603 pub fn into_actor_parts(
609 self,
610 ) -> (
611 PlatformLifecycle,
612 PlatformEmergencySignal,
613 Option<PlatformStdin>,
614 Option<PlatformOutput>,
615 Option<PlatformOutput>,
616 ) {
617 (
618 PlatformLifecycle { child: self.child },
619 self.signal,
620 self.stdin.map(|stdin| PlatformStdin { stdin }),
621 self.stdout.map(PlatformOutput::stdout),
622 self.stderr.map(PlatformOutput::stderr),
623 )
624 }
625}
626
627#[cfg(feature = "async-process")]
629pub struct PlatformLifecycle {
630 child: Child,
631}
632
633#[cfg(feature = "async-process")]
634impl PlatformLifecycle {
635 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
637 self.child.wait().await
638 }
639
640 pub fn start_kill(&mut self) -> io::Result<()> {
648 self.child.start_kill()
649 }
650}
651
652#[cfg(feature = "async-process")]
657pub struct PlatformEmergencySignal {
658 identity: Option<AsyncChildIdentity>,
659 own_process_group: bool,
660 legacy_group_pid: Option<u32>,
661}
662
663#[cfg(feature = "async-process")]
664impl PlatformEmergencySignal {
665 pub fn kill(&self) -> io::Result<()> {
667 let Some(identity) = self.identity.as_ref() else {
668 return Err(signal_target_unavailable());
669 };
670 signal_async_child(identity)
671 }
672
673 pub fn terminate_group_soft(&self) -> io::Result<bool> {
682 if !self.own_process_group {
683 return Ok(false);
684 }
685 let Some(identity) = self.identity.as_ref() else {
686 return Err(signal_target_unavailable());
687 };
688 signal_async_child_group(identity).map(|()| true)
689 }
690
691 pub fn terminate_group_soft_legacy(&self) -> io::Result<bool> {
700 if !self.own_process_group {
701 return Ok(false);
702 }
703 if let Some(identity) = self.identity.as_ref() {
704 return signal_async_child_group(identity).map(|()| true);
705 }
706 let pid = self
707 .legacy_group_pid
708 .ok_or_else(signal_target_unavailable)?;
709 crate::platform::process::soft_terminate_process_group(pid).map(|()| true)
710 }
711
712 pub fn cpu_time(&self) -> io::Result<Option<std::time::Duration>> {
716 self.identity
717 .as_ref()
718 .map_or(Ok(None), async_child_cpu_time)
719 }
720}
721
722#[cfg(feature = "async-process")]
723fn signal_target_unavailable() -> io::Error {
724 io::Error::new(
725 io::ErrorKind::BrokenPipe,
726 "child process launch identity is no longer available",
727 )
728}
729
730#[cfg(feature = "async-process")]
732pub struct PlatformStdin {
733 stdin: ChildStdin,
734}
735
736#[cfg(feature = "async-process")]
737impl PlatformStdin {
738 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
740 self.stdin.write_all(bytes).await?;
741 self.stdin.flush().await
742 }
743}
744
745#[cfg(feature = "async-process")]
747pub struct PlatformOutput {
748 reader: OutputReader,
749}
750
751#[cfg(feature = "async-process")]
752enum OutputReader {
753 Stdout(ChildStdout),
754 Stderr(ChildStderr),
755}
756
757#[cfg(feature = "async-process")]
758impl PlatformOutput {
759 fn stdout(stdout: ChildStdout) -> Self {
760 Self {
761 reader: OutputReader::Stdout(stdout),
762 }
763 }
764
765 fn stderr(stderr: ChildStderr) -> Self {
766 Self {
767 reader: OutputReader::Stderr(stderr),
768 }
769 }
770
771 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
773 match self.reader {
774 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
775 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
776 }
777 }
778
779 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
784 match &mut self.reader {
785 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
786 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
787 }
788 }
789}
790
791#[cfg(feature = "async-process")]
792fn stdin_not_piped() -> io::Error {
793 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
794}
795
796#[cfg(feature = "async-process")]
797fn stdout_not_piped() -> io::Error {
798 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
799}
800
801#[cfg(feature = "async-process")]
802fn stderr_not_piped() -> io::Error {
803 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
804}
805
806#[cfg(feature = "async-process")]
807async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
808where
809 R: AsyncRead + Unpin,
810{
811 let Some(mut reader) = reader else {
812 return Ok(Vec::new());
813 };
814 let mut bytes = Vec::new();
815 reader.read_to_end(&mut bytes).await?;
816 Ok(bytes)
817}
818
819#[cfg(feature = "async-process")]
821pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
822 platform_imp::shell_spec(command.as_ref())
823}
824
825#[cfg(all(test, feature = "async-process"))]
826mod tests {
827 use super::{shell_spec, SpawnSpec, StreamMode};
828
829 fn fixture_command() -> SpawnSpec {
830 #[cfg(windows)]
831 {
832 shell_spec("echo async-platform-internal")
833 }
834 #[cfg(not(windows))]
835 {
836 shell_spec("printf async-platform-internal")
837 }
838 }
839
840 #[tokio::test]
841 async fn blessed_spawn_captures_output_without_sync_wait() {
842 let output = fixture_command()
843 .stdout(StreamMode::Piped)
844 .stderr(StreamMode::Piped)
845 .spawn()
846 .await
847 .expect("spawn")
848 .wait_with_output()
849 .await
850 .expect("wait with output");
851
852 assert!(output.status.success());
853 let expected = if cfg!(windows) {
854 b"async-platform-internal\r\n".as_slice()
855 } else {
856 b"async-platform-internal".as_slice()
857 };
858 assert_eq!(output.stdout, expected);
859 assert!(output.stderr.is_empty());
860 }
861
862 #[tokio::test]
863 async fn blessed_spawn_reports_missing_program() {
864 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
865 .spawn()
866 .await;
867 assert!(result.is_err());
868 }
869
870 #[tokio::test]
871 async fn one_shot_output_closes_owned_stdin() {
872 #[cfg(windows)]
873 let spec = shell_spec("more > nul & echo done");
874 #[cfg(not(windows))]
875 let spec = shell_spec("cat > /dev/null; printf done");
876
877 let output = tokio::time::timeout(
878 std::time::Duration::from_secs(2),
879 spec.stdin(StreamMode::Piped)
880 .stdout(StreamMode::Piped)
881 .stderr(StreamMode::Piped)
882 .spawn()
883 .await
884 .expect("spawn")
885 .wait_with_output(),
886 )
887 .await
888 .expect("stdin is closed for one-shot output")
889 .expect("output succeeds");
890
891 let expected = if cfg!(windows) {
892 b"done\r\n".as_slice()
893 } else {
894 b"done".as_slice()
895 };
896 assert_eq!(output.stdout, expected);
897 }
898}