use std::cfg_select;
#[cfg(feature = "async-process")]
use std::ffi::{OsStr, OsString};
#[cfg(any(feature = "async-process", feature = "ipc"))]
use std::io;
#[cfg(feature = "async-process")]
use std::path::PathBuf;
#[cfg(feature = "async-process")]
use std::process::{ExitStatus, Output, Stdio};
#[cfg(feature = "async-process")]
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
#[cfg(feature = "async-process")]
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
pub mod platform;
#[cfg(feature = "pty")]
#[doc(hidden)]
pub use portable_pty as portable_pty_compat;
cfg_select! {
target_os = "windows" => {
mod platform_win;
pub(crate) use platform_win as platform_imp;
}
target_os = "linux" => {
mod platform_linux;
pub(crate) use platform_linux as platform_imp;
}
target_os = "macos" => {
mod platform_macos;
pub(crate) use platform_macos as platform_imp;
}
}
pub use platform_imp::{
active_graphics_probe, assign_child_to_windows_job, cancel_capture_reader,
canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
configure_process_command, configure_sync_contained_command, configure_sync_daemon_command,
configure_trampoline_command, current_executable_build_id, exact_trace_capability, exit_code,
monitor_console_windows, parent_has_console, prepare_capture_reader, set_process_name,
set_window_icon_impl, shell_command, soft_terminate_process_group, spawn_sync,
spawn_sync_daemon, start_descendant_monitor, start_exact_trace, sync_child_native_handle,
trampoline_exit_code, unix_mark_extra_fds_close_on_exec, unix_set_priority,
unix_signal_process, unix_signal_process_group, unix_signal_raw, window_icon_support_impl,
CaptureCancellation, TracedChild, WindowsJobHandle,
};
#[cfg(feature = "process-inspection")]
pub use platform_imp::{kill_tree, process_snapshot, process_snapshot_for_pid};
pub use platform_imp::{autostart_register, autostart_render_registration, autostart_unregister};
pub use platform_imp::{process_install_owner_death_cleanup, process_owner_death_cleanup_target};
pub use platform_imp::process_install_shutdown_request_handler;
pub use platform_imp::fs_write_all_to_descriptor;
pub use platform_imp::{process_can_replace_current_image, process_replace_current_image};
pub use platform_imp::{
process_executable_path, process_force_kill, process_same_executable_path,
process_signal_terminate, ProcessLiveness,
};
pub use platform_imp::{
resources_fd_exhaustion_error, resources_inode_capacity, resources_signals_fd_exhaustion,
resources_signals_storage_exhaustion, resources_storage_exhaustion_error,
};
pub use platform_imp::{
executable_file_name, executable_sibling_of_current_image, EXECUTABLE_EXTENSION,
};
#[cfg(feature = "fs")]
pub use platform_imp::{
fs_create_private_file, fs_decode_path_bytes, fs_encode_path_bytes, fs_file_identity,
fs_is_lock_conflict, fs_open_lock_file, fs_path_identity, fs_replace_file, fs_sync_directory,
fs_try_lock_exclusive, fs_unlock, fs_user_config_dir, fs_user_data_dir, fs_user_run_data_root,
fs_user_runtime_dir, fs_user_state_dir, FsFileIdentity,
};
pub use platform_imp::{
host_boot_id, host_current_process_privilege, host_environment_keys_are_case_insensitive,
host_filesystem_device_id, host_hostname, host_login_environment, host_machine_id,
host_namespace_id, host_user_machine_identity, HostPrivilegedIdentity,
};
pub use platform_imp::host_login_environment_block;
pub use platform_imp::terminal_input;
#[cfg(feature = "ipc")]
pub use platform_imp::{
ipc_broker_endpoint_name as IpcBrokerEndpointName, ipc_broker_v1_endpoint_path,
ipc_broker_v2_runtime_dir, ipc_current_user_id, ipc_endpoint_is_filesystem_backed,
ipc_endpoint_name_limit, ipc_endpoint_scope_bytes, ipc_ensure_owner_private_directory,
ipc_nonblocking_zero_read_is_pending, ipc_owner_private_directory, ipc_select_endpoint_address,
IpcEndpoint, IpcInheritedListener, IpcListener, IpcListenerNonblockingMode, IpcPeerIdentity,
IpcPeerIdentitySource, IpcStream,
};
#[cfg(feature = "ipc")]
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LegacyHandoffError {
kind: platform::ipc::HandoffTransferErrorKind,
raw_os_error: Option<i32>,
transferred_bytes: Option<usize>,
expected_bytes: Option<usize>,
detail: Option<String>,
}
#[cfg(feature = "ipc")]
impl LegacyHandoffError {
pub(crate) fn new(
kind: platform::ipc::HandoffTransferErrorKind,
raw_os_error: Option<i32>,
) -> Self {
Self {
kind,
raw_os_error,
transferred_bytes: None,
expected_bytes: None,
detail: None,
}
}
pub(crate) fn with_detail(
kind: platform::ipc::HandoffTransferErrorKind,
raw_os_error: Option<i32>,
detail: impl Into<String>,
) -> Self {
Self {
kind,
raw_os_error,
transferred_bytes: None,
expected_bytes: None,
detail: Some(detail.into()),
}
}
#[doc(hidden)]
pub fn partial(transferred_bytes: usize, expected_bytes: usize) -> Self {
Self {
kind: platform::ipc::HandoffTransferErrorKind::Failed,
raw_os_error: None,
transferred_bytes: Some(transferred_bytes),
expected_bytes: Some(expected_bytes),
detail: Some(format!(
"SCM_RIGHTS connection transfer was partial ({transferred_bytes}/{expected_bytes} bytes)"
)),
}
}
pub fn kind(&self) -> platform::ipc::HandoffTransferErrorKind {
self.kind
}
pub fn raw_os_error(&self) -> Option<i32> {
self.raw_os_error
}
pub fn partial_counts(&self) -> Option<(usize, usize)> {
self.transferred_bytes.zip(self.expected_bytes)
}
pub(crate) fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
platform_imp::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool =
platform_imp::LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED;
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn legacy_send_fd_to(
socket: &std::path::Path,
sent_fd: i32,
payload: &[u8],
) -> Result<(), LegacyHandoffError> {
platform_imp::legacy_send_fd_to(socket, sent_fd, payload)
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn legacy_send_fd_over(
socket_fd: i32,
sent_fd: i32,
payload: &[u8],
) -> Result<(), LegacyHandoffError> {
platform_imp::legacy_send_fd_over(socket_fd, sent_fd, payload)
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn legacy_duplicate_handle(
source_handle: usize,
backend_pid: u32,
) -> Result<usize, LegacyHandoffError> {
platform_imp::legacy_duplicate_handle(source_handle, backend_pid)
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
platform_imp::into_legacy_ipc_stream(stream)
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
platform_imp::from_legacy_ipc_stream(stream)
}
#[cfg(feature = "ipc")]
#[doc(hidden)]
pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
platform_imp::legacy_ipc_name(path)
}
#[cfg(feature = "ipc-async")]
pub use platform_imp::{
IpcAsyncListener, IpcAsyncStream, IpcIntoAsyncListener, IpcIntoAsyncStream,
};
#[cfg(feature = "pty")]
pub use platform_imp::terminal::{
before_pty_spawn, current_backend_kind, find_child_processes, find_orphan_conhosts,
input_payload, is_ignorable_process_control_error, prepare_unmanaged_pty_child,
query_responses, resize_pty, shell_argv, signal_pty_tree, terminate_pty_child,
wait_before_pty_close_supported, Backend, ChildProcessInfo, ConPtyBackendKind,
OrphanConhostInfo, PtyProcessGuard, PtySpawnContext, TerminalInputSession,
};
#[cfg(feature = "session-relay")]
pub use platform_imp::relay_local_socket_session;
#[cfg(feature = "async-process")]
pub fn configure_compat_tokio_command(
command: &mut Command,
show_console: bool,
kill_when_owner_dies: bool,
) -> io::Result<()> {
platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
}
#[cfg(feature = "async-process")]
pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) -> io::Result<()> {
platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
}
#[cfg(feature = "async-process")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamMode {
Inherit,
Piped,
Null,
}
#[cfg(feature = "async-process")]
impl StreamMode {
fn apply(self) -> Stdio {
match self {
Self::Inherit => Stdio::inherit(),
Self::Piped => Stdio::piped(),
Self::Null => Stdio::null(),
}
}
}
#[cfg(feature = "async-process")]
#[derive(Debug, Clone)]
pub struct SpawnSpec {
program: OsString,
args: Vec<OsString>,
current_dir: Option<PathBuf>,
env: Vec<(OsString, OsString)>,
clear_env: bool,
stdin: StreamMode,
stdout: StreamMode,
stderr: StreamMode,
create_process_group: bool,
kill_when_owner_dies: bool,
}
#[cfg(feature = "async-process")]
impl SpawnSpec {
pub fn new(program: impl Into<OsString>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
current_dir: None,
env: Vec::new(),
clear_env: false,
stdin: StreamMode::Inherit,
stdout: StreamMode::Inherit,
stderr: StreamMode::Inherit,
create_process_group: false,
kill_when_owner_dies: false,
}
}
pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
self.args.push(arg.into());
self
}
pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.current_dir = Some(path.into());
self
}
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn clear_env(mut self, clear: bool) -> Self {
self.clear_env = clear;
self
}
pub fn stdin(mut self, mode: StreamMode) -> Self {
self.stdin = mode;
self
}
pub fn stdout(mut self, mode: StreamMode) -> Self {
self.stdout = mode;
self
}
pub fn stderr(mut self, mode: StreamMode) -> Self {
self.stderr = mode;
self
}
pub fn create_process_group(mut self, create: bool) -> Self {
self.create_process_group = create;
self
}
pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
self.kill_when_owner_dies = kill;
self
}
pub async fn spawn(self) -> io::Result<PlatformChild> {
let mut command = Command::new(&self.program);
command.args(&self.args);
if let Some(current_dir) = self.current_dir.as_deref() {
command.current_dir(current_dir);
}
if self.clear_env {
command.env_clear();
}
for (key, value) in &self.env {
command.env(key, value);
}
command
.stdin(self.stdin.apply())
.stdout(self.stdout.apply())
.stderr(self.stderr.apply());
platform_imp::configure_command(
&mut command,
self.create_process_group,
self.kill_when_owner_dies,
)?;
let child = command.spawn()?;
platform_imp::after_spawn(&child, self.kill_when_owner_dies)?;
Ok(PlatformChild::new(child, self.create_process_group))
}
}
#[cfg(feature = "async-process")]
pub struct PlatformChild {
child: Child,
stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>,
stderr: Option<ChildStderr>,
signal: PlatformEmergencySignal,
}
#[cfg(feature = "async-process")]
impl PlatformChild {
fn new(mut child: Child, own_process_group: bool) -> Self {
let signal = PlatformEmergencySignal {
pid: child.id(),
own_process_group,
};
Self {
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
child,
signal,
}
}
pub fn id(&self) -> Option<u32> {
self.child.id()
}
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait().await
}
pub async fn kill(&mut self) -> io::Result<()> {
self.child.kill().await
}
pub async fn wait_with_output(self) -> io::Result<Output> {
let Self {
mut child,
stdin,
stdout,
stderr,
..
} = self;
drop(stdin);
let (status, stdout, stderr) = tokio::try_join!(
child.wait(),
read_owned_to_end(stdout),
read_owned_to_end(stderr),
)?;
Ok(Output {
status,
stdout,
stderr,
})
}
pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
stdin.write_all(bytes).await?;
stdin.flush().await
}
pub fn close_stdin(&mut self) {
drop(self.stdin.take());
}
pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
let mut bytes = Vec::new();
stdout.read_to_end(&mut bytes).await?;
Ok(bytes)
}
pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
let mut bytes = Vec::new();
stderr.read_to_end(&mut bytes).await?;
Ok(bytes)
}
pub fn into_actor_parts(
self,
) -> (
PlatformLifecycle,
PlatformEmergencySignal,
Option<PlatformStdin>,
Option<PlatformOutput>,
Option<PlatformOutput>,
) {
(
PlatformLifecycle { child: self.child },
self.signal,
self.stdin.map(|stdin| PlatformStdin { stdin }),
self.stdout.map(PlatformOutput::stdout),
self.stderr.map(PlatformOutput::stderr),
)
}
}
#[cfg(feature = "async-process")]
pub struct PlatformLifecycle {
child: Child,
}
#[cfg(feature = "async-process")]
impl PlatformLifecycle {
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait().await
}
}
#[cfg(feature = "async-process")]
pub struct PlatformEmergencySignal {
pid: Option<u32>,
own_process_group: bool,
}
#[cfg(feature = "async-process")]
impl PlatformEmergencySignal {
pub fn kill(&self) -> io::Result<()> {
platform_imp::signal_process(self.target()?)
}
pub fn terminate_group_soft(&self) -> io::Result<bool> {
if !self.own_process_group {
return Ok(false);
}
platform_imp::signal_process_group(self.target()?).map(|()| true)
}
fn target(&self) -> io::Result<u32> {
self.pid.ok_or_else(|| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"child process no longer has an emergency signal target",
)
})
}
}
#[cfg(feature = "async-process")]
pub struct PlatformStdin {
stdin: ChildStdin,
}
#[cfg(feature = "async-process")]
impl PlatformStdin {
pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
self.stdin.write_all(bytes).await?;
self.stdin.flush().await
}
}
#[cfg(feature = "async-process")]
pub struct PlatformOutput {
reader: OutputReader,
}
#[cfg(feature = "async-process")]
enum OutputReader {
Stdout(ChildStdout),
Stderr(ChildStderr),
}
#[cfg(feature = "async-process")]
impl PlatformOutput {
fn stdout(stdout: ChildStdout) -> Self {
Self {
reader: OutputReader::Stdout(stdout),
}
}
fn stderr(stderr: ChildStderr) -> Self {
Self {
reader: OutputReader::Stderr(stderr),
}
}
pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
match self.reader {
OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
}
}
pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
match &mut self.reader {
OutputReader::Stdout(stdout) => stdout.read(buffer).await,
OutputReader::Stderr(stderr) => stderr.read(buffer).await,
}
}
}
#[cfg(feature = "async-process")]
fn stdin_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
}
#[cfg(feature = "async-process")]
fn stdout_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
}
#[cfg(feature = "async-process")]
fn stderr_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
}
#[cfg(feature = "async-process")]
async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
where
R: AsyncRead + Unpin,
{
let Some(mut reader) = reader else {
return Ok(Vec::new());
};
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
Ok(bytes)
}
#[cfg(feature = "async-process")]
pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
platform_imp::shell_spec(command.as_ref())
}
#[cfg(all(test, feature = "async-process"))]
mod tests {
use super::{shell_spec, SpawnSpec, StreamMode};
fn fixture_command() -> SpawnSpec {
#[cfg(windows)]
{
shell_spec("echo async-platform-internal")
}
#[cfg(not(windows))]
{
shell_spec("printf async-platform-internal")
}
}
#[tokio::test]
async fn blessed_spawn_captures_output_without_sync_wait() {
let output = fixture_command()
.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn")
.wait_with_output()
.await
.expect("wait with output");
assert!(output.status.success());
let expected = if cfg!(windows) {
b"async-platform-internal\r\n".as_slice()
} else {
b"async-platform-internal".as_slice()
};
assert_eq!(output.stdout, expected);
assert!(output.stderr.is_empty());
}
#[tokio::test]
async fn blessed_spawn_reports_missing_program() {
let result = SpawnSpec::new("running-process-program-that-does-not-exist")
.spawn()
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn one_shot_output_closes_owned_stdin() {
#[cfg(windows)]
let spec = shell_spec("more > nul & echo done");
#[cfg(not(windows))]
let spec = shell_spec("cat > /dev/null; printf done");
let output = tokio::time::timeout(
std::time::Duration::from_secs(2),
spec.stdin(StreamMode::Piped)
.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn")
.wait_with_output(),
)
.await
.expect("stdin is closed for one-shot output")
.expect("output succeeds");
let expected = if cfg!(windows) {
b"done\r\n".as_slice()
} else {
b"done".as_slice()
};
assert_eq!(output.stdout, expected);
}
}