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}