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}