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