1use std::cfg_select;
9#[cfg(feature = "async-process")]
10use std::ffi::{OsStr, OsString};
11#[cfg(any(feature = "async-process", feature = "ipc"))]
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 active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
60 canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
61 configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
62 configure_sync_daemon_command_with_inheritance, configure_trampoline_command,
63 current_executable_build_id, exact_trace_capability, exit_code, monitor_console_windows,
64 parent_has_console, prepare_capture_reader, set_process_name, set_window_icon_impl,
65 shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
66 spawn_sync_daemon_with_inheritance, start_descendant_monitor, start_exact_trace,
67 sync_child_native_handle, trampoline_exit_code, unix_mark_extra_fds_close_on_exec,
68 unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
69 window_icon_support_impl, CaptureCancellation, TracedChild, WindowsJobHandle,
70};
71
72#[cfg(feature = "process-inspection")]
73pub use platform_imp::{kill_tree, process_snapshot, process_snapshot_for_pid};
74
75pub use platform_imp::{autostart_register, autostart_render_registration, autostart_unregister};
76
77pub use platform_imp::{process_install_owner_death_cleanup, process_owner_death_cleanup_target};
78
79pub use platform_imp::process_install_shutdown_request_handler;
80
81pub use platform_imp::fs_write_all_to_descriptor;
82
83pub use platform_imp::{process_can_replace_current_image, process_replace_current_image};
84
85pub use platform_imp::{
86 process_executable_path, process_force_kill, process_same_executable_path,
87 process_signal_terminate, ProcessLiveness,
88};
89
90pub use platform_imp::{
91 resources_fd_exhaustion_error, resources_inode_capacity, resources_signals_fd_exhaustion,
92 resources_signals_storage_exhaustion, resources_storage_exhaustion_error,
93};
94
95pub use platform_imp::{
96 executable_file_name, executable_sibling_of_current_image, EXECUTABLE_EXTENSION,
97};
98
99#[cfg(feature = "fs")]
100pub use platform_imp::{
101 fs_create_private_file, fs_decode_path_bytes, fs_encode_path_bytes, fs_file_identity,
102 fs_is_lock_conflict, fs_open_lock_file, fs_path_identity, fs_replace_file, fs_sync_directory,
103 fs_try_lock_exclusive, fs_unlock, fs_user_config_dir, fs_user_data_dir, fs_user_run_data_root,
104 fs_user_runtime_dir, fs_user_state_dir, FsFileIdentity,
105};
106
107pub use platform_imp::{
108 host_boot_id, host_current_process_privilege, host_environment_keys_are_case_insensitive,
109 host_filesystem_device_id, host_hostname, host_login_environment, host_machine_id,
110 host_namespace_id, host_user_machine_identity, HostPrivilegedIdentity,
111};
112
113pub use platform_imp::host_login_environment_block;
114
115pub use platform_imp::terminal_input;
116
117#[cfg(feature = "ipc")]
118pub use platform_imp::{
119 ipc_broker_endpoint_name as IpcBrokerEndpointName, ipc_broker_v1_endpoint_path,
120 ipc_broker_v2_runtime_dir, ipc_current_user_id, ipc_endpoint_is_filesystem_backed,
121 ipc_endpoint_name_limit, ipc_endpoint_scope_bytes, ipc_ensure_owner_private_directory,
122 ipc_nonblocking_zero_read_is_pending, ipc_owner_private_directory, ipc_select_endpoint_address,
123 IpcEndpoint, IpcInheritedListener, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
124 IpcPeerIdentitySource, IpcStream,
125};
126
127#[cfg(feature = "ipc")]
132#[doc(hidden)]
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct LegacyHandoffError {
135 kind: platform::ipc::HandoffTransferErrorKind,
136 raw_os_error: Option<i32>,
137 transferred_bytes: Option<usize>,
138 expected_bytes: Option<usize>,
139 detail: Option<String>,
140}
141
142#[cfg(feature = "ipc")]
143impl LegacyHandoffError {
144 pub(crate) fn new(
145 kind: platform::ipc::HandoffTransferErrorKind,
146 raw_os_error: Option<i32>,
147 ) -> Self {
148 Self {
149 kind,
150 raw_os_error,
151 transferred_bytes: None,
152 expected_bytes: None,
153 detail: None,
154 }
155 }
156
157 pub(crate) fn with_detail(
158 kind: platform::ipc::HandoffTransferErrorKind,
159 raw_os_error: Option<i32>,
160 detail: impl Into<String>,
161 ) -> Self {
162 Self {
163 kind,
164 raw_os_error,
165 transferred_bytes: None,
166 expected_bytes: None,
167 detail: Some(detail.into()),
168 }
169 }
170
171 #[doc(hidden)]
172 pub fn partial(transferred_bytes: usize, expected_bytes: usize) -> Self {
173 Self {
174 kind: platform::ipc::HandoffTransferErrorKind::Failed,
175 raw_os_error: None,
176 transferred_bytes: Some(transferred_bytes),
177 expected_bytes: Some(expected_bytes),
178 detail: Some(format!(
179 "SCM_RIGHTS connection transfer was partial ({transferred_bytes}/{expected_bytes} bytes)"
180 )),
181 }
182 }
183
184 pub fn kind(&self) -> platform::ipc::HandoffTransferErrorKind {
186 self.kind
187 }
188
189 pub fn raw_os_error(&self) -> Option<i32> {
191 self.raw_os_error
192 }
193
194 pub fn partial_counts(&self) -> Option<(usize, usize)> {
196 self.transferred_bytes.zip(self.expected_bytes)
197 }
198
199 pub(crate) fn detail(&self) -> Option<&str> {
200 self.detail.as_deref()
201 }
202}
203
204#[cfg(feature = "ipc")]
206#[doc(hidden)]
207pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
208 platform_imp::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
209
210#[cfg(feature = "ipc")]
212#[doc(hidden)]
213pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool =
214 platform_imp::LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED;
215
216#[cfg(feature = "ipc")]
218#[doc(hidden)]
219pub fn legacy_send_fd_to(
220 socket: &std::path::Path,
221 sent_fd: i32,
222 payload: &[u8],
223) -> Result<(), LegacyHandoffError> {
224 platform_imp::legacy_send_fd_to(socket, sent_fd, payload)
225}
226
227#[cfg(feature = "ipc")]
229#[doc(hidden)]
230pub fn legacy_send_fd_over(
231 socket_fd: i32,
232 sent_fd: i32,
233 payload: &[u8],
234) -> Result<(), LegacyHandoffError> {
235 platform_imp::legacy_send_fd_over(socket_fd, sent_fd, payload)
236}
237
238#[cfg(feature = "ipc")]
240#[doc(hidden)]
241pub fn legacy_duplicate_handle(
242 source_handle: usize,
243 backend_pid: u32,
244) -> Result<usize, LegacyHandoffError> {
245 platform_imp::legacy_duplicate_handle(source_handle, backend_pid)
246}
247
248#[cfg(feature = "ipc")]
256#[doc(hidden)]
257pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
258 platform_imp::into_legacy_ipc_stream(stream)
259}
260
261#[cfg(feature = "ipc")]
263#[doc(hidden)]
264pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
265 platform_imp::from_legacy_ipc_stream(stream)
266}
267
268#[cfg(feature = "ipc")]
271#[doc(hidden)]
272pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
273 platform_imp::legacy_ipc_name(path)
274}
275
276#[cfg(feature = "ipc-async")]
277pub use platform_imp::{
278 IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
279};
280
281#[cfg(feature = "pty")]
282pub use platform_imp::terminal::{
283 before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
284 input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
285 query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
286 wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
287 OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
288};
289
290#[cfg(feature = "session-relay")]
291pub use platform_imp::relay_local_socket_session;
292
293#[cfg(feature = "async-process")]
298pub fn configure_compat_tokio_command(
299 command: &mut Command,
300 show_console: bool,
301 kill_when_owner_dies: bool,
302) -> io::Result<()> {
303 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
304}
305
306#[cfg(feature = "async-process")]
308pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) -> io::Result<()> {
309 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
310}
311
312#[cfg(feature = "async-process")]
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum StreamMode {
316 Inherit,
318 Piped,
320 Null,
322}
323
324#[cfg(feature = "async-process")]
325impl StreamMode {
326 fn apply(self) -> Stdio {
327 match self {
328 Self::Inherit => Stdio::inherit(),
329 Self::Piped => Stdio::piped(),
330 Self::Null => Stdio::null(),
331 }
332 }
333}
334
335#[cfg(feature = "async-process")]
337#[derive(Debug, Clone)]
338pub struct SpawnSpec {
339 program: OsString,
340 args: Vec<OsString>,
341 current_dir: Option<PathBuf>,
342 env: Vec<(OsString, OsString)>,
343 clear_env: bool,
344 stdin: StreamMode,
345 stdout: StreamMode,
346 stderr: StreamMode,
347 create_process_group: bool,
348 kill_when_owner_dies: bool,
349}
350
351#[cfg(feature = "async-process")]
352impl SpawnSpec {
353 pub fn new(program: impl Into<OsString>) -> Self {
355 Self {
356 program: program.into(),
357 args: Vec::new(),
358 current_dir: None,
359 env: Vec::new(),
360 clear_env: false,
361 stdin: StreamMode::Inherit,
362 stdout: StreamMode::Inherit,
363 stderr: StreamMode::Inherit,
364 create_process_group: false,
365 kill_when_owner_dies: false,
366 }
367 }
368
369 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
371 self.args.push(arg.into());
372 self
373 }
374
375 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
377 self.current_dir = Some(path.into());
378 self
379 }
380
381 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
383 self.env.push((key.into(), value.into()));
384 self
385 }
386
387 pub fn clear_env(mut self, clear: bool) -> Self {
389 self.clear_env = clear;
390 self
391 }
392
393 pub fn stdin(mut self, mode: StreamMode) -> Self {
395 self.stdin = mode;
396 self
397 }
398
399 pub fn stdout(mut self, mode: StreamMode) -> Self {
401 self.stdout = mode;
402 self
403 }
404
405 pub fn stderr(mut self, mode: StreamMode) -> Self {
407 self.stderr = mode;
408 self
409 }
410
411 pub fn create_process_group(mut self, create: bool) -> Self {
420 self.create_process_group = create;
421 self
422 }
423
424 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
431 self.kill_when_owner_dies = kill;
432 self
433 }
434
435 pub async fn spawn(self) -> io::Result<PlatformChild> {
437 let mut command = Command::new(&self.program);
438 command.args(&self.args);
439 if let Some(current_dir) = self.current_dir.as_deref() {
440 command.current_dir(current_dir);
441 }
442 if self.clear_env {
443 command.env_clear();
444 }
445 for (key, value) in &self.env {
446 command.env(key, value);
447 }
448 command
449 .stdin(self.stdin.apply())
450 .stdout(self.stdout.apply())
451 .stderr(self.stderr.apply());
452 platform_imp::configure_command(
453 &mut command,
454 self.create_process_group,
455 self.kill_when_owner_dies,
456 )?;
457
458 let child = command.spawn()?;
459 platform_imp::after_spawn(&child, self.kill_when_owner_dies)?;
460 Ok(PlatformChild::new(child, self.create_process_group))
461 }
462}
463
464#[cfg(feature = "async-process")]
466pub struct PlatformChild {
467 child: Child,
468 stdin: Option<ChildStdin>,
469 stdout: Option<ChildStdout>,
470 stderr: Option<ChildStderr>,
471 signal: PlatformEmergencySignal,
472}
473
474#[cfg(feature = "async-process")]
475impl PlatformChild {
476 fn new(mut child: Child, own_process_group: bool) -> Self {
477 let signal = PlatformEmergencySignal {
478 pid: child.id(),
479 own_process_group,
480 };
481 Self {
482 stdin: child.stdin.take(),
483 stdout: child.stdout.take(),
484 stderr: child.stderr.take(),
485 child,
486 signal,
487 }
488 }
489
490 pub fn id(&self) -> Option<u32> {
492 self.child.id()
493 }
494
495 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
497 self.child.wait().await
498 }
499
500 pub async fn kill(&mut self) -> io::Result<()> {
502 self.child.kill().await
503 }
504
505 pub async fn wait_with_output(self) -> io::Result<Output> {
507 let Self {
508 mut child,
509 stdin,
510 stdout,
511 stderr,
512 ..
513 } = self;
514 drop(stdin);
517 let (status, stdout, stderr) = tokio::try_join!(
518 child.wait(),
519 read_owned_to_end(stdout),
520 read_owned_to_end(stderr),
521 )?;
522 Ok(Output {
523 status,
524 stdout,
525 stderr,
526 })
527 }
528
529 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
531 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
532 stdin.write_all(bytes).await?;
533 stdin.flush().await
534 }
535
536 pub fn close_stdin(&mut self) {
541 drop(self.stdin.take());
542 }
543
544 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
546 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
547 let mut bytes = Vec::new();
548 stdout.read_to_end(&mut bytes).await?;
549 Ok(bytes)
550 }
551
552 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
554 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
555 let mut bytes = Vec::new();
556 stderr.read_to_end(&mut bytes).await?;
557 Ok(bytes)
558 }
559
560 pub fn into_actor_parts(
566 self,
567 ) -> (
568 PlatformLifecycle,
569 PlatformEmergencySignal,
570 Option<PlatformStdin>,
571 Option<PlatformOutput>,
572 Option<PlatformOutput>,
573 ) {
574 (
575 PlatformLifecycle { child: self.child },
576 self.signal,
577 self.stdin.map(|stdin| PlatformStdin { stdin }),
578 self.stdout.map(PlatformOutput::stdout),
579 self.stderr.map(PlatformOutput::stderr),
580 )
581 }
582}
583
584#[cfg(feature = "async-process")]
586pub struct PlatformLifecycle {
587 child: Child,
588}
589
590#[cfg(feature = "async-process")]
591impl PlatformLifecycle {
592 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
594 self.child.wait().await
595 }
596}
597
598#[cfg(feature = "async-process")]
603pub struct PlatformEmergencySignal {
604 pid: Option<u32>,
605 own_process_group: bool,
606}
607
608#[cfg(feature = "async-process")]
609impl PlatformEmergencySignal {
610 pub fn kill(&self) -> io::Result<()> {
612 platform_imp::signal_process(self.target()?)
613 }
614
615 pub fn terminate_group_soft(&self) -> io::Result<bool> {
624 if !self.own_process_group {
625 return Ok(false);
626 }
627 platform_imp::signal_process_group(self.target()?).map(|()| true)
628 }
629
630 fn target(&self) -> io::Result<u32> {
631 self.pid.ok_or_else(|| {
632 io::Error::new(
633 io::ErrorKind::BrokenPipe,
634 "child process no longer has an emergency signal target",
635 )
636 })
637 }
638}
639
640#[cfg(feature = "async-process")]
642pub struct PlatformStdin {
643 stdin: ChildStdin,
644}
645
646#[cfg(feature = "async-process")]
647impl PlatformStdin {
648 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
650 self.stdin.write_all(bytes).await?;
651 self.stdin.flush().await
652 }
653}
654
655#[cfg(feature = "async-process")]
657pub struct PlatformOutput {
658 reader: OutputReader,
659}
660
661#[cfg(feature = "async-process")]
662enum OutputReader {
663 Stdout(ChildStdout),
664 Stderr(ChildStderr),
665}
666
667#[cfg(feature = "async-process")]
668impl PlatformOutput {
669 fn stdout(stdout: ChildStdout) -> Self {
670 Self {
671 reader: OutputReader::Stdout(stdout),
672 }
673 }
674
675 fn stderr(stderr: ChildStderr) -> Self {
676 Self {
677 reader: OutputReader::Stderr(stderr),
678 }
679 }
680
681 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
683 match self.reader {
684 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
685 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
686 }
687 }
688
689 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
694 match &mut self.reader {
695 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
696 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
697 }
698 }
699}
700
701#[cfg(feature = "async-process")]
702fn stdin_not_piped() -> io::Error {
703 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
704}
705
706#[cfg(feature = "async-process")]
707fn stdout_not_piped() -> io::Error {
708 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
709}
710
711#[cfg(feature = "async-process")]
712fn stderr_not_piped() -> io::Error {
713 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
714}
715
716#[cfg(feature = "async-process")]
717async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
718where
719 R: AsyncRead + Unpin,
720{
721 let Some(mut reader) = reader else {
722 return Ok(Vec::new());
723 };
724 let mut bytes = Vec::new();
725 reader.read_to_end(&mut bytes).await?;
726 Ok(bytes)
727}
728
729#[cfg(feature = "async-process")]
731pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
732 platform_imp::shell_spec(command.as_ref())
733}
734
735#[cfg(all(test, feature = "async-process"))]
736mod tests {
737 use super::{shell_spec, SpawnSpec, StreamMode};
738
739 fn fixture_command() -> SpawnSpec {
740 #[cfg(windows)]
741 {
742 shell_spec("echo async-platform-internal")
743 }
744 #[cfg(not(windows))]
745 {
746 shell_spec("printf async-platform-internal")
747 }
748 }
749
750 #[tokio::test]
751 async fn blessed_spawn_captures_output_without_sync_wait() {
752 let output = fixture_command()
753 .stdout(StreamMode::Piped)
754 .stderr(StreamMode::Piped)
755 .spawn()
756 .await
757 .expect("spawn")
758 .wait_with_output()
759 .await
760 .expect("wait with output");
761
762 assert!(output.status.success());
763 let expected = if cfg!(windows) {
764 b"async-platform-internal\r\n".as_slice()
765 } else {
766 b"async-platform-internal".as_slice()
767 };
768 assert_eq!(output.stdout, expected);
769 assert!(output.stderr.is_empty());
770 }
771
772 #[tokio::test]
773 async fn blessed_spawn_reports_missing_program() {
774 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
775 .spawn()
776 .await;
777 assert!(result.is_err());
778 }
779
780 #[tokio::test]
781 async fn one_shot_output_closes_owned_stdin() {
782 #[cfg(windows)]
783 let spec = shell_spec("more > nul & echo done");
784 #[cfg(not(windows))]
785 let spec = shell_spec("cat > /dev/null; printf done");
786
787 let output = tokio::time::timeout(
788 std::time::Duration::from_secs(2),
789 spec.stdin(StreamMode::Piped)
790 .stdout(StreamMode::Piped)
791 .stderr(StreamMode::Piped)
792 .spawn()
793 .await
794 .expect("spawn")
795 .wait_with_output(),
796 )
797 .await
798 .expect("stdin is closed for one-shot output")
799 .expect("output succeeds");
800
801 let expected = if cfg!(windows) {
802 b"done\r\n".as_slice()
803 } else {
804 b"done".as_slice()
805 };
806 assert_eq!(output.stdout, expected);
807 }
808}