Skip to main content

running_process/
spawn.rs

1//! Two-mode process spawning. Free functions only — no module-internal traits.
2//!
3//! Modes (only two; the dangerous combination `detached + caller-pipes` has no
4//! API surface):
5//!
6//!   * [`spawn_daemon`] — detached lifetime, sanitized file-or-NUL stdio,
7//!     sanitized handle list, no console window, ignores parent's Ctrl-C. The
8//!     returned [`DaemonChild`] does NOT die when dropped.
9//!   * [`spawn`] — contained lifetime, caller-controlled stdio via
10//!     [`SpawnStdio`], sanitized handle list, no console window by default
11//!     (opt in via [`SpawnStdio::show_console`]), bounded drain. The returned
12//!     [`SpawnedChild`] kills the child on Drop.
13//!
14//! ## Sanitized handle inheritance
15//!
16//! Both modes inherit ONLY the three stdio handles we resolve here. On
17//! Windows we use `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` to whitelist exactly
18//! the resolved handles. On Unix the spawned child runs a `pre_exec` closure
19//! that walks `/proc/self/fd` (or `/dev/fd`) and closes every fd > 2.
20//!
21//! Motivation: when a process tree has a pipe-redirected ancestor (Python
22//! `subprocess.Popen(stdout=PIPE)`, IDE language-server hosts, CI runners,
23//! etc.), every intermediate `CreateProcessW(bInheritHandles=TRUE)` on
24//! Windows — and every `fork`+`exec` of a non-`O_CLOEXEC` fd on Unix —
25//! duplicates that orphaned pipe write-end into the new child. The original
26//! reader at the top never sees EOF.
27//!
28//! Issue: <https://github.com/zackees/running-process/issues/110>.
29
30use std::process::Command;
31
32pub use running_process_platform_internal::platform::process::{
33    DaemonChild, DaemonStdio, DaemonStdioSource, SpawnStdio, SpawnedChild, SpawnedChildControl,
34    StdioSource, SyncEnvironment,
35};
36
37/// Selects the base environment used for a newly spawned process.
38///
39/// Explicit mutations added through [`Command::env`], [`Command::envs`], or
40/// [`Command::env_remove`] are applied after the selected base and therefore
41/// always win.
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
43pub enum EnvironmentPolicy {
44    /// Choose from the process lifetime: contained subprocesses inherit,
45    /// while detached daemons start from the logged-in user's baseline.
46    #[default]
47    Auto,
48    /// Inherit the spawning process's environment.
49    Inherit,
50    /// Start from the logged-in user's machine + user environment, discarding
51    /// the spawning process's ambient environment except for the documented
52    /// Unix locale, time-zone, and temporary-directory allowlist.
53    ///
54    /// Windows implements this with `CreateEnvironmentBlock`. Unix
55    /// reconstructs a clean login environment from the user's identity
56    /// (`getpwuid_r` → `USER`/`LOGNAME`/`HOME`/`SHELL`, platform default
57    /// `PATH`, carried-over locale/`TZ`/`TMPDIR`), falling back to inheritance
58    /// only when the passwd entry cannot be resolved.
59    ///
60    /// Consumers that need values such as `CARGO_HOME`, `RUSTUP_HOME`,
61    /// `SOLDR_*`, credentials, or runner-specific paths must pass them
62    /// explicitly on the [`Command`].
63    UserBaseline,
64    /// Start from an empty environment.
65    Clear,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(crate) enum SpawnLifetime {
70    Contained,
71    Daemon,
72}
73
74impl EnvironmentPolicy {
75    pub(crate) fn resolve(self, lifetime: SpawnLifetime) -> Self {
76        match (self, lifetime) {
77            (Self::Auto, SpawnLifetime::Contained) => Self::Inherit,
78            (Self::Auto, SpawnLifetime::Daemon) => Self::UserBaseline,
79            (explicit, _) => explicit,
80        }
81    }
82
83    /// Decode the additive wire policy, falling back to the deprecated
84    /// `clear_inherited_env` bit for older clients.
85    #[cfg(any(feature = "daemon", feature = "client-async", test))]
86    pub(crate) fn from_wire(value: i32, legacy_clear: bool) -> Result<Self, &'static str> {
87        match value {
88            0 => Ok(if legacy_clear {
89                Self::Clear
90            } else {
91                Self::Inherit
92            }),
93            1 => Ok(Self::Inherit),
94            2 => Ok(Self::UserBaseline),
95            3 => Ok(Self::Clear),
96            _ => Err("unknown environment policy"),
97        }
98    }
99
100    /// Encode a resolved policy for either daemon or broker-v2 protobufs.
101    #[cfg(any(feature = "client", test))]
102    pub(crate) fn wire_value(self) -> Result<i32, &'static str> {
103        match self {
104            Self::Inherit => Ok(1),
105            Self::UserBaseline => Ok(2),
106            Self::Clear => Ok(3),
107            Self::Auto => Err("Auto environment policy must be resolved before serialization"),
108        }
109    }
110
111    /// Compatibility bit written for servers that predate the wire enum.
112    /// `UserBaseline` deliberately degrades to `Clear`, never ambient inherit.
113    #[cfg(any(feature = "client", test))]
114    pub(crate) fn legacy_clear_fallback(self) -> Result<bool, &'static str> {
115        match self {
116            Self::Inherit => Ok(false),
117            Self::UserBaseline | Self::Clear => Ok(true),
118            Self::Auto => Err("Auto environment policy must be resolved before serialization"),
119        }
120    }
121}
122
123// ── Public API ──────────────────────────────────────────────────────────────
124
125/// Creation policy for [`spawn_tokio`].
126///
127/// This compatibility entrypoint lets async daemons keep Tokio's pipe and
128/// wait APIs while making `running-process` the sole owner of child-creation
129/// policy. It defaults to contained, console-less children.
130#[cfg(feature = "client-async")]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub struct TokioSpawnOptions {
133    /// Terminate the child when Tokio's child handle is dropped.
134    pub kill_on_drop: bool,
135    /// Whether Windows children may inherit or allocate a visible console.
136    pub show_console: bool,
137    /// Kill this child at the OS level when the spawning process dies.
138    ///
139    /// - **Linux**: installs `PR_SET_PDEATHSIG(SIGTERM)` in the child.
140    /// - **Windows**: assigns the child to a process-wide `KILL_ON_JOB_CLOSE`
141    ///   Job Object, so the child (and its descendants) die when the spawner's
142    ///   handle to the job closes — i.e. when the spawner process exits.
143    /// - **macOS**: forks a kqueue supervisor before exec and waits for its
144    ///   owner/child watches to be registered before reporting spawn success.
145    ///
146    /// `kill_on_drop` only fires if the spawner runs its `Drop`; this option
147    /// covers the crash / SIGKILL / taskkill case where `Drop` never runs. Use
148    /// for transient children of a long-lived process (e.g. a daemon's compiler
149    /// subprocesses) that must not outlive their owner.
150    pub kill_when_owner_dies: bool,
151}
152
153#[cfg(feature = "client-async")]
154impl Default for TokioSpawnOptions {
155    fn default() -> Self {
156        Self {
157            kill_on_drop: true,
158            show_console: false,
159            kill_when_owner_dies: false,
160        }
161    }
162}
163
164/// Set on every child spawned through the daemon path, so a process can be
165/// recognized as a *declared daemon* rather than inferred to be one.
166///
167/// # Why a positive marker
168///
169/// Reapers previously had to infer daemon-ness from the **absence** of
170/// [`crate::ORIGINATOR_ENV_VAR`], which `spawn_daemon` strips. But absence is
171/// overloaded: it means both "this process deliberately detached itself" and
172/// "something in the chain clobbered the environment" — and those are
173/// byte-identical at the observation point, so no amount of process-lineage
174/// tracking can separate them. See zackees/clud#522, where an
175/// ancestry-fallback proposal and a daemon exemption read the same signal and
176/// drew opposite conclusions.
177///
178/// A positive declaration removes the ambiguity: only a process that actually
179/// went through the daemon path carries this.
180///
181/// # Caveat
182///
183/// This is still an environment variable, so a chain that strips
184/// `RUNNING_PROCESS_ORIGINATOR` strips this too. It narrows the ambiguous case
185/// rather than eliminating it; a durable answer would need the daemon's
186/// supervisor to register the PID somewhere the reaper can read.
187///
188/// Distinct from `RUNNING_PROCESS_DAEMON_SCOPE`, which names a broker scope
189/// and is unrelated.
190pub const DAEMON_MARKER_ENV_VAR: &str = "RUNNING_PROCESS_IS_DAEMON";
191
192/// Spawn `command` as a detached daemon. NUL stdio, sanitized handles,
193/// no console window, ignores parent's Ctrl-C / SIGINT (Windows:
194/// `CREATE_NEW_PROCESS_GROUP` + `DETACHED_PROCESS`; Unix: `setsid` puts the
195/// daemon in a new session so it's not in the parent's foreground group).
196///
197/// Use [`spawn_daemon_with_stdio`] when the daemon must write to stable
198/// caller-owned files. Parent stdio and anonymous pipes remain unavailable
199/// for detached children.
200pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
201    spawn_daemon_inner(
202        command,
203        DaemonStdio::default(),
204        EnvironmentPolicy::Auto,
205        false,
206        None,
207    )
208}
209
210/// Spawn a detached daemon with file-or-NUL stdout and stderr.
211///
212/// Stdin remains connected to null. The supplied handles are duplicated into
213/// the sanitized child handle list, so the caller can close its files after
214/// this function returns without affecting the daemon.
215pub fn spawn_daemon_with_stdio(
216    command: &mut Command,
217    stdio: DaemonStdio<'_>,
218) -> std::io::Result<DaemonChild> {
219    spawn_daemon_with_stdio_and_env_policy(command, stdio, EnvironmentPolicy::Auto)
220}
221
222/// [`spawn_daemon_with_stdio`] with an explicit environment policy.
223pub fn spawn_daemon_with_stdio_and_env_policy(
224    command: &mut Command,
225    stdio: DaemonStdio<'_>,
226    policy: EnvironmentPolicy,
227) -> std::io::Result<DaemonChild> {
228    spawn_daemon_inner(command, stdio, policy, false, None)
229}
230
231/// Like [`spawn_daemon`] but with explicit control over whether the
232/// daemon's inherited env is passed through to the child.
233///
234/// `clear_env = false` uses [`EnvironmentPolicy::Auto`], matching
235/// [`spawn_daemon`].
236///
237/// `clear_env = true`: child sees ONLY the explicit `command.env(...)`
238/// entries. Mirrors `command.env_clear()` semantics for callers using
239/// the manual `CreateProcessW` path (Rust stdlib's `env_clear` flag
240/// isn't observable through `Command::get_envs`, so our sanitized
241/// spawn machinery can't otherwise honour it).
242pub fn spawn_daemon_with_clear_env(
243    command: &mut Command,
244    clear_env: bool,
245) -> std::io::Result<DaemonChild> {
246    let policy = if clear_env {
247        EnvironmentPolicy::Clear
248    } else {
249        EnvironmentPolicy::Auto
250    };
251    spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
252}
253
254/// Spawn a detached daemon using an explicit environment policy.
255///
256/// [`EnvironmentPolicy::Auto`] resolves to
257/// [`EnvironmentPolicy::UserBaseline`] for daemons, excluding unlisted
258/// ambient variables. Use [`EnvironmentPolicy::Inherit`] as the explicit
259/// escape hatch for trusted callers that require the full parent environment.
260/// In every mode, explicit command environment additions, overrides, and
261/// removals are applied last.
262pub fn spawn_daemon_with_env_policy(
263    command: &mut Command,
264    policy: EnvironmentPolicy,
265) -> std::io::Result<DaemonChild> {
266    spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
267}
268
269/// Spawn a daemon from a caller-assembled complete environment base.
270pub fn spawn_daemon_with_explicit_environment(
271    command: &mut Command,
272    stdio: DaemonStdio<'_>,
273    environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
274    breakaway: bool,
275) -> std::io::Result<DaemonChild> {
276    spawn_daemon_with_environment(
277        command,
278        stdio,
279        SyncEnvironment::Explicit(environment),
280        breakaway,
281    )
282}
283
284/// Spawn a daemon using an explicit live environment base.
285pub fn spawn_daemon_with_environment(
286    command: &mut Command,
287    stdio: DaemonStdio<'_>,
288    environment: SyncEnvironment,
289    breakaway: bool,
290) -> std::io::Result<DaemonChild> {
291    mark_as_daemon(command);
292    running_process_platform_internal::spawn_sync_daemon(command, stdio, environment, breakaway)
293}
294
295/// Like [`spawn_daemon`], but the child also **breaks away from any Job
296/// Object the spawner belongs to** (Windows; a no-op elsewhere).
297///
298/// Use this for a daemon that must outlive the process tree that happened to
299/// start it — a build cache server, a language server, anything discovered
300/// and reused by later, unrelated invocations.
301///
302/// # Why this is separate from [`spawn_daemon`]
303///
304/// "Detached lifetime" and "escapes my caller's containment" are different
305/// properties, and callers genuinely want them independently. Job Object
306/// membership is inherited by every descendant at any depth, and jobs created
307/// by this crate carry `KILL_ON_JOB_CLOSE` — so without breakaway the kernel
308/// terminates such a daemon the moment the spawner's job handle drops, no
309/// matter how detached the daemon made itself.
310///
311/// But making that unconditional breaks the opposite use: a child spawned as
312/// a daemon purely to obtain a sanitized handle list must stay inside the
313/// caller's job. `testbins/src/bin/spawner.rs` does exactly this, and
314/// `containment_test::test_contained_group_kills_grandchildren` fails if its
315/// sleepers escape.
316///
317/// # Refusal is not silent
318///
319/// `CREATE_BREAKAWAY_FROM_JOB` is *refused*, not ignored, when the spawner
320/// sits inside a job that lacks `JOB_OBJECT_LIMIT_BREAKAWAY_OK`:
321/// `CreateProcessW` fails with `ERROR_ACCESS_DENIED`. Outer jobs we do not
322/// control are common (CI runners, container supervisors, debuggers), so the
323/// spawn retries once with the flag cleared — a daemon that stays contained
324/// beats a daemon that fails to start.
325pub fn spawn_daemon_breaking_away_from_job(command: &mut Command) -> std::io::Result<DaemonChild> {
326    spawn_daemon_inner(
327        command,
328        DaemonStdio::default(),
329        EnvironmentPolicy::Auto,
330        true,
331        None,
332    )
333}
334
335/// [`spawn_daemon_breaking_away_from_job`] with an explicit env policy.
336pub fn spawn_daemon_breaking_away_with_env_policy(
337    command: &mut Command,
338    policy: EnvironmentPolicy,
339) -> std::io::Result<DaemonChild> {
340    spawn_daemon_inner(command, DaemonStdio::default(), policy, true, None)
341}
342
343/// Spawn a daemon while preserving one explicitly prepared IPC listener.
344///
345/// This stays crate-private: ordinary callers must retain the sanitized
346/// close-extra-descriptors contract, while the broker launcher receives the
347/// opaque inheritance token only from `InheritableListener::prepare_for_daemon`.
348#[cfg(feature = "client")]
349pub(crate) fn spawn_daemon_with_inheritance(
350    command: &mut Command,
351    inheritance: running_process_platform_internal::platform::process::DaemonExecInheritance,
352) -> std::io::Result<DaemonChild> {
353    spawn_daemon_inner(
354        command,
355        DaemonStdio::default(),
356        EnvironmentPolicy::Auto,
357        false,
358        Some(inheritance),
359    )
360}
361
362/// Apply the daemon self-declaration to `command`. Split out from
363/// [`spawn_daemon_inner`] so the policy is unit-testable without spawning a
364/// real detached process.
365pub(crate) fn mark_as_daemon(command: &mut Command) {
366    command.env(DAEMON_MARKER_ENV_VAR, "1");
367}
368
369fn prepare_sync_environment(
370    policy: EnvironmentPolicy,
371) -> std::io::Result<running_process_platform_internal::platform::process::SyncEnvironment> {
372    use running_process_platform_internal::platform::process::SyncEnvironment;
373
374    if policy == EnvironmentPolicy::Inherit {
375        return Ok(SyncEnvironment::Inherit);
376    }
377    if policy == EnvironmentPolicy::Auto {
378        return Err(std::io::Error::new(
379            std::io::ErrorKind::InvalidInput,
380            "Auto environment policy must be resolved before platform spawn",
381        ));
382    }
383
384    let baseline = match policy {
385        EnvironmentPolicy::UserBaseline => crate::environment::user_baseline_environment()?,
386        EnvironmentPolicy::Clear => Vec::new(),
387        EnvironmentPolicy::Auto | EnvironmentPolicy::Inherit => unreachable!(),
388    };
389    Ok(SyncEnvironment::Explicit(baseline))
390}
391
392fn spawn_daemon_inner(
393    command: &mut Command,
394    stdio: DaemonStdio<'_>,
395    policy: EnvironmentPolicy,
396    breakaway: bool,
397    inheritance: Option<
398        running_process_platform_internal::platform::process::DaemonExecInheritance,
399    >,
400) -> std::io::Result<DaemonChild> {
401    // Every daemon-spawn variant funnels through here, so this is the one
402    // place that can mark them all — including the free functions consumers
403    // like zccache call directly.
404    mark_as_daemon(command);
405    let policy = policy.resolve(SpawnLifetime::Daemon);
406    let environment = prepare_sync_environment(policy)?;
407    match inheritance {
408        Some(inheritance) => {
409            running_process_platform_internal::platform::process::spawn_sync_daemon_with_inheritance(
410                command,
411                stdio,
412                environment,
413                breakaway,
414                inheritance,
415            )
416        }
417        None => running_process_platform_internal::platform::process::spawn_sync_daemon(
418            command,
419            stdio,
420            environment,
421            breakaway,
422        ),
423    }
424}
425
426/// Spawn `command` as a contained child with caller-controlled stdio.
427/// Sanitized handles, and no console (`DETACHED_PROCESS` on Windows). Child
428/// dies when the returned
429/// [`SpawnedChild`] is dropped.
430pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
431    spawn_with_env_policy(command, stdio, EnvironmentPolicy::Auto)
432}
433
434/// Spawn a contained child using an explicit environment policy.
435pub fn spawn_with_env_policy(
436    command: &mut Command,
437    stdio: SpawnStdio<'_>,
438    policy: EnvironmentPolicy,
439) -> std::io::Result<SpawnedChild> {
440    let policy = policy.resolve(SpawnLifetime::Contained);
441    let environment = prepare_sync_environment(policy)?;
442    running_process_platform_internal::platform::process::spawn_sync(command, stdio, environment)
443}
444
445/// Spawn a contained child from a caller-assembled complete environment base.
446pub fn spawn_with_explicit_environment(
447    command: &mut Command,
448    stdio: SpawnStdio<'_>,
449    environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
450    shutdown_timeout: Option<fn() -> std::time::Duration>,
451) -> std::io::Result<SpawnedChild> {
452    spawn_with_environment(
453        command,
454        stdio,
455        SyncEnvironment::Explicit(environment),
456        shutdown_timeout,
457    )
458}
459
460/// Spawn a contained child using an explicit live environment base.
461///
462/// The selected synchronous substrate owns the contained-child drop policy;
463/// the optional historical shutdown callback is accepted for source
464/// compatibility but is not evaluated by this boundary.
465pub fn spawn_with_environment(
466    command: &mut Command,
467    stdio: SpawnStdio<'_>,
468    environment: SyncEnvironment,
469    shutdown_timeout: Option<fn() -> std::time::Duration>,
470) -> std::io::Result<SpawnedChild> {
471    let _ = shutdown_timeout;
472    running_process_platform_internal::spawn_sync(command, stdio, environment)
473}
474
475/// Spawn a Tokio child through the centralized process-creation boundary.
476///
477/// Callers retain Tokio's async stdin/stdout/stderr and wait APIs, but may not
478/// apply platform creation flags themselves. On Windows, console suppression
479/// is owned here. Use [`spawn`] when the stronger sanitized-handle-list and
480/// kill-on-close Job Object contract is required.
481#[cfg(feature = "client-async")]
482pub fn spawn_tokio(
483    command: &mut tokio::process::Command,
484    options: TokioSpawnOptions,
485) -> std::io::Result<tokio::process::Child> {
486    command.kill_on_drop(options.kill_on_drop);
487    running_process_platform_internal::configure_compat_tokio_command(
488        command,
489        options.show_console,
490        options.kill_when_owner_dies,
491    )?;
492
493    let child = command.spawn()?;
494
495    // A containment failure is reported, not swallowed. `kill_when_owner_dies`
496    // is asked for by callers that must not leak children -- zccache's compile
497    // workers are the case this exists for -- and a spawn that quietly returns
498    // an uncontained child hands them exactly the orphan they asked to avoid.
499    running_process_platform_internal::after_compat_tokio_spawn(
500        &child,
501        options.kill_when_owner_dies,
502    )?;
503
504    Ok(child)
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    #[cfg(feature = "client")]
511    use prost::Message;
512    use std::time::Duration;
513
514    fn assert_child_auto_traits<T>()
515    where
516        T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe,
517    {
518    }
519
520    #[test]
521    fn child_handles_preserve_thread_and_unwind_auto_traits() {
522        assert_child_auto_traits::<DaemonChild>();
523        assert_child_auto_traits::<SpawnedChild>();
524    }
525
526    #[cfg(feature = "client")]
527    #[derive(Clone, PartialEq, Message)]
528    struct LegacyClearAtTag4 {
529        #[prost(bool, tag = "4")]
530        clear_inherited_env: bool,
531    }
532
533    #[cfg(feature = "client")]
534    #[derive(Clone, PartialEq, Message)]
535    struct LegacyClearAtTag5 {
536        #[prost(bool, tag = "5")]
537        clear_inherited_env: bool,
538    }
539
540    #[cfg(feature = "client-async")]
541    #[test]
542    fn kill_when_owner_dies_defaults_off() {
543        // Opt-in only — existing callers keep today's behavior.
544        assert!(!TokioSpawnOptions::default().kill_when_owner_dies);
545    }
546
547    #[test]
548    fn spawn_stdio_default_has_sane_values() {
549        let s = SpawnStdio::default();
550        assert!(matches!(s.stdin, StdioSource::Null));
551        assert!(matches!(s.stdout, StdioSource::Parent));
552        assert!(matches!(s.stderr, StdioSource::Parent));
553        assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
554        // No console window by default — opt-in only.
555        assert!(!s.show_console);
556    }
557
558    #[test]
559    fn daemon_stdio_default_is_null() {
560        let stdio = DaemonStdio::default();
561        assert!(matches!(stdio.stdout, DaemonStdioSource::Null));
562        assert!(matches!(stdio.stderr, DaemonStdioSource::Null));
563    }
564
565    #[test]
566    fn auto_environment_policy_depends_on_lifetime() {
567        assert_eq!(
568            EnvironmentPolicy::Auto.resolve(SpawnLifetime::Contained),
569            EnvironmentPolicy::Inherit
570        );
571        assert_eq!(
572            EnvironmentPolicy::Auto.resolve(SpawnLifetime::Daemon),
573            EnvironmentPolicy::UserBaseline
574        );
575    }
576
577    #[test]
578    fn explicit_environment_policy_is_not_rewritten() {
579        for policy in [
580            EnvironmentPolicy::Inherit,
581            EnvironmentPolicy::UserBaseline,
582            EnvironmentPolicy::Clear,
583        ] {
584            assert_eq!(policy.resolve(SpawnLifetime::Contained), policy);
585            assert_eq!(policy.resolve(SpawnLifetime::Daemon), policy);
586        }
587    }
588
589    #[test]
590    fn wire_environment_policy_preserves_legacy_and_fails_closed() {
591        assert_eq!(
592            EnvironmentPolicy::from_wire(0, false),
593            Ok(EnvironmentPolicy::Inherit)
594        );
595        assert_eq!(
596            EnvironmentPolicy::from_wire(0, true),
597            Ok(EnvironmentPolicy::Clear)
598        );
599        assert_eq!(
600            EnvironmentPolicy::from_wire(1, true),
601            Ok(EnvironmentPolicy::Inherit)
602        );
603        assert_eq!(
604            EnvironmentPolicy::from_wire(2, false),
605            Ok(EnvironmentPolicy::UserBaseline)
606        );
607        assert_eq!(
608            EnvironmentPolicy::from_wire(3, false),
609            Ok(EnvironmentPolicy::Clear)
610        );
611        assert!(EnvironmentPolicy::from_wire(99, false).is_err());
612        assert_eq!(
613            EnvironmentPolicy::UserBaseline.legacy_clear_fallback(),
614            Ok(true)
615        );
616        assert!(EnvironmentPolicy::Auto.wire_value().is_err());
617    }
618
619    #[cfg(feature = "client")]
620    #[test]
621    fn old_clients_and_new_servers_interoperate_on_all_spawn_messages() {
622        use crate::broker::protocol_v2::SessionStart;
623        use crate::proto::daemon::{
624            SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
625        };
626
627        for legacy_clear in [false, true] {
628            let tag5 = LegacyClearAtTag5 {
629                clear_inherited_env: legacy_clear,
630            }
631            .encode_to_vec();
632            let daemon = SpawnDaemonRequest::decode(tag5.as_slice()).unwrap();
633            let session = SessionStart::decode(tag5.as_slice()).unwrap();
634            let expected = if legacy_clear {
635                EnvironmentPolicy::Clear
636            } else {
637                EnvironmentPolicy::Inherit
638            };
639            assert_eq!(
640                EnvironmentPolicy::from_wire(daemon.environment_policy, daemon.clear_inherited_env),
641                Ok(expected)
642            );
643            assert_eq!(
644                EnvironmentPolicy::from_wire(
645                    session.environment_policy,
646                    session.clear_inherited_env
647                ),
648                Ok(expected)
649            );
650
651            let tag4 = LegacyClearAtTag4 {
652                clear_inherited_env: legacy_clear,
653            }
654            .encode_to_vec();
655            let pipe = SpawnPipeSessionRequest::decode(tag4.as_slice()).unwrap();
656            let pty = SpawnPtySessionRequest::decode(tag4.as_slice()).unwrap();
657            assert_eq!(
658                EnvironmentPolicy::from_wire(pipe.environment_policy, pipe.clear_inherited_env),
659                Ok(expected)
660            );
661            assert_eq!(
662                EnvironmentPolicy::from_wire(pty.environment_policy, pty.clear_inherited_env),
663                Ok(expected)
664            );
665        }
666    }
667
668    #[cfg(feature = "client")]
669    #[test]
670    fn new_clients_dual_write_fallback_for_old_servers_on_all_spawn_messages() {
671        use crate::broker::protocol_v2::SessionStart;
672        use crate::proto::daemon::{
673            SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
674        };
675
676        for policy in [
677            EnvironmentPolicy::Inherit,
678            EnvironmentPolicy::UserBaseline,
679            EnvironmentPolicy::Clear,
680        ] {
681            let legacy_clear = policy.legacy_clear_fallback().unwrap();
682            let wire_policy = policy.wire_value().unwrap();
683            let daemon = SpawnDaemonRequest {
684                clear_inherited_env: legacy_clear,
685                environment_policy: wire_policy,
686                ..Default::default()
687            };
688            let pipe = SpawnPipeSessionRequest {
689                clear_inherited_env: legacy_clear,
690                environment_policy: wire_policy,
691                ..Default::default()
692            };
693            let pty = SpawnPtySessionRequest {
694                clear_inherited_env: legacy_clear,
695                environment_policy: wire_policy,
696                ..Default::default()
697            };
698            let session = SessionStart {
699                clear_inherited_env: legacy_clear,
700                environment_policy: wire_policy,
701                ..Default::default()
702            };
703
704            assert_eq!(
705                LegacyClearAtTag5::decode(daemon.encode_to_vec().as_slice())
706                    .unwrap()
707                    .clear_inherited_env,
708                legacy_clear
709            );
710            assert_eq!(
711                LegacyClearAtTag4::decode(pipe.encode_to_vec().as_slice())
712                    .unwrap()
713                    .clear_inherited_env,
714                legacy_clear
715            );
716            assert_eq!(
717                LegacyClearAtTag4::decode(pty.encode_to_vec().as_slice())
718                    .unwrap()
719                    .clear_inherited_env,
720                legacy_clear
721            );
722            assert_eq!(
723                LegacyClearAtTag5::decode(session.encode_to_vec().as_slice())
724                    .unwrap()
725                    .clear_inherited_env,
726                legacy_clear
727            );
728        }
729    }
730
731    #[cfg(feature = "client-async")]
732    #[test]
733    fn tokio_spawn_defaults_to_contained_consoleless_children() {
734        assert_eq!(
735            TokioSpawnOptions::default(),
736            TokioSpawnOptions {
737                kill_on_drop: true,
738                show_console: false,
739                kill_when_owner_dies: false,
740            }
741        );
742    }
743}