use std::process::Command;
pub use running_process_platform_internal::platform::process::{
DaemonChild, DaemonStdio, DaemonStdioSource, SpawnStdio, SpawnedChild, SpawnedChildControl,
StdioSource, SyncEnvironment,
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum EnvironmentPolicy {
#[default]
Auto,
Inherit,
UserBaseline,
Clear,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SpawnLifetime {
Contained,
Daemon,
}
impl EnvironmentPolicy {
pub(crate) fn resolve(self, lifetime: SpawnLifetime) -> Self {
match (self, lifetime) {
(Self::Auto, SpawnLifetime::Contained) => Self::Inherit,
(Self::Auto, SpawnLifetime::Daemon) => Self::UserBaseline,
(explicit, _) => explicit,
}
}
#[cfg(any(feature = "daemon", feature = "client-async", test))]
pub(crate) fn from_wire(value: i32, legacy_clear: bool) -> Result<Self, &'static str> {
match value {
0 => Ok(if legacy_clear {
Self::Clear
} else {
Self::Inherit
}),
1 => Ok(Self::Inherit),
2 => Ok(Self::UserBaseline),
3 => Ok(Self::Clear),
_ => Err("unknown environment policy"),
}
}
#[cfg(any(feature = "client", test))]
pub(crate) fn wire_value(self) -> Result<i32, &'static str> {
match self {
Self::Inherit => Ok(1),
Self::UserBaseline => Ok(2),
Self::Clear => Ok(3),
Self::Auto => Err("Auto environment policy must be resolved before serialization"),
}
}
#[cfg(any(feature = "client", test))]
pub(crate) fn legacy_clear_fallback(self) -> Result<bool, &'static str> {
match self {
Self::Inherit => Ok(false),
Self::UserBaseline | Self::Clear => Ok(true),
Self::Auto => Err("Auto environment policy must be resolved before serialization"),
}
}
}
#[cfg(feature = "client-async")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TokioSpawnOptions {
pub kill_on_drop: bool,
pub show_console: bool,
pub kill_when_owner_dies: bool,
}
#[cfg(feature = "client-async")]
impl Default for TokioSpawnOptions {
fn default() -> Self {
Self {
kill_on_drop: true,
show_console: false,
kill_when_owner_dies: false,
}
}
}
pub const DAEMON_MARKER_ENV_VAR: &str = "RUNNING_PROCESS_IS_DAEMON";
pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(
command,
DaemonStdio::default(),
EnvironmentPolicy::Auto,
false,
None,
)
}
pub fn spawn_daemon_with_stdio(
command: &mut Command,
stdio: DaemonStdio<'_>,
) -> std::io::Result<DaemonChild> {
spawn_daemon_with_stdio_and_env_policy(command, stdio, EnvironmentPolicy::Auto)
}
pub fn spawn_daemon_with_stdio_and_env_policy(
command: &mut Command,
stdio: DaemonStdio<'_>,
policy: EnvironmentPolicy,
) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(command, stdio, policy, false, None)
}
pub fn spawn_daemon_with_clear_env(
command: &mut Command,
clear_env: bool,
) -> std::io::Result<DaemonChild> {
let policy = if clear_env {
EnvironmentPolicy::Clear
} else {
EnvironmentPolicy::Auto
};
spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
}
pub fn spawn_daemon_with_env_policy(
command: &mut Command,
policy: EnvironmentPolicy,
) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
}
pub fn spawn_daemon_with_explicit_environment(
command: &mut Command,
stdio: DaemonStdio<'_>,
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
breakaway: bool,
) -> std::io::Result<DaemonChild> {
spawn_daemon_with_environment(
command,
stdio,
SyncEnvironment::Explicit(environment),
breakaway,
)
}
pub fn spawn_daemon_with_environment(
command: &mut Command,
stdio: DaemonStdio<'_>,
environment: SyncEnvironment,
breakaway: bool,
) -> std::io::Result<DaemonChild> {
mark_as_daemon(command);
running_process_platform_internal::spawn_sync_daemon(command, stdio, environment, breakaway)
}
pub fn spawn_daemon_breaking_away_from_job(command: &mut Command) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(
command,
DaemonStdio::default(),
EnvironmentPolicy::Auto,
true,
None,
)
}
pub fn spawn_daemon_breaking_away_with_env_policy(
command: &mut Command,
policy: EnvironmentPolicy,
) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(command, DaemonStdio::default(), policy, true, None)
}
#[cfg(feature = "client")]
pub(crate) fn spawn_daemon_with_inheritance(
command: &mut Command,
inheritance: running_process_platform_internal::platform::process::DaemonExecInheritance,
) -> std::io::Result<DaemonChild> {
spawn_daemon_inner(
command,
DaemonStdio::default(),
EnvironmentPolicy::Auto,
false,
Some(inheritance),
)
}
pub(crate) fn mark_as_daemon(command: &mut Command) {
command.env(DAEMON_MARKER_ENV_VAR, "1");
}
fn prepare_sync_environment(
policy: EnvironmentPolicy,
) -> std::io::Result<running_process_platform_internal::platform::process::SyncEnvironment> {
use running_process_platform_internal::platform::process::SyncEnvironment;
if policy == EnvironmentPolicy::Inherit {
return Ok(SyncEnvironment::Inherit);
}
if policy == EnvironmentPolicy::Auto {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Auto environment policy must be resolved before platform spawn",
));
}
let baseline = match policy {
EnvironmentPolicy::UserBaseline => crate::environment::user_baseline_environment()?,
EnvironmentPolicy::Clear => Vec::new(),
EnvironmentPolicy::Auto | EnvironmentPolicy::Inherit => unreachable!(),
};
Ok(SyncEnvironment::Explicit(baseline))
}
fn spawn_daemon_inner(
command: &mut Command,
stdio: DaemonStdio<'_>,
policy: EnvironmentPolicy,
breakaway: bool,
inheritance: Option<
running_process_platform_internal::platform::process::DaemonExecInheritance,
>,
) -> std::io::Result<DaemonChild> {
mark_as_daemon(command);
let policy = policy.resolve(SpawnLifetime::Daemon);
let environment = prepare_sync_environment(policy)?;
match inheritance {
Some(inheritance) => {
running_process_platform_internal::platform::process::spawn_sync_daemon_with_inheritance(
command,
stdio,
environment,
breakaway,
inheritance,
)
}
None => running_process_platform_internal::platform::process::spawn_sync_daemon(
command,
stdio,
environment,
breakaway,
),
}
}
pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
spawn_with_env_policy(command, stdio, EnvironmentPolicy::Auto)
}
pub fn spawn_with_env_policy(
command: &mut Command,
stdio: SpawnStdio<'_>,
policy: EnvironmentPolicy,
) -> std::io::Result<SpawnedChild> {
let policy = policy.resolve(SpawnLifetime::Contained);
let environment = prepare_sync_environment(policy)?;
running_process_platform_internal::platform::process::spawn_sync(command, stdio, environment)
}
pub fn spawn_with_explicit_environment(
command: &mut Command,
stdio: SpawnStdio<'_>,
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
shutdown_timeout: Option<fn() -> std::time::Duration>,
) -> std::io::Result<SpawnedChild> {
spawn_with_environment(
command,
stdio,
SyncEnvironment::Explicit(environment),
shutdown_timeout,
)
}
pub fn spawn_with_environment(
command: &mut Command,
stdio: SpawnStdio<'_>,
environment: SyncEnvironment,
shutdown_timeout: Option<fn() -> std::time::Duration>,
) -> std::io::Result<SpawnedChild> {
let _ = shutdown_timeout;
running_process_platform_internal::spawn_sync(command, stdio, environment)
}
#[cfg(feature = "client-async")]
pub fn spawn_tokio(
command: &mut tokio::process::Command,
options: TokioSpawnOptions,
) -> std::io::Result<tokio::process::Child> {
command.kill_on_drop(options.kill_on_drop);
running_process_platform_internal::configure_compat_tokio_command(
command,
options.show_console,
options.kill_when_owner_dies,
)?;
let child =
running_process_platform_internal::platform::ape::spawn_tokio(command, |command| {
command.spawn()
})?;
running_process_platform_internal::after_compat_tokio_spawn(
&child,
options.kill_when_owner_dies,
)?;
Ok(child)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "client")]
use prost::Message;
use std::time::Duration;
fn assert_child_auto_traits<T>()
where
T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe,
{
}
#[test]
fn child_handles_preserve_thread_and_unwind_auto_traits() {
assert_child_auto_traits::<DaemonChild>();
assert_child_auto_traits::<SpawnedChild>();
}
#[cfg(feature = "client")]
#[derive(Clone, PartialEq, Message)]
struct LegacyClearAtTag4 {
#[prost(bool, tag = "4")]
clear_inherited_env: bool,
}
#[cfg(feature = "client")]
#[derive(Clone, PartialEq, Message)]
struct LegacyClearAtTag5 {
#[prost(bool, tag = "5")]
clear_inherited_env: bool,
}
#[cfg(feature = "client-async")]
#[test]
fn kill_when_owner_dies_defaults_off() {
assert!(!TokioSpawnOptions::default().kill_when_owner_dies);
}
#[test]
fn spawn_stdio_default_has_sane_values() {
let s = SpawnStdio::default();
assert!(matches!(s.stdin, StdioSource::Null));
assert!(matches!(s.stdout, StdioSource::Parent));
assert!(matches!(s.stderr, StdioSource::Parent));
assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
assert!(!s.show_console);
}
#[test]
fn daemon_stdio_default_is_null() {
let stdio = DaemonStdio::default();
assert!(matches!(stdio.stdout, DaemonStdioSource::Null));
assert!(matches!(stdio.stderr, DaemonStdioSource::Null));
}
#[test]
fn auto_environment_policy_depends_on_lifetime() {
assert_eq!(
EnvironmentPolicy::Auto.resolve(SpawnLifetime::Contained),
EnvironmentPolicy::Inherit
);
assert_eq!(
EnvironmentPolicy::Auto.resolve(SpawnLifetime::Daemon),
EnvironmentPolicy::UserBaseline
);
}
#[test]
fn explicit_environment_policy_is_not_rewritten() {
for policy in [
EnvironmentPolicy::Inherit,
EnvironmentPolicy::UserBaseline,
EnvironmentPolicy::Clear,
] {
assert_eq!(policy.resolve(SpawnLifetime::Contained), policy);
assert_eq!(policy.resolve(SpawnLifetime::Daemon), policy);
}
}
#[test]
fn wire_environment_policy_preserves_legacy_and_fails_closed() {
assert_eq!(
EnvironmentPolicy::from_wire(0, false),
Ok(EnvironmentPolicy::Inherit)
);
assert_eq!(
EnvironmentPolicy::from_wire(0, true),
Ok(EnvironmentPolicy::Clear)
);
assert_eq!(
EnvironmentPolicy::from_wire(1, true),
Ok(EnvironmentPolicy::Inherit)
);
assert_eq!(
EnvironmentPolicy::from_wire(2, false),
Ok(EnvironmentPolicy::UserBaseline)
);
assert_eq!(
EnvironmentPolicy::from_wire(3, false),
Ok(EnvironmentPolicy::Clear)
);
assert!(EnvironmentPolicy::from_wire(99, false).is_err());
assert_eq!(
EnvironmentPolicy::UserBaseline.legacy_clear_fallback(),
Ok(true)
);
assert!(EnvironmentPolicy::Auto.wire_value().is_err());
}
#[cfg(feature = "client")]
#[test]
fn old_clients_and_new_servers_interoperate_on_all_spawn_messages() {
use crate::broker::protocol_v2::SessionStart;
use crate::proto::daemon::{
SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
};
for legacy_clear in [false, true] {
let tag5 = LegacyClearAtTag5 {
clear_inherited_env: legacy_clear,
}
.encode_to_vec();
let daemon = SpawnDaemonRequest::decode(tag5.as_slice()).unwrap();
let session = SessionStart::decode(tag5.as_slice()).unwrap();
let expected = if legacy_clear {
EnvironmentPolicy::Clear
} else {
EnvironmentPolicy::Inherit
};
assert_eq!(
EnvironmentPolicy::from_wire(daemon.environment_policy, daemon.clear_inherited_env),
Ok(expected)
);
assert_eq!(
EnvironmentPolicy::from_wire(
session.environment_policy,
session.clear_inherited_env
),
Ok(expected)
);
let tag4 = LegacyClearAtTag4 {
clear_inherited_env: legacy_clear,
}
.encode_to_vec();
let pipe = SpawnPipeSessionRequest::decode(tag4.as_slice()).unwrap();
let pty = SpawnPtySessionRequest::decode(tag4.as_slice()).unwrap();
assert_eq!(
EnvironmentPolicy::from_wire(pipe.environment_policy, pipe.clear_inherited_env),
Ok(expected)
);
assert_eq!(
EnvironmentPolicy::from_wire(pty.environment_policy, pty.clear_inherited_env),
Ok(expected)
);
}
}
#[cfg(feature = "client")]
#[test]
fn new_clients_dual_write_fallback_for_old_servers_on_all_spawn_messages() {
use crate::broker::protocol_v2::SessionStart;
use crate::proto::daemon::{
SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
};
for policy in [
EnvironmentPolicy::Inherit,
EnvironmentPolicy::UserBaseline,
EnvironmentPolicy::Clear,
] {
let legacy_clear = policy.legacy_clear_fallback().unwrap();
let wire_policy = policy.wire_value().unwrap();
let daemon = SpawnDaemonRequest {
clear_inherited_env: legacy_clear,
environment_policy: wire_policy,
..Default::default()
};
let pipe = SpawnPipeSessionRequest {
clear_inherited_env: legacy_clear,
environment_policy: wire_policy,
..Default::default()
};
let pty = SpawnPtySessionRequest {
clear_inherited_env: legacy_clear,
environment_policy: wire_policy,
..Default::default()
};
let session = SessionStart {
clear_inherited_env: legacy_clear,
environment_policy: wire_policy,
..Default::default()
};
assert_eq!(
LegacyClearAtTag5::decode(daemon.encode_to_vec().as_slice())
.unwrap()
.clear_inherited_env,
legacy_clear
);
assert_eq!(
LegacyClearAtTag4::decode(pipe.encode_to_vec().as_slice())
.unwrap()
.clear_inherited_env,
legacy_clear
);
assert_eq!(
LegacyClearAtTag4::decode(pty.encode_to_vec().as_slice())
.unwrap()
.clear_inherited_env,
legacy_clear
);
assert_eq!(
LegacyClearAtTag5::decode(session.encode_to_vec().as_slice())
.unwrap()
.clear_inherited_env,
legacy_clear
);
}
}
#[cfg(feature = "client-async")]
#[test]
fn tokio_spawn_defaults_to_contained_consoleless_children() {
assert_eq!(
TokioSpawnOptions::default(),
TokioSpawnOptions {
kill_on_drop: true,
show_console: false,
kill_when_owner_dies: false,
}
);
}
}