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. The
143    ///   child is created suspended and resumed only once it is in the job, so
144    ///   it is contained from its first instruction and no descendant it starts
145    ///   can escape. If containment fails the child is terminated, never run.
146    /// - **macOS**: forks a kqueue supervisor before exec and waits for its
147    ///   owner/child watches to be registered before reporting spawn success.
148    ///
149    /// `kill_on_drop` only fires if the spawner runs its `Drop`; this option
150    /// covers the crash / SIGKILL / taskkill case where `Drop` never runs. Use
151    /// for transient children of a long-lived process (e.g. a daemon's compiler
152    /// subprocesses) that must not outlive their owner.
153    pub kill_when_owner_dies: bool,
154}
155
156#[cfg(feature = "client-async")]
157impl Default for TokioSpawnOptions {
158    fn default() -> Self {
159        Self {
160            kill_on_drop: true,
161            show_console: false,
162            kill_when_owner_dies: false,
163        }
164    }
165}
166
167/// Set on every child spawned through the daemon path, so a process can be
168/// recognized as a *declared daemon* rather than inferred to be one.
169///
170/// # Why a positive marker
171///
172/// Reapers previously had to infer daemon-ness from the **absence** of
173/// [`crate::ORIGINATOR_ENV_VAR`], which `spawn_daemon` strips. But absence is
174/// overloaded: it means both "this process deliberately detached itself" and
175/// "something in the chain clobbered the environment" — and those are
176/// byte-identical at the observation point, so no amount of process-lineage
177/// tracking can separate them. See zackees/clud#522, where an
178/// ancestry-fallback proposal and a daemon exemption read the same signal and
179/// drew opposite conclusions.
180///
181/// A positive declaration removes the ambiguity: only a process that actually
182/// went through the daemon path carries this.
183///
184/// # Caveat
185///
186/// This is still an environment variable, so a chain that strips
187/// `RUNNING_PROCESS_ORIGINATOR` strips this too. It narrows the ambiguous case
188/// rather than eliminating it; a durable answer would need the daemon's
189/// supervisor to register the PID somewhere the reaper can read.
190///
191/// Distinct from `RUNNING_PROCESS_DAEMON_SCOPE`, which names a broker scope
192/// and is unrelated.
193pub const DAEMON_MARKER_ENV_VAR: &str = "RUNNING_PROCESS_IS_DAEMON";
194
195/// Spawn `command` as a detached daemon. NUL stdio, sanitized handles,
196/// no console window, ignores parent's Ctrl-C / SIGINT (Windows:
197/// `CREATE_NEW_PROCESS_GROUP` + `DETACHED_PROCESS`; Unix: `setsid` puts the
198/// daemon in a new session so it's not in the parent's foreground group).
199///
200/// Use [`spawn_daemon_with_stdio`] when the daemon must write to stable
201/// caller-owned files. Parent stdio and anonymous pipes remain unavailable
202/// for detached children.
203pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
204    spawn_daemon_inner(
205        command,
206        DaemonStdio::default(),
207        EnvironmentPolicy::Auto,
208        false,
209        None,
210    )
211}
212
213/// Spawn a detached daemon with file-or-NUL stdout and stderr.
214///
215/// Stdin remains connected to null. The supplied handles are duplicated into
216/// the sanitized child handle list, so the caller can close its files after
217/// this function returns without affecting the daemon.
218pub fn spawn_daemon_with_stdio(
219    command: &mut Command,
220    stdio: DaemonStdio<'_>,
221) -> std::io::Result<DaemonChild> {
222    spawn_daemon_with_stdio_and_env_policy(command, stdio, EnvironmentPolicy::Auto)
223}
224
225/// [`spawn_daemon_with_stdio`] with an explicit environment policy.
226pub fn spawn_daemon_with_stdio_and_env_policy(
227    command: &mut Command,
228    stdio: DaemonStdio<'_>,
229    policy: EnvironmentPolicy,
230) -> std::io::Result<DaemonChild> {
231    spawn_daemon_inner(command, stdio, policy, false, None)
232}
233
234/// Like [`spawn_daemon`] but with explicit control over whether the
235/// daemon's inherited env is passed through to the child.
236///
237/// `clear_env = false` uses [`EnvironmentPolicy::Auto`], matching
238/// [`spawn_daemon`].
239///
240/// `clear_env = true`: child sees ONLY the explicit `command.env(...)`
241/// entries. Mirrors `command.env_clear()` semantics for callers using
242/// the manual `CreateProcessW` path (Rust stdlib's `env_clear` flag
243/// isn't observable through `Command::get_envs`, so our sanitized
244/// spawn machinery can't otherwise honour it).
245pub fn spawn_daemon_with_clear_env(
246    command: &mut Command,
247    clear_env: bool,
248) -> std::io::Result<DaemonChild> {
249    let policy = if clear_env {
250        EnvironmentPolicy::Clear
251    } else {
252        EnvironmentPolicy::Auto
253    };
254    spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
255}
256
257/// Spawn a detached daemon using an explicit environment policy.
258///
259/// [`EnvironmentPolicy::Auto`] resolves to
260/// [`EnvironmentPolicy::UserBaseline`] for daemons, excluding unlisted
261/// ambient variables. Use [`EnvironmentPolicy::Inherit`] as the explicit
262/// escape hatch for trusted callers that require the full parent environment.
263/// In every mode, explicit command environment additions, overrides, and
264/// removals are applied last.
265pub fn spawn_daemon_with_env_policy(
266    command: &mut Command,
267    policy: EnvironmentPolicy,
268) -> std::io::Result<DaemonChild> {
269    spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
270}
271
272/// Spawn a daemon from a caller-assembled complete environment base.
273pub fn spawn_daemon_with_explicit_environment(
274    command: &mut Command,
275    stdio: DaemonStdio<'_>,
276    environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
277    breakaway: bool,
278) -> std::io::Result<DaemonChild> {
279    spawn_daemon_with_environment(
280        command,
281        stdio,
282        SyncEnvironment::Explicit(environment),
283        breakaway,
284    )
285}
286
287/// Spawn a daemon using an explicit live environment base.
288pub fn spawn_daemon_with_environment(
289    command: &mut Command,
290    stdio: DaemonStdio<'_>,
291    environment: SyncEnvironment,
292    breakaway: bool,
293) -> std::io::Result<DaemonChild> {
294    mark_as_daemon(command);
295    running_process_platform_internal::spawn_sync_daemon(command, stdio, environment, breakaway)
296}
297
298/// Like [`spawn_daemon`], but the child also **breaks away from any Job
299/// Object the spawner belongs to** (Windows; a no-op elsewhere).
300///
301/// Use this for a daemon that must outlive the process tree that happened to
302/// start it — a build cache server, a language server, anything discovered
303/// and reused by later, unrelated invocations.
304///
305/// # Why this is separate from [`spawn_daemon`]
306///
307/// "Detached lifetime" and "escapes my caller's containment" are different
308/// properties, and callers genuinely want them independently. Job Object
309/// membership is inherited by every descendant at any depth, and jobs created
310/// by this crate carry `KILL_ON_JOB_CLOSE` — so without breakaway the kernel
311/// terminates such a daemon the moment the spawner's job handle drops, no
312/// matter how detached the daemon made itself.
313///
314/// But making that unconditional breaks the opposite use: a child spawned as
315/// a daemon purely to obtain a sanitized handle list must stay inside the
316/// caller's job. `testbins/src/bin/spawner.rs` does exactly this, and
317/// `containment_test::test_contained_group_kills_grandchildren` fails if its
318/// sleepers escape.
319///
320/// # Refusal is not silent
321///
322/// `CREATE_BREAKAWAY_FROM_JOB` is *refused*, not ignored, when the spawner
323/// sits inside a job that lacks `JOB_OBJECT_LIMIT_BREAKAWAY_OK`:
324/// `CreateProcessW` fails with `ERROR_ACCESS_DENIED`. Outer jobs we do not
325/// control are common (CI runners, container supervisors, debuggers), so the
326/// spawn retries once with the flag cleared — a daemon that stays contained
327/// beats a daemon that fails to start.
328pub fn spawn_daemon_breaking_away_from_job(command: &mut Command) -> std::io::Result<DaemonChild> {
329    spawn_daemon_inner(
330        command,
331        DaemonStdio::default(),
332        EnvironmentPolicy::Auto,
333        true,
334        None,
335    )
336}
337
338/// [`spawn_daemon_breaking_away_from_job`] with an explicit env policy.
339pub fn spawn_daemon_breaking_away_with_env_policy(
340    command: &mut Command,
341    policy: EnvironmentPolicy,
342) -> std::io::Result<DaemonChild> {
343    spawn_daemon_inner(command, DaemonStdio::default(), policy, true, None)
344}
345
346/// Spawn a daemon while preserving one explicitly prepared IPC listener.
347///
348/// This stays crate-private: ordinary callers must retain the sanitized
349/// close-extra-descriptors contract, while the broker launcher receives the
350/// opaque inheritance token only from `InheritableListener::prepare_for_daemon`.
351#[cfg(feature = "client")]
352pub(crate) fn spawn_daemon_with_inheritance(
353    command: &mut Command,
354    inheritance: running_process_platform_internal::platform::process::DaemonExecInheritance,
355) -> std::io::Result<DaemonChild> {
356    spawn_daemon_inner(
357        command,
358        DaemonStdio::default(),
359        EnvironmentPolicy::Auto,
360        false,
361        Some(inheritance),
362    )
363}
364
365/// Apply the daemon self-declaration to `command`. Split out from
366/// [`spawn_daemon_inner`] so the policy is unit-testable without spawning a
367/// real detached process.
368pub(crate) fn mark_as_daemon(command: &mut Command) {
369    command.env(DAEMON_MARKER_ENV_VAR, "1");
370}
371
372fn prepare_sync_environment(
373    policy: EnvironmentPolicy,
374) -> std::io::Result<running_process_platform_internal::platform::process::SyncEnvironment> {
375    use running_process_platform_internal::platform::process::SyncEnvironment;
376
377    if policy == EnvironmentPolicy::Inherit {
378        return Ok(SyncEnvironment::Inherit);
379    }
380    if policy == EnvironmentPolicy::Auto {
381        return Err(std::io::Error::new(
382            std::io::ErrorKind::InvalidInput,
383            "Auto environment policy must be resolved before platform spawn",
384        ));
385    }
386
387    let baseline = match policy {
388        EnvironmentPolicy::UserBaseline => crate::environment::user_baseline_environment()?,
389        EnvironmentPolicy::Clear => Vec::new(),
390        EnvironmentPolicy::Auto | EnvironmentPolicy::Inherit => unreachable!(),
391    };
392    Ok(SyncEnvironment::Explicit(baseline))
393}
394
395fn spawn_daemon_inner(
396    command: &mut Command,
397    stdio: DaemonStdio<'_>,
398    policy: EnvironmentPolicy,
399    breakaway: bool,
400    inheritance: Option<
401        running_process_platform_internal::platform::process::DaemonExecInheritance,
402    >,
403) -> std::io::Result<DaemonChild> {
404    // Every daemon-spawn variant funnels through here, so this is the one
405    // place that can mark them all — including the free functions consumers
406    // like zccache call directly.
407    mark_as_daemon(command);
408    let policy = policy.resolve(SpawnLifetime::Daemon);
409    let environment = prepare_sync_environment(policy)?;
410    match inheritance {
411        Some(inheritance) => {
412            running_process_platform_internal::platform::process::spawn_sync_daemon_with_inheritance(
413                command,
414                stdio,
415                environment,
416                breakaway,
417                inheritance,
418            )
419        }
420        None => running_process_platform_internal::platform::process::spawn_sync_daemon(
421            command,
422            stdio,
423            environment,
424            breakaway,
425        ),
426    }
427}
428
429/// Spawn `command` as a contained child with caller-controlled stdio.
430/// Sanitized handles, and no console (`DETACHED_PROCESS` on Windows). Child
431/// dies when the returned
432/// [`SpawnedChild`] is dropped.
433pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
434    spawn_with_env_policy(command, stdio, EnvironmentPolicy::Auto)
435}
436
437/// Spawn a contained child using an explicit environment policy.
438pub fn spawn_with_env_policy(
439    command: &mut Command,
440    stdio: SpawnStdio<'_>,
441    policy: EnvironmentPolicy,
442) -> std::io::Result<SpawnedChild> {
443    let policy = policy.resolve(SpawnLifetime::Contained);
444    let environment = prepare_sync_environment(policy)?;
445    running_process_platform_internal::platform::process::spawn_sync(command, stdio, environment)
446}
447
448/// Spawn a contained child from a caller-assembled complete environment base.
449pub fn spawn_with_explicit_environment(
450    command: &mut Command,
451    stdio: SpawnStdio<'_>,
452    environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
453    shutdown_timeout: Option<fn() -> std::time::Duration>,
454) -> std::io::Result<SpawnedChild> {
455    spawn_with_environment(
456        command,
457        stdio,
458        SyncEnvironment::Explicit(environment),
459        shutdown_timeout,
460    )
461}
462
463/// Spawn a contained child using an explicit live environment base.
464///
465/// The selected synchronous substrate owns the contained-child drop policy;
466/// the optional historical shutdown callback is accepted for source
467/// compatibility but is not evaluated by this boundary.
468pub fn spawn_with_environment(
469    command: &mut Command,
470    stdio: SpawnStdio<'_>,
471    environment: SyncEnvironment,
472    shutdown_timeout: Option<fn() -> std::time::Duration>,
473) -> std::io::Result<SpawnedChild> {
474    let _ = shutdown_timeout;
475    running_process_platform_internal::spawn_sync(command, stdio, environment)
476}
477
478/// Spawn a Tokio child through the centralized process-creation boundary.
479///
480/// Callers retain Tokio's async stdin/stdout/stderr and wait APIs, but may not
481/// apply platform creation flags themselves. On Windows, console suppression
482/// is owned here. Use [`spawn`] when the stronger sanitized-handle-list and
483/// kill-on-close Job Object contract is required.
484#[cfg(feature = "client-async")]
485pub fn spawn_tokio(
486    command: &mut tokio::process::Command,
487    options: TokioSpawnOptions,
488) -> std::io::Result<tokio::process::Child> {
489    command.kill_on_drop(options.kill_on_drop);
490    running_process_platform_internal::configure_compat_tokio_command(
491        command,
492        options.show_console,
493        options.kill_when_owner_dies,
494    )?;
495
496    let child =
497        running_process_platform_internal::platform::ape::spawn_tokio(command, |command| {
498            command.spawn()
499        })?;
500
501    // A containment failure is reported, not swallowed. `kill_when_owner_dies`
502    // is asked for by callers that must not leak children -- zccache's compile
503    // workers are the case this exists for -- and a spawn that quietly returns
504    // an uncontained child hands them exactly the orphan they asked to avoid.
505    running_process_platform_internal::after_compat_tokio_spawn(
506        &child,
507        options.kill_when_owner_dies,
508    )?;
509
510    Ok(child)
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    #[cfg(feature = "client")]
517    use prost::Message;
518    use std::time::Duration;
519
520    fn assert_child_auto_traits<T>()
521    where
522        T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe,
523    {
524    }
525
526    #[test]
527    fn child_handles_preserve_thread_and_unwind_auto_traits() {
528        assert_child_auto_traits::<DaemonChild>();
529        assert_child_auto_traits::<SpawnedChild>();
530    }
531
532    #[cfg(feature = "client")]
533    #[derive(Clone, PartialEq, Message)]
534    struct LegacyClearAtTag4 {
535        #[prost(bool, tag = "4")]
536        clear_inherited_env: bool,
537    }
538
539    #[cfg(feature = "client")]
540    #[derive(Clone, PartialEq, Message)]
541    struct LegacyClearAtTag5 {
542        #[prost(bool, tag = "5")]
543        clear_inherited_env: bool,
544    }
545
546    #[cfg(feature = "client-async")]
547    #[test]
548    fn kill_when_owner_dies_defaults_off() {
549        // Opt-in only — existing callers keep today's behavior.
550        assert!(!TokioSpawnOptions::default().kill_when_owner_dies);
551    }
552
553    #[test]
554    fn spawn_stdio_default_has_sane_values() {
555        let s = SpawnStdio::default();
556        assert!(matches!(s.stdin, StdioSource::Null));
557        assert!(matches!(s.stdout, StdioSource::Parent));
558        assert!(matches!(s.stderr, StdioSource::Parent));
559        assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
560        // No console window by default — opt-in only.
561        assert!(!s.show_console);
562    }
563
564    #[test]
565    fn daemon_stdio_default_is_null() {
566        let stdio = DaemonStdio::default();
567        assert!(matches!(stdio.stdout, DaemonStdioSource::Null));
568        assert!(matches!(stdio.stderr, DaemonStdioSource::Null));
569    }
570
571    #[test]
572    fn auto_environment_policy_depends_on_lifetime() {
573        assert_eq!(
574            EnvironmentPolicy::Auto.resolve(SpawnLifetime::Contained),
575            EnvironmentPolicy::Inherit
576        );
577        assert_eq!(
578            EnvironmentPolicy::Auto.resolve(SpawnLifetime::Daemon),
579            EnvironmentPolicy::UserBaseline
580        );
581    }
582
583    #[test]
584    fn explicit_environment_policy_is_not_rewritten() {
585        for policy in [
586            EnvironmentPolicy::Inherit,
587            EnvironmentPolicy::UserBaseline,
588            EnvironmentPolicy::Clear,
589        ] {
590            assert_eq!(policy.resolve(SpawnLifetime::Contained), policy);
591            assert_eq!(policy.resolve(SpawnLifetime::Daemon), policy);
592        }
593    }
594
595    #[test]
596    fn wire_environment_policy_preserves_legacy_and_fails_closed() {
597        assert_eq!(
598            EnvironmentPolicy::from_wire(0, false),
599            Ok(EnvironmentPolicy::Inherit)
600        );
601        assert_eq!(
602            EnvironmentPolicy::from_wire(0, true),
603            Ok(EnvironmentPolicy::Clear)
604        );
605        assert_eq!(
606            EnvironmentPolicy::from_wire(1, true),
607            Ok(EnvironmentPolicy::Inherit)
608        );
609        assert_eq!(
610            EnvironmentPolicy::from_wire(2, false),
611            Ok(EnvironmentPolicy::UserBaseline)
612        );
613        assert_eq!(
614            EnvironmentPolicy::from_wire(3, false),
615            Ok(EnvironmentPolicy::Clear)
616        );
617        assert!(EnvironmentPolicy::from_wire(99, false).is_err());
618        assert_eq!(
619            EnvironmentPolicy::UserBaseline.legacy_clear_fallback(),
620            Ok(true)
621        );
622        assert!(EnvironmentPolicy::Auto.wire_value().is_err());
623    }
624
625    #[cfg(feature = "client")]
626    #[test]
627    fn old_clients_and_new_servers_interoperate_on_all_spawn_messages() {
628        use crate::broker::protocol_v2::SessionStart;
629        use crate::proto::daemon::{
630            SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
631        };
632
633        for legacy_clear in [false, true] {
634            let tag5 = LegacyClearAtTag5 {
635                clear_inherited_env: legacy_clear,
636            }
637            .encode_to_vec();
638            let daemon = SpawnDaemonRequest::decode(tag5.as_slice()).unwrap();
639            let session = SessionStart::decode(tag5.as_slice()).unwrap();
640            let expected = if legacy_clear {
641                EnvironmentPolicy::Clear
642            } else {
643                EnvironmentPolicy::Inherit
644            };
645            assert_eq!(
646                EnvironmentPolicy::from_wire(daemon.environment_policy, daemon.clear_inherited_env),
647                Ok(expected)
648            );
649            assert_eq!(
650                EnvironmentPolicy::from_wire(
651                    session.environment_policy,
652                    session.clear_inherited_env
653                ),
654                Ok(expected)
655            );
656
657            let tag4 = LegacyClearAtTag4 {
658                clear_inherited_env: legacy_clear,
659            }
660            .encode_to_vec();
661            let pipe = SpawnPipeSessionRequest::decode(tag4.as_slice()).unwrap();
662            let pty = SpawnPtySessionRequest::decode(tag4.as_slice()).unwrap();
663            assert_eq!(
664                EnvironmentPolicy::from_wire(pipe.environment_policy, pipe.clear_inherited_env),
665                Ok(expected)
666            );
667            assert_eq!(
668                EnvironmentPolicy::from_wire(pty.environment_policy, pty.clear_inherited_env),
669                Ok(expected)
670            );
671        }
672    }
673
674    #[cfg(feature = "client")]
675    #[test]
676    fn new_clients_dual_write_fallback_for_old_servers_on_all_spawn_messages() {
677        use crate::broker::protocol_v2::SessionStart;
678        use crate::proto::daemon::{
679            SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
680        };
681
682        for policy in [
683            EnvironmentPolicy::Inherit,
684            EnvironmentPolicy::UserBaseline,
685            EnvironmentPolicy::Clear,
686        ] {
687            let legacy_clear = policy.legacy_clear_fallback().unwrap();
688            let wire_policy = policy.wire_value().unwrap();
689            let daemon = SpawnDaemonRequest {
690                clear_inherited_env: legacy_clear,
691                environment_policy: wire_policy,
692                ..Default::default()
693            };
694            let pipe = SpawnPipeSessionRequest {
695                clear_inherited_env: legacy_clear,
696                environment_policy: wire_policy,
697                ..Default::default()
698            };
699            let pty = SpawnPtySessionRequest {
700                clear_inherited_env: legacy_clear,
701                environment_policy: wire_policy,
702                ..Default::default()
703            };
704            let session = SessionStart {
705                clear_inherited_env: legacy_clear,
706                environment_policy: wire_policy,
707                ..Default::default()
708            };
709
710            assert_eq!(
711                LegacyClearAtTag5::decode(daemon.encode_to_vec().as_slice())
712                    .unwrap()
713                    .clear_inherited_env,
714                legacy_clear
715            );
716            assert_eq!(
717                LegacyClearAtTag4::decode(pipe.encode_to_vec().as_slice())
718                    .unwrap()
719                    .clear_inherited_env,
720                legacy_clear
721            );
722            assert_eq!(
723                LegacyClearAtTag4::decode(pty.encode_to_vec().as_slice())
724                    .unwrap()
725                    .clear_inherited_env,
726                legacy_clear
727            );
728            assert_eq!(
729                LegacyClearAtTag5::decode(session.encode_to_vec().as_slice())
730                    .unwrap()
731                    .clear_inherited_env,
732                legacy_clear
733            );
734        }
735    }
736
737    #[cfg(feature = "client-async")]
738    #[test]
739    fn tokio_spawn_defaults_to_contained_consoleless_children() {
740        assert_eq!(
741            TokioSpawnOptions::default(),
742            TokioSpawnOptions {
743                kill_on_drop: true,
744                show_console: false,
745                kill_when_owner_dies: false,
746            }
747        );
748    }
749}