1use 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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
43pub enum EnvironmentPolicy {
44 #[default]
47 Auto,
48 Inherit,
50 UserBaseline,
64 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 #[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 #[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 #[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#[cfg(feature = "client-async")]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub struct TokioSpawnOptions {
133 pub kill_on_drop: bool,
135 pub show_console: bool,
137 pub kill_when_owner_dies: bool,
151}
152
153#[cfg(feature = "client-async")]
154impl Default for TokioSpawnOptions {
155 fn default() -> Self {
156 Self {
157 kill_on_drop: true,
158 show_console: false,
159 kill_when_owner_dies: false,
160 }
161 }
162}
163
164pub const DAEMON_MARKER_ENV_VAR: &str = "RUNNING_PROCESS_IS_DAEMON";
191
192pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
201 spawn_daemon_inner(
202 command,
203 DaemonStdio::default(),
204 EnvironmentPolicy::Auto,
205 false,
206 None,
207 )
208}
209
210pub fn spawn_daemon_with_stdio(
216 command: &mut Command,
217 stdio: DaemonStdio<'_>,
218) -> std::io::Result<DaemonChild> {
219 spawn_daemon_with_stdio_and_env_policy(command, stdio, EnvironmentPolicy::Auto)
220}
221
222pub fn spawn_daemon_with_stdio_and_env_policy(
224 command: &mut Command,
225 stdio: DaemonStdio<'_>,
226 policy: EnvironmentPolicy,
227) -> std::io::Result<DaemonChild> {
228 spawn_daemon_inner(command, stdio, policy, false, None)
229}
230
231pub fn spawn_daemon_with_clear_env(
243 command: &mut Command,
244 clear_env: bool,
245) -> std::io::Result<DaemonChild> {
246 let policy = if clear_env {
247 EnvironmentPolicy::Clear
248 } else {
249 EnvironmentPolicy::Auto
250 };
251 spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
252}
253
254pub fn spawn_daemon_with_env_policy(
263 command: &mut Command,
264 policy: EnvironmentPolicy,
265) -> std::io::Result<DaemonChild> {
266 spawn_daemon_inner(command, DaemonStdio::default(), policy, false, None)
267}
268
269pub fn spawn_daemon_with_explicit_environment(
271 command: &mut Command,
272 stdio: DaemonStdio<'_>,
273 environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
274 breakaway: bool,
275) -> std::io::Result<DaemonChild> {
276 spawn_daemon_with_environment(
277 command,
278 stdio,
279 SyncEnvironment::Explicit(environment),
280 breakaway,
281 )
282}
283
284pub fn spawn_daemon_with_environment(
286 command: &mut Command,
287 stdio: DaemonStdio<'_>,
288 environment: SyncEnvironment,
289 breakaway: bool,
290) -> std::io::Result<DaemonChild> {
291 mark_as_daemon(command);
292 running_process_platform_internal::spawn_sync_daemon(command, stdio, environment, breakaway)
293}
294
295pub fn spawn_daemon_breaking_away_from_job(command: &mut Command) -> std::io::Result<DaemonChild> {
326 spawn_daemon_inner(
327 command,
328 DaemonStdio::default(),
329 EnvironmentPolicy::Auto,
330 true,
331 None,
332 )
333}
334
335pub fn spawn_daemon_breaking_away_with_env_policy(
337 command: &mut Command,
338 policy: EnvironmentPolicy,
339) -> std::io::Result<DaemonChild> {
340 spawn_daemon_inner(command, DaemonStdio::default(), policy, true, None)
341}
342
343#[cfg(feature = "client")]
349pub(crate) fn spawn_daemon_with_inheritance(
350 command: &mut Command,
351 inheritance: running_process_platform_internal::platform::process::DaemonExecInheritance,
352) -> std::io::Result<DaemonChild> {
353 spawn_daemon_inner(
354 command,
355 DaemonStdio::default(),
356 EnvironmentPolicy::Auto,
357 false,
358 Some(inheritance),
359 )
360}
361
362pub(crate) fn mark_as_daemon(command: &mut Command) {
366 command.env(DAEMON_MARKER_ENV_VAR, "1");
367}
368
369fn prepare_sync_environment(
370 policy: EnvironmentPolicy,
371) -> std::io::Result<running_process_platform_internal::platform::process::SyncEnvironment> {
372 use running_process_platform_internal::platform::process::SyncEnvironment;
373
374 if policy == EnvironmentPolicy::Inherit {
375 return Ok(SyncEnvironment::Inherit);
376 }
377 if policy == EnvironmentPolicy::Auto {
378 return Err(std::io::Error::new(
379 std::io::ErrorKind::InvalidInput,
380 "Auto environment policy must be resolved before platform spawn",
381 ));
382 }
383
384 let baseline = match policy {
385 EnvironmentPolicy::UserBaseline => crate::environment::user_baseline_environment()?,
386 EnvironmentPolicy::Clear => Vec::new(),
387 EnvironmentPolicy::Auto | EnvironmentPolicy::Inherit => unreachable!(),
388 };
389 Ok(SyncEnvironment::Explicit(baseline))
390}
391
392fn spawn_daemon_inner(
393 command: &mut Command,
394 stdio: DaemonStdio<'_>,
395 policy: EnvironmentPolicy,
396 breakaway: bool,
397 inheritance: Option<
398 running_process_platform_internal::platform::process::DaemonExecInheritance,
399 >,
400) -> std::io::Result<DaemonChild> {
401 mark_as_daemon(command);
405 let policy = policy.resolve(SpawnLifetime::Daemon);
406 let environment = prepare_sync_environment(policy)?;
407 match inheritance {
408 Some(inheritance) => {
409 running_process_platform_internal::platform::process::spawn_sync_daemon_with_inheritance(
410 command,
411 stdio,
412 environment,
413 breakaway,
414 inheritance,
415 )
416 }
417 None => running_process_platform_internal::platform::process::spawn_sync_daemon(
418 command,
419 stdio,
420 environment,
421 breakaway,
422 ),
423 }
424}
425
426pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
431 spawn_with_env_policy(command, stdio, EnvironmentPolicy::Auto)
432}
433
434pub fn spawn_with_env_policy(
436 command: &mut Command,
437 stdio: SpawnStdio<'_>,
438 policy: EnvironmentPolicy,
439) -> std::io::Result<SpawnedChild> {
440 let policy = policy.resolve(SpawnLifetime::Contained);
441 let environment = prepare_sync_environment(policy)?;
442 running_process_platform_internal::platform::process::spawn_sync(command, stdio, environment)
443}
444
445pub fn spawn_with_explicit_environment(
447 command: &mut Command,
448 stdio: SpawnStdio<'_>,
449 environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
450 shutdown_timeout: Option<fn() -> std::time::Duration>,
451) -> std::io::Result<SpawnedChild> {
452 spawn_with_environment(
453 command,
454 stdio,
455 SyncEnvironment::Explicit(environment),
456 shutdown_timeout,
457 )
458}
459
460pub fn spawn_with_environment(
466 command: &mut Command,
467 stdio: SpawnStdio<'_>,
468 environment: SyncEnvironment,
469 shutdown_timeout: Option<fn() -> std::time::Duration>,
470) -> std::io::Result<SpawnedChild> {
471 let _ = shutdown_timeout;
472 running_process_platform_internal::spawn_sync(command, stdio, environment)
473}
474
475#[cfg(feature = "client-async")]
482pub fn spawn_tokio(
483 command: &mut tokio::process::Command,
484 options: TokioSpawnOptions,
485) -> std::io::Result<tokio::process::Child> {
486 command.kill_on_drop(options.kill_on_drop);
487 running_process_platform_internal::configure_compat_tokio_command(
488 command,
489 options.show_console,
490 options.kill_when_owner_dies,
491 )?;
492
493 let child = command.spawn()?;
494
495 running_process_platform_internal::after_compat_tokio_spawn(
500 &child,
501 options.kill_when_owner_dies,
502 )?;
503
504 Ok(child)
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 #[cfg(feature = "client")]
511 use prost::Message;
512 use std::time::Duration;
513
514 fn assert_child_auto_traits<T>()
515 where
516 T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe,
517 {
518 }
519
520 #[test]
521 fn child_handles_preserve_thread_and_unwind_auto_traits() {
522 assert_child_auto_traits::<DaemonChild>();
523 assert_child_auto_traits::<SpawnedChild>();
524 }
525
526 #[cfg(feature = "client")]
527 #[derive(Clone, PartialEq, Message)]
528 struct LegacyClearAtTag4 {
529 #[prost(bool, tag = "4")]
530 clear_inherited_env: bool,
531 }
532
533 #[cfg(feature = "client")]
534 #[derive(Clone, PartialEq, Message)]
535 struct LegacyClearAtTag5 {
536 #[prost(bool, tag = "5")]
537 clear_inherited_env: bool,
538 }
539
540 #[cfg(feature = "client-async")]
541 #[test]
542 fn kill_when_owner_dies_defaults_off() {
543 assert!(!TokioSpawnOptions::default().kill_when_owner_dies);
545 }
546
547 #[test]
548 fn spawn_stdio_default_has_sane_values() {
549 let s = SpawnStdio::default();
550 assert!(matches!(s.stdin, StdioSource::Null));
551 assert!(matches!(s.stdout, StdioSource::Parent));
552 assert!(matches!(s.stderr, StdioSource::Parent));
553 assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
554 assert!(!s.show_console);
556 }
557
558 #[test]
559 fn daemon_stdio_default_is_null() {
560 let stdio = DaemonStdio::default();
561 assert!(matches!(stdio.stdout, DaemonStdioSource::Null));
562 assert!(matches!(stdio.stderr, DaemonStdioSource::Null));
563 }
564
565 #[test]
566 fn auto_environment_policy_depends_on_lifetime() {
567 assert_eq!(
568 EnvironmentPolicy::Auto.resolve(SpawnLifetime::Contained),
569 EnvironmentPolicy::Inherit
570 );
571 assert_eq!(
572 EnvironmentPolicy::Auto.resolve(SpawnLifetime::Daemon),
573 EnvironmentPolicy::UserBaseline
574 );
575 }
576
577 #[test]
578 fn explicit_environment_policy_is_not_rewritten() {
579 for policy in [
580 EnvironmentPolicy::Inherit,
581 EnvironmentPolicy::UserBaseline,
582 EnvironmentPolicy::Clear,
583 ] {
584 assert_eq!(policy.resolve(SpawnLifetime::Contained), policy);
585 assert_eq!(policy.resolve(SpawnLifetime::Daemon), policy);
586 }
587 }
588
589 #[test]
590 fn wire_environment_policy_preserves_legacy_and_fails_closed() {
591 assert_eq!(
592 EnvironmentPolicy::from_wire(0, false),
593 Ok(EnvironmentPolicy::Inherit)
594 );
595 assert_eq!(
596 EnvironmentPolicy::from_wire(0, true),
597 Ok(EnvironmentPolicy::Clear)
598 );
599 assert_eq!(
600 EnvironmentPolicy::from_wire(1, true),
601 Ok(EnvironmentPolicy::Inherit)
602 );
603 assert_eq!(
604 EnvironmentPolicy::from_wire(2, false),
605 Ok(EnvironmentPolicy::UserBaseline)
606 );
607 assert_eq!(
608 EnvironmentPolicy::from_wire(3, false),
609 Ok(EnvironmentPolicy::Clear)
610 );
611 assert!(EnvironmentPolicy::from_wire(99, false).is_err());
612 assert_eq!(
613 EnvironmentPolicy::UserBaseline.legacy_clear_fallback(),
614 Ok(true)
615 );
616 assert!(EnvironmentPolicy::Auto.wire_value().is_err());
617 }
618
619 #[cfg(feature = "client")]
620 #[test]
621 fn old_clients_and_new_servers_interoperate_on_all_spawn_messages() {
622 use crate::broker::protocol_v2::SessionStart;
623 use crate::proto::daemon::{
624 SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
625 };
626
627 for legacy_clear in [false, true] {
628 let tag5 = LegacyClearAtTag5 {
629 clear_inherited_env: legacy_clear,
630 }
631 .encode_to_vec();
632 let daemon = SpawnDaemonRequest::decode(tag5.as_slice()).unwrap();
633 let session = SessionStart::decode(tag5.as_slice()).unwrap();
634 let expected = if legacy_clear {
635 EnvironmentPolicy::Clear
636 } else {
637 EnvironmentPolicy::Inherit
638 };
639 assert_eq!(
640 EnvironmentPolicy::from_wire(daemon.environment_policy, daemon.clear_inherited_env),
641 Ok(expected)
642 );
643 assert_eq!(
644 EnvironmentPolicy::from_wire(
645 session.environment_policy,
646 session.clear_inherited_env
647 ),
648 Ok(expected)
649 );
650
651 let tag4 = LegacyClearAtTag4 {
652 clear_inherited_env: legacy_clear,
653 }
654 .encode_to_vec();
655 let pipe = SpawnPipeSessionRequest::decode(tag4.as_slice()).unwrap();
656 let pty = SpawnPtySessionRequest::decode(tag4.as_slice()).unwrap();
657 assert_eq!(
658 EnvironmentPolicy::from_wire(pipe.environment_policy, pipe.clear_inherited_env),
659 Ok(expected)
660 );
661 assert_eq!(
662 EnvironmentPolicy::from_wire(pty.environment_policy, pty.clear_inherited_env),
663 Ok(expected)
664 );
665 }
666 }
667
668 #[cfg(feature = "client")]
669 #[test]
670 fn new_clients_dual_write_fallback_for_old_servers_on_all_spawn_messages() {
671 use crate::broker::protocol_v2::SessionStart;
672 use crate::proto::daemon::{
673 SpawnDaemonRequest, SpawnPipeSessionRequest, SpawnPtySessionRequest,
674 };
675
676 for policy in [
677 EnvironmentPolicy::Inherit,
678 EnvironmentPolicy::UserBaseline,
679 EnvironmentPolicy::Clear,
680 ] {
681 let legacy_clear = policy.legacy_clear_fallback().unwrap();
682 let wire_policy = policy.wire_value().unwrap();
683 let daemon = SpawnDaemonRequest {
684 clear_inherited_env: legacy_clear,
685 environment_policy: wire_policy,
686 ..Default::default()
687 };
688 let pipe = SpawnPipeSessionRequest {
689 clear_inherited_env: legacy_clear,
690 environment_policy: wire_policy,
691 ..Default::default()
692 };
693 let pty = SpawnPtySessionRequest {
694 clear_inherited_env: legacy_clear,
695 environment_policy: wire_policy,
696 ..Default::default()
697 };
698 let session = SessionStart {
699 clear_inherited_env: legacy_clear,
700 environment_policy: wire_policy,
701 ..Default::default()
702 };
703
704 assert_eq!(
705 LegacyClearAtTag5::decode(daemon.encode_to_vec().as_slice())
706 .unwrap()
707 .clear_inherited_env,
708 legacy_clear
709 );
710 assert_eq!(
711 LegacyClearAtTag4::decode(pipe.encode_to_vec().as_slice())
712 .unwrap()
713 .clear_inherited_env,
714 legacy_clear
715 );
716 assert_eq!(
717 LegacyClearAtTag4::decode(pty.encode_to_vec().as_slice())
718 .unwrap()
719 .clear_inherited_env,
720 legacy_clear
721 );
722 assert_eq!(
723 LegacyClearAtTag5::decode(session.encode_to_vec().as_slice())
724 .unwrap()
725 .clear_inherited_env,
726 legacy_clear
727 );
728 }
729 }
730
731 #[cfg(feature = "client-async")]
732 #[test]
733 fn tokio_spawn_defaults_to_contained_consoleless_children() {
734 assert_eq!(
735 TokioSpawnOptions::default(),
736 TokioSpawnOptions {
737 kill_on_drop: true,
738 show_console: false,
739 kill_when_owner_dies: false,
740 }
741 );
742 }
743}