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