1use std::collections::HashMap;
16use std::collections::HashSet;
17use std::collections::VecDeque;
18use std::env::VarError;
19use std::fmt;
20use std::fs::OpenOptions;
21use std::future;
22use std::io;
23use std::io::Write;
24use std::panic::AssertUnwindSafe;
25use std::path::Path;
26use std::path::PathBuf;
27use std::str::FromStr;
28use std::sync::Arc;
29use std::sync::Mutex;
30use std::sync::OnceLock;
31use std::sync::Weak;
32use std::time::Duration;
33use std::time::SystemTime;
34
35use anyhow::Context;
36use async_trait::async_trait;
37use base64::prelude::*;
38use futures::FutureExt;
39use futures::StreamExt;
40use futures::stream;
41use humantime::format_duration;
42use hyperactor::ActorAddr;
43use hyperactor::ActorHandle;
44use hyperactor::ActorRef;
45use hyperactor::Endpoint as _;
46use hyperactor::Gateway;
47use hyperactor::Label;
48use hyperactor::ProcAddr;
49use hyperactor::channel;
50use hyperactor::channel::ChannelAddr;
51use hyperactor::channel::ChannelError;
52use hyperactor::channel::ChannelTransport;
53use hyperactor::channel::Rx;
54use hyperactor::channel::Tx;
55use hyperactor::context;
56use hyperactor::id::Uid;
57use hyperactor::mailbox::IntoBoxedMailboxSender;
58use hyperactor::mailbox::MailboxClient;
59use hyperactor::proc::Proc;
60use hyperactor_cast::cast_actor::CAST_ACTOR_NAME;
61use hyperactor_config::CONFIG;
62use hyperactor_config::ConfigAttr;
63use hyperactor_config::attrs::Attrs;
64use hyperactor_config::attrs::declare_attrs;
65use hyperactor_config::global::override_or_global;
66use serde::Deserialize;
67use serde::Serialize;
68use tempfile::TempDir;
69use tokio::process::Command;
70use tokio::sync::watch;
71use tracing::Instrument;
72use tracing::Level;
73use typeuri::Named;
74
75use crate::config::MESH_PROC_LAUNCHER_KIND;
76use crate::host::BulkTerminate;
77use crate::host::Host;
78use crate::host::HostError;
79use crate::host::HostShutdownHandles;
80use crate::host::ProcHandle;
81use crate::host::ProcManager;
82use crate::host::ReadyError as HostReadyError;
83use crate::host::SingleTerminate;
84use crate::host::TerminateError;
85use crate::host::TerminateSummary;
86use crate::host::WaitError;
87use crate::host::legacy_service_proc_id;
88use crate::host_mesh::host_agent::HOST_MESH_AGENT_ACTOR_NAME;
89use crate::host_mesh::host_agent::HostAgent;
90use crate::logging::OutputTarget;
91use crate::logging::StreamFwder;
92use crate::proc_agent::ProcAgent;
93use crate::proc_launcher::LaunchOptions;
94use crate::proc_launcher::NativeProcLauncher;
95use crate::proc_launcher::ProcExitKind;
96use crate::proc_launcher::ProcExitResult;
97use crate::proc_launcher::ProcLauncher;
98use crate::proc_launcher::ProcLauncherError;
99use crate::proc_launcher::StdioHandling;
100#[cfg(target_os = "linux")]
101use crate::proc_launcher::SystemdProcLauncher;
102use crate::proc_launcher::format_process_name;
103use crate::resource;
104
105mod mailbox;
106
107declare_attrs! {
108 @meta(CONFIG = ConfigAttr::new(
124 Some("HYPERACTOR_MESH_ENABLE_LOG_FORWARDING".to_string()),
125 Some("enable_log_forwarding".to_string()),
126 ))
127 pub attr MESH_ENABLE_LOG_FORWARDING: bool = false;
128
129 @meta(CONFIG = ConfigAttr::new(
149 Some("HYPERACTOR_MESH_ENABLE_FILE_CAPTURE".to_string()),
150 Some("enable_file_capture".to_string()),
151 ))
152 pub attr MESH_ENABLE_FILE_CAPTURE: bool = false;
153
154 @meta(CONFIG = ConfigAttr::new(
158 Some("HYPERACTOR_MESH_TAIL_LOG_LINES".to_string()),
159 Some("tail_log_lines".to_string()),
160 ))
161 pub attr MESH_TAIL_LOG_LINES: usize = 0;
162
163 @meta(CONFIG = ConfigAttr::new(
170 Some("HYPERACTOR_MESH_BOOTSTRAP_ENABLE_PDEATHSIG".to_string()),
171 Some("mesh_bootstrap_enable_pdeathsig".to_string()),
172 ))
173 pub attr MESH_BOOTSTRAP_ENABLE_PDEATHSIG: bool = true;
174
175 @meta(CONFIG = ConfigAttr::new(
180 Some("HYPERACTOR_MESH_TERMINATE_CONCURRENCY".to_string()),
181 Some("mesh_terminate_concurrency".to_string()),
182 ))
183 pub attr MESH_TERMINATE_CONCURRENCY: usize = 16;
184
185 @meta(CONFIG = ConfigAttr::new(
189 Some("HYPERACTOR_MESH_TERMINATE_TIMEOUT".to_string()),
190 Some("mesh_terminate_timeout".to_string()),
191 ))
192 pub attr MESH_TERMINATE_TIMEOUT: Duration = Duration::from_secs(10);
193}
194
195pub const CLIENT_TRACE_ID_ENV: &str = "MONARCH_CLIENT_TRACE_ID";
196
197pub(crate) const BOOTSTRAP_LOG_CHANNEL: &str = "BOOTSTRAP_LOG_CHANNEL";
201
202pub(crate) const BOOTSTRAP_MODE_ENV: &str = "HYPERACTOR_MESH_BOOTSTRAP_MODE";
203pub(crate) const PROCESS_NAME_ENV: &str = "HYPERACTOR_PROCESS_NAME";
204
205#[macro_export]
206macro_rules! ok {
207 ($expr:expr $(,)?) => {
208 match $expr {
209 Ok(value) => value,
210 Err(e) => return ::anyhow::Error::from(e),
211 }
212 };
213}
214
215pub async fn halt<R>() -> R {
216 future::pending::<()>().await;
217 unreachable!()
218}
219
220pub struct HostShutdownHandle {
231 rx: tokio::sync::oneshot::Receiver<HostShutdownHandles>,
232 exit_on_shutdown: bool,
233}
234
235pub struct DrainedHostShutdown {
240 handles: Option<HostShutdownHandles>,
241 exit_on_shutdown: bool,
242}
243
244impl HostShutdownHandle {
245 pub async fn wait_for_drain(self) -> DrainedHostShutdown {
251 DrainedHostShutdown {
252 handles: self.rx.await.ok(),
253 exit_on_shutdown: self.exit_on_shutdown,
254 }
255 }
256
257 pub async fn stop_and_join_after_drain(self, before_join: impl future::Future<Output = ()>) {
260 let drained = self.wait_for_drain().await;
261 before_join.await;
262 drained.stop_and_join().await;
263 }
264
265 pub async fn stop_and_join(self) {
268 self.stop_and_join_after_drain(future::ready(())).await;
269 }
270}
271
272impl DrainedHostShutdown {
273 pub async fn stop_and_join(mut self) {
275 if let Some(handles) = self.handles.take() {
276 handles
277 .stop_and_join("host shutdown: draining transport servers")
278 .await;
279 }
280 if self.exit_on_shutdown {
281 std::process::exit(0);
282 }
283 }
284}
285
286pub async fn host(
320 addr: ProcAddr,
321 command: Option<BootstrapCommand>,
322 config: Option<Attrs>,
323 exit_on_shutdown: bool,
324 listener: Option<std::net::TcpListener>,
325 gateway: Gateway,
326 via: Option<ChannelAddr>,
327) -> anyhow::Result<(ActorHandle<HostAgent>, HostShutdownHandle)> {
328 if let Some(attrs) = config {
329 hyperactor_config::global::set(hyperactor_config::global::Source::Runtime, attrs);
330 tracing::debug!("bootstrap: installed Runtime config snapshot (Host)");
331 } else {
332 tracing::debug!("bootstrap: no config snapshot provided (Host)");
333 }
334
335 let command = match command {
336 Some(command) => command,
337 None => BootstrapCommand::current()?,
338 };
339 let manager = BootstrapProcManager::new(command)?;
340
341 let host = Host::new_with_gateway(manager, addr, listener, gateway, via).await?;
342 let addr = host.addr().clone();
343
344 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<HostShutdownHandles>();
348
349 let system_proc = host.system_proc().clone();
352 let host_mesh_agent = system_proc.spawn_bound_with_uid(
353 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
354 HostAgent::new_process(host, Some(shutdown_tx)),
355 )?;
356 HostAgent::wait_initialized(&host_mesh_agent).await?;
357
358 system_proc.spawn_bound_with_uid(
359 Uid::singleton(Label::strip(CAST_ACTOR_NAME)),
360 hyperactor_cast::cast_actor::CastActor::default(),
361 )?;
362
363 tracing::info!(
364 "serving host at {}, agent: {}",
365 addr,
366 host_mesh_agent.bind::<HostAgent>()
367 );
368
369 Ok((
370 host_mesh_agent,
371 HostShutdownHandle {
372 rx: shutdown_rx,
373 exit_on_shutdown,
374 },
375 ))
376}
377
378#[derive(Clone, Debug, Serialize, Deserialize)]
387pub enum Bootstrap {
388 Proc {
390 proc_id: ProcAddr,
392 backend_addr: ChannelAddr,
395 callback_addr: ChannelAddr,
397 socket_dir_path: PathBuf,
401 config: Option<Attrs>,
406 },
407
408 Host {
411 addr: ChannelAddr,
413 callback_addr: ChannelAddr,
416 command: Option<BootstrapCommand>,
419 config: Option<Attrs>,
424 exit_on_shutdown: bool,
426 },
427}
428
429pub struct HostBootstrapReady {
431 host_addr: ChannelAddr,
432 callback_addr: ChannelAddr,
433 callback_rx: hyperactor::channel::ChannelRx<()>,
434}
435
436impl HostBootstrapReady {
437 pub fn new(host_addr: ChannelAddr) -> Result<Self, ChannelError> {
438 let (callback_addr, callback_rx) =
439 channel::serve(ChannelAddr::any(ChannelTransport::Unix))?;
440 Ok(Self {
441 host_addr,
442 callback_addr,
443 callback_rx,
444 })
445 }
446
447 pub fn callback_addr(&self) -> ChannelAddr {
448 self.callback_addr.clone()
449 }
450
451 pub async fn wait(mut self) -> anyhow::Result<()> {
452 let timeout = hyperactor_config::global::get(hyperactor::config::HOST_SPAWN_READY_TIMEOUT);
453 if timeout.is_zero() {
454 self.callback_rx.recv().await.with_context(|| {
455 format!("host {} closed its bootstrap callback", self.host_addr)
456 })?;
457 } else {
458 tokio::time::timeout(timeout, self.callback_rx.recv())
459 .await
460 .with_context(|| {
461 format!(
462 "host {} did not become ready within {:?}",
463 self.host_addr, timeout
464 )
465 })?
466 .with_context(|| {
467 format!("host {} closed its bootstrap callback", self.host_addr)
468 })?;
469 }
470 Ok(())
471 }
472}
473
474impl Bootstrap {
475 #[allow(clippy::result_large_err)]
478 pub(crate) fn to_env_safe_string(&self) -> crate::Result<String> {
479 Ok(BASE64_STANDARD.encode(serde_json::to_string(&self)?))
480 }
481
482 #[allow(clippy::result_large_err)]
484 pub(crate) fn from_env_safe_string(str: &str) -> crate::Result<Self> {
485 let data = BASE64_STANDARD.decode(str)?;
486 let data = std::str::from_utf8(&data)?;
487 Ok(serde_json::from_str(data)?)
488 }
489
490 pub fn get_from_env() -> anyhow::Result<Option<Self>> {
493 match std::env::var("HYPERACTOR_MESH_BOOTSTRAP_MODE") {
494 Ok(mode) => match Bootstrap::from_env_safe_string(&mode) {
495 Ok(mode) => Ok(Some(mode)),
496 Err(e) => {
497 Err(anyhow::Error::from(e).context("parsing HYPERACTOR_MESH_BOOTSTRAP_MODE"))
498 }
499 },
500 Err(VarError::NotPresent) => Ok(None),
501 Err(e) => Err(anyhow::Error::from(e).context("reading HYPERACTOR_MESH_BOOTSTRAP_MODE")),
502 }
503 }
504
505 pub fn to_env(&self, cmd: &mut Command) {
507 cmd.env(
508 "HYPERACTOR_MESH_BOOTSTRAP_MODE",
509 self.to_env_safe_string().unwrap(),
510 );
511 }
512
513 pub async fn bootstrap(self) -> anyhow::Result<i32> {
517 tracing::info!(
518 "bootstrapping mesh process: {}",
519 serde_json::to_string(&self).unwrap()
520 );
521
522 if Debug::is_active() {
523 let mut buf = Vec::new();
524 writeln!(&mut buf, "bootstrapping {}:", std::process::id()).unwrap();
525 #[cfg(unix)]
526 writeln!(
527 &mut buf,
528 "\tparent pid: {}",
529 std::os::unix::process::parent_id()
530 )
531 .unwrap();
532 writeln!(
533 &mut buf,
534 "\tconfig: {}",
535 serde_json::to_string(&self).unwrap()
536 )
537 .unwrap();
538 match std::env::current_exe() {
539 Ok(path) => writeln!(&mut buf, "\tcurrent_exe: {}", path.display()).unwrap(),
540 Err(e) => writeln!(&mut buf, "\tcurrent_exe: error<{}>", e).unwrap(),
541 }
542 writeln!(&mut buf, "\targs:").unwrap();
543 for arg in std::env::args() {
544 writeln!(&mut buf, "\t\t{}", arg).unwrap();
545 }
546 writeln!(&mut buf, "\tenv:").unwrap();
547 for (key, val) in std::env::vars() {
548 writeln!(&mut buf, "\t\t{}={}", key, val).unwrap();
549 }
550 let _ = Debug.write(&buf);
551 if let Ok(s) = std::str::from_utf8(&buf) {
552 tracing::info!("{}", s);
553 } else {
554 tracing::info!("{:?}", buf);
555 }
556 }
557
558 match self {
559 Bootstrap::Proc {
560 proc_id,
561 backend_addr,
562 callback_addr,
563 socket_dir_path,
564 config,
565 } => {
566 let entered = tracing::span!(
567 Level::INFO,
568 "proc_bootstrap",
569 %proc_id,
570 %backend_addr,
571 %callback_addr,
572 socket_dir_path = %socket_dir_path.display(),
573 )
574 .entered();
575 if let Some(attrs) = config {
576 hyperactor_config::global::set(
577 hyperactor_config::global::Source::ClientOverride,
578 attrs,
579 );
580 tracing::debug!("bootstrap: installed ClientOverride config snapshot (Proc)");
581 } else {
582 tracing::debug!("bootstrap: no config snapshot provided (Proc)");
583 }
584
585 if hyperactor_config::global::get(MESH_BOOTSTRAP_ENABLE_PDEATHSIG) {
586 let _ = install_pdeathsig_kill();
591 } else {
592 eprintln!("(bootstrap) PDEATHSIG disabled via config");
593 }
594
595 let local_addr = proc_id.addr().clone();
596 let (serve_addr, _) = local_proc_addr(&socket_dir_path, proc_id.id())?;
597
598 let proc_sender = mailbox::LocalProcDialer::new(
603 local_addr.clone(),
604 socket_dir_path,
605 MailboxClient::dial(backend_addr)?,
606 );
607
608 let proc = Proc::configured(proc_id.clone(), proc_sender.into_boxed());
609
610 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<i32>();
611 let agent_handle = ProcAgent::boot_v1(proc.clone(), Some(shutdown_tx))
612 .map_err(|e| HostError::AgentSpawnFailure(proc_id, e))?;
613
614 let span = entered.exit();
615
616 let (proc_addr, proc_rx) = channel::serve(serve_addr)?;
619 let mailbox_handle =
620 hyperactor::mailbox::MailboxServer::serve(proc.clone(), proc_rx);
621 channel::dial(callback_addr)?
622 .send((proc_addr, agent_handle.bind::<ProcAgent>()))
623 .instrument(span)
624 .await
625 .map_err(ChannelError::from)?;
626
627 let exit_code = shutdown_rx.await.unwrap_or(1);
630 mailbox_handle.stop("process shutting down");
631 let _ = mailbox_handle.await;
632 tracing::info!("bootstrap shutting down with exit code {}", exit_code);
633 Ok(exit_code)
636 }
637 Bootstrap::Host {
638 addr,
639 callback_addr,
640 command,
641 config,
642 exit_on_shutdown,
643 } => {
644 let (_agent_handle, shutdown) = host(
645 ProcAddr::new(legacy_service_proc_id(), addr.into()),
646 command,
647 config,
648 exit_on_shutdown,
649 None,
650 Gateway::global().clone(),
651 None,
652 )
653 .await?;
654 channel::dial(callback_addr)?
655 .send(())
656 .await
657 .map_err(ChannelError::from)?;
658 shutdown.stop_and_join().await;
659 halt().await
660 }
661 }
662 }
663
664 pub async fn bootstrap_or_die(self) -> ! {
667 let exit_code = match self.bootstrap().await {
668 Ok(exit_code) => exit_code,
669 Err(err) => {
670 tracing::error!("failed to bootstrap mesh process: {}", err);
671 1
672 }
673 };
674 std::process::exit(exit_code);
675 }
676}
677
678pub fn install_pdeathsig_kill() -> io::Result<()> {
680 #[cfg(target_os = "linux")]
681 {
682 let ppid_before = unsafe { libc::getppid() };
685
686 let rc = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_int) };
689 if rc != 0 {
690 return Err(io::Error::last_os_error());
691 }
692
693 let ppid_after = unsafe { libc::getppid() };
703 if ppid_before != ppid_after {
704 std::process::exit(0);
705 }
706 }
707 Ok(())
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
725pub enum ProcStatus {
726 Starting,
729 Running { started_at: SystemTime },
732 Ready {
735 started_at: SystemTime,
736 addr: ChannelAddr,
737 agent: ActorRef<ProcAgent>,
738 },
739 Stopping { started_at: SystemTime },
743 Stopped {
746 exit_code: i32,
747 stderr_tail: Vec<String>,
748 },
749 Killed { signal: i32, core_dumped: bool },
752 Failed { reason: String },
756}
757
758impl ProcStatus {
759 #[inline]
763 pub fn is_exit(&self) -> bool {
764 matches!(
765 self,
766 ProcStatus::Stopped { .. } | ProcStatus::Killed { .. } | ProcStatus::Failed { .. }
767 )
768 }
769}
770
771impl std::fmt::Display for ProcStatus {
772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773 match self {
774 ProcStatus::Starting => write!(f, "Starting"),
775 ProcStatus::Running { started_at } => {
776 let uptime = started_at
777 .elapsed()
778 .map(|d| format!(" up {}", format_duration(d)))
779 .unwrap_or_default();
780 write!(f, "Running{uptime}")
781 }
782 ProcStatus::Ready {
783 started_at, addr, ..
784 } => {
785 let uptime = started_at
786 .elapsed()
787 .map(|d| format!(" up {}", format_duration(d)))
788 .unwrap_or_default();
789 write!(f, "Ready at {addr}{uptime}")
790 }
791 ProcStatus::Stopping { started_at } => {
792 let uptime = started_at
793 .elapsed()
794 .map(|d| format!(" up {}", format_duration(d)))
795 .unwrap_or_default();
796 write!(f, "Stopping{uptime}")
797 }
798 ProcStatus::Stopped { exit_code, .. } => write!(f, "Stopped(exit={exit_code})"),
799 ProcStatus::Killed {
800 signal,
801 core_dumped,
802 } => {
803 if *core_dumped {
804 write!(f, "Killed(sig={signal}, core)")
805 } else {
806 write!(f, "Killed(sig={signal})")
807 }
808 }
809 ProcStatus::Failed { reason } => write!(f, "Failed({reason})"),
810 }
811 }
812}
813
814#[derive(Debug, Clone)]
816pub enum ReadyError {
817 Terminal(ProcStatus),
819 ChannelClosed,
821}
822
823impl std::fmt::Display for ReadyError {
824 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825 match self {
826 ReadyError::Terminal(st) => write!(f, "proc terminated before running: {st:?}"),
827 ReadyError::ChannelClosed => write!(f, "status channel closed"),
828 }
829 }
830}
831impl std::error::Error for ReadyError {}
832
833#[derive(Clone)]
872pub struct BootstrapProcHandle {
873 proc_id: ProcAddr,
875
876 status: Arc<std::sync::Mutex<ProcStatus>>,
882
883 launcher: Weak<dyn ProcLauncher>,
892
893 stdout_fwder: Arc<std::sync::Mutex<Option<StreamFwder>>>,
898
899 stderr_fwder: Arc<std::sync::Mutex<Option<StreamFwder>>>,
902
903 tx: tokio::sync::watch::Sender<ProcStatus>,
908
909 rx: tokio::sync::watch::Receiver<ProcStatus>,
913}
914
915impl fmt::Debug for BootstrapProcHandle {
916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917 let status = self.status.lock().expect("status mutex poisoned").clone();
918 f.debug_struct("BootstrapProcHandle")
919 .field("proc_id", &self.proc_id)
920 .field("status", &status)
921 .field("launcher", &"<dyn ProcLauncher>")
922 .field("tx", &"<watch::Sender>")
923 .field("rx", &"<watch::Receiver>")
924 .finish()
927 }
928}
929
930impl BootstrapProcHandle {
932 pub(crate) fn new(proc_id: ProcAddr, launcher: Weak<dyn ProcLauncher>) -> Self {
943 let (tx, rx) = watch::channel(ProcStatus::Starting);
944 Self {
945 proc_id,
946 status: Arc::new(std::sync::Mutex::new(ProcStatus::Starting)),
947 launcher,
948 stdout_fwder: Arc::new(std::sync::Mutex::new(None)),
949 stderr_fwder: Arc::new(std::sync::Mutex::new(None)),
950 tx,
951 rx,
952 }
953 }
954
955 #[inline]
957 pub fn proc_addr(&self) -> &ProcAddr {
958 &self.proc_id
959 }
960
961 #[inline]
974 pub fn watch(&self) -> tokio::sync::watch::Receiver<ProcStatus> {
975 self.rx.clone()
976 }
977
978 #[inline]
996 pub async fn changed(&self) {
997 let _ = self.watch().changed().await;
998 }
999
1000 #[must_use]
1013 pub fn status(&self) -> ProcStatus {
1014 self.status.lock().expect("status mutex poisoned").clone()
1018 }
1019
1020 fn transition<F>(&self, f: F) -> bool
1026 where
1027 F: FnOnce(&mut ProcStatus) -> bool,
1028 {
1029 let mut guard = self.status.lock().expect("status mutex poisoned");
1030 let _before = guard.clone();
1031 let changed = f(&mut guard);
1032 if changed {
1033 let _ = self.tx.send(guard.clone());
1035 }
1036 changed
1037 }
1038
1039 pub(crate) fn mark_running(&self, started_at: SystemTime) -> bool {
1049 self.transition(|st| match *st {
1050 ProcStatus::Starting => {
1051 *st = ProcStatus::Running { started_at };
1052 true
1053 }
1054 _ => {
1055 tracing::warn!(
1056 "illegal transition: {:?} -> Running; leaving status unchanged",
1057 *st
1058 );
1059 false
1060 }
1061 })
1062 }
1063
1064 pub(crate) fn mark_ready(&self, addr: ChannelAddr, agent: ActorRef<ProcAgent>) -> bool {
1076 tracing::info!(proc_id = %self.proc_id, %addr, "{} ready at {}", self.proc_id, addr);
1077 self.transition(|st| match st {
1078 ProcStatus::Starting => {
1079 *st = ProcStatus::Ready {
1082 started_at: std::time::SystemTime::now(),
1083 addr,
1084 agent,
1085 };
1086 true
1087 }
1088 ProcStatus::Running { started_at } => {
1089 let started_at = *started_at;
1090 *st = ProcStatus::Ready {
1091 started_at,
1092 addr,
1093 agent,
1094 };
1095 true
1096 }
1097 _ => {
1098 tracing::warn!(
1099 "illegal transition: {:?} -> Ready; leaving status unchanged",
1100 st
1101 );
1102 false
1103 }
1104 })
1105 }
1106
1107 pub(crate) fn mark_stopping(&self) -> bool {
1111 let now = std::time::SystemTime::now();
1112
1113 self.transition(|st| match *st {
1114 ProcStatus::Running { started_at } => {
1115 *st = ProcStatus::Stopping { started_at };
1116 true
1117 }
1118 ProcStatus::Ready { started_at, .. } => {
1119 *st = ProcStatus::Stopping { started_at };
1120 true
1121 }
1122 ProcStatus::Starting => {
1123 *st = ProcStatus::Stopping { started_at: now };
1124 true
1125 }
1126 _ => false,
1127 })
1128 }
1129
1130 pub(crate) fn mark_stopped(&self, exit_code: i32, stderr_tail: Vec<String>) -> bool {
1133 self.transition(|st| match *st {
1134 ProcStatus::Starting
1135 | ProcStatus::Running { .. }
1136 | ProcStatus::Ready { .. }
1137 | ProcStatus::Stopping { .. } => {
1138 *st = ProcStatus::Stopped {
1139 exit_code,
1140 stderr_tail,
1141 };
1142 true
1143 }
1144 _ => {
1145 tracing::warn!(
1146 "illegal transition: {:?} -> Stopped; leaving status unchanged",
1147 *st
1148 );
1149 false
1150 }
1151 })
1152 }
1153
1154 pub(crate) fn mark_killed(&self, signal: i32, core_dumped: bool) -> bool {
1157 self.transition(|st| match *st {
1158 ProcStatus::Starting
1159 | ProcStatus::Running { .. }
1160 | ProcStatus::Ready { .. }
1161 | ProcStatus::Stopping { .. } => {
1162 *st = ProcStatus::Killed {
1163 signal,
1164 core_dumped,
1165 };
1166 true
1167 }
1168 _ => {
1169 tracing::warn!(
1170 "illegal transition: {:?} -> Killed; leaving status unchanged",
1171 *st
1172 );
1173 false
1174 }
1175 })
1176 }
1177
1178 pub(crate) fn mark_failed<S: Into<String>>(&self, reason: S) -> bool {
1181 self.transition(|st| match *st {
1182 ProcStatus::Starting
1183 | ProcStatus::Running { .. }
1184 | ProcStatus::Ready { .. }
1185 | ProcStatus::Stopping { .. } => {
1186 *st = ProcStatus::Failed {
1187 reason: reason.into(),
1188 };
1189 true
1190 }
1191 _ => {
1192 tracing::warn!(
1193 "illegal transition: {:?} -> Failed; leaving status unchanged",
1194 *st
1195 );
1196 false
1197 }
1198 })
1199 }
1200
1201 #[must_use]
1220 pub async fn wait_inner(&self) -> ProcStatus {
1221 let mut rx = self.watch();
1222 loop {
1223 let st = rx.borrow().clone();
1224 if st.is_exit() {
1225 return st;
1226 }
1227 if rx.changed().await.is_err() {
1229 return st;
1230 }
1231 }
1232 }
1233
1234 pub async fn ready_inner(&self) -> Result<(), ReadyError> {
1253 let mut rx = self.watch();
1254 loop {
1255 let st = rx.borrow().clone();
1256 match &st {
1257 ProcStatus::Ready { .. } => return Ok(()),
1258 s if s.is_exit() => return Err(ReadyError::Terminal(st)),
1259 _non_terminal => {
1260 if rx.changed().await.is_err() {
1261 return Err(ReadyError::ChannelClosed);
1262 }
1263 }
1264 }
1265 }
1266 }
1267
1268 pub fn set_stream_monitors(&self, out: Option<StreamFwder>, err: Option<StreamFwder>) {
1269 *self
1270 .stdout_fwder
1271 .lock()
1272 .expect("stdout_tailer mutex poisoned") = out;
1273 *self
1274 .stderr_fwder
1275 .lock()
1276 .expect("stderr_tailer mutex poisoned") = err;
1277 }
1278
1279 fn take_stream_monitors(&self) -> (Option<StreamFwder>, Option<StreamFwder>) {
1280 let out = self
1281 .stdout_fwder
1282 .lock()
1283 .expect("stdout_tailer mutex poisoned")
1284 .take();
1285 let err = self
1286 .stderr_fwder
1287 .lock()
1288 .expect("stderr_tailer mutex poisoned")
1289 .take();
1290 (out, err)
1291 }
1292
1293 pub(crate) async fn wait_or_brutally_kill(&self, timeout: Duration) {
1300 match tokio::time::timeout(timeout, self.wait_inner()).await {
1301 Ok(st) if st.is_exit() => return,
1302 _ => {}
1303 }
1304
1305 let _ = self.mark_stopping();
1306
1307 if let Some(launcher) = self.launcher.upgrade() {
1308 let ref_proc_id: ProcAddr = self.proc_id.clone();
1309 if let Err(e) = launcher.terminate(&ref_proc_id, timeout).await {
1310 tracing::warn!(
1311 proc_id = %self.proc_id,
1312 error = %e,
1313 "wait_or_brutally_kill: launcher terminate failed, trying kill"
1314 );
1315 let _ = launcher.kill(&ref_proc_id).await;
1316 }
1317 }
1318
1319 let _ = self.wait_inner().await;
1320 }
1321
1322 async fn send_stop_all(
1326 &self,
1327 cx: &impl context::Actor,
1328 agent: ActorRef<ProcAgent>,
1329 timeout: Duration,
1330 reason: &str,
1331 ) -> anyhow::Result<ProcStatus> {
1332 let mut agent_port = agent.port();
1339 agent_port.return_undeliverable(false);
1340 agent_port.post(
1341 cx,
1342 resource::StopAll {
1343 reason: reason.to_string(),
1344 },
1345 );
1346 match tokio::time::timeout(timeout, self.wait()).await {
1349 Ok(Ok(st)) => Ok(st),
1350 Ok(Err(e)) => Err(anyhow::anyhow!("agent did not exit the process: {:?}", e)),
1351 Err(_) => Err(anyhow::anyhow!("agent did not exit the process in time")),
1352 }
1353 }
1354}
1355
1356#[async_trait]
1357impl ProcHandle for BootstrapProcHandle {
1358 type Agent = ProcAgent;
1359 type TerminalStatus = ProcStatus;
1360
1361 #[inline]
1362 fn proc_addr(&self) -> &ProcAddr {
1363 &self.proc_id
1364 }
1365
1366 #[inline]
1367 fn addr(&self) -> Option<ChannelAddr> {
1368 match &*self.status.lock().expect("status mutex poisoned") {
1369 ProcStatus::Ready { addr, .. } => Some(addr.clone()),
1370 _ => None,
1371 }
1372 }
1373
1374 #[inline]
1375 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>> {
1376 match &*self.status.lock().expect("status mutex poisoned") {
1377 ProcStatus::Ready { agent, .. } => Some(agent.clone()),
1378 _ => None,
1379 }
1380 }
1381
1382 async fn ready(&self) -> Result<(), HostReadyError<Self::TerminalStatus>> {
1393 match self.ready_inner().await {
1394 Ok(()) => Ok(()),
1395 Err(ReadyError::Terminal(status)) => Err(HostReadyError::Terminal(status)),
1396 Err(ReadyError::ChannelClosed) => Err(HostReadyError::ChannelClosed),
1397 }
1398 }
1399
1400 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError> {
1408 let status = self.wait_inner().await;
1409 if status.is_exit() {
1410 Ok(status)
1411 } else {
1412 Err(WaitError::ChannelClosed)
1413 }
1414 }
1415
1416 async fn terminate(
1437 &self,
1438 cx: &impl context::Actor,
1439 timeout: Duration,
1440 reason: &str,
1441 ) -> Result<ProcStatus, TerminateError<Self::TerminalStatus>> {
1442 let st0 = self.status();
1444 if st0.is_exit() {
1445 tracing::debug!(?st0, "terminate(): already terminal");
1446 return Err(TerminateError::AlreadyTerminated(st0));
1447 }
1448
1449 let agent = self.agent_ref();
1452 if let Some(agent) = agent {
1453 match self.send_stop_all(cx, agent.clone(), timeout, reason).await {
1454 Ok(st) => return Ok(st),
1455 Err(e) => {
1456 tracing::warn!(
1458 "ProcAgent {} could not successfully stop all actors: {}",
1459 agent.actor_addr(),
1460 e,
1461 );
1462 }
1463 }
1464 }
1465
1466 let _ = self.mark_stopping();
1468
1469 tracing::info!(proc_id = %self.proc_id, ?timeout, "terminate(): delegating to launcher");
1471 let ref_proc_id: ProcAddr = self.proc_id.clone();
1472 if let Some(launcher) = self.launcher.upgrade() {
1473 if let Err(e) = launcher.terminate(&ref_proc_id, timeout).await {
1474 tracing::warn!(proc_id = %self.proc_id, error=%e, "terminate(): launcher termination failed");
1475 return Err(TerminateError::Io(anyhow::anyhow!(
1476 "launcher termination failed: {}",
1477 e
1478 )));
1479 }
1480 } else {
1481 tracing::debug!(proc_id = %self.proc_id, "terminate(): launcher gone, proc cleanup in progress");
1483 }
1484
1485 let st = self.wait_inner().await;
1487 if st.is_exit() {
1488 tracing::info!(proc_id = %self.proc_id, ?st, "terminate(): exited");
1489 Ok(st)
1490 } else {
1491 Err(TerminateError::ChannelClosed)
1492 }
1493 }
1494
1495 async fn kill(&self) -> Result<ProcStatus, TerminateError<Self::TerminalStatus>> {
1513 let st0 = self.status();
1515 if st0.is_exit() {
1516 return Err(TerminateError::AlreadyTerminated(st0));
1517 }
1518
1519 tracing::info!(proc_id = %self.proc_id, "kill(): delegating to launcher");
1521 let ref_proc_id: ProcAddr = self.proc_id.clone();
1522 if let Some(launcher) = self.launcher.upgrade() {
1523 if let Err(e) = launcher.kill(&ref_proc_id).await {
1524 tracing::warn!(proc_id = %self.proc_id, error=%e, "kill(): launcher kill failed");
1525 return Err(TerminateError::Io(anyhow::anyhow!(
1526 "launcher kill failed: {}",
1527 e
1528 )));
1529 }
1530 } else {
1531 tracing::debug!(proc_id = %self.proc_id, "kill(): launcher gone, proc cleanup in progress");
1533 }
1534
1535 let st = self.wait_inner().await;
1537 if st.is_exit() {
1538 Ok(st)
1539 } else {
1540 Err(TerminateError::ChannelClosed)
1541 }
1542 }
1543}
1544
1545#[derive(Debug, Named, Serialize, Deserialize, Clone)]
1547pub struct BootstrapCommand {
1548 pub program: PathBuf,
1549 pub arg0: Option<String>,
1550 pub args: Vec<String>,
1551 pub env: HashMap<String, String>,
1557 pub env_remove: HashSet<String>,
1560 pub inherit_env: bool,
1563}
1564wirevalue::register_type!(BootstrapCommand);
1565
1566impl Default for BootstrapCommand {
1567 fn default() -> Self {
1568 Self {
1569 program: PathBuf::new(),
1570 arg0: None,
1571 args: vec![],
1572 env: HashMap::new(),
1573 env_remove: HashSet::new(),
1574 inherit_env: true,
1575 }
1576 }
1577}
1578
1579impl std::hash::Hash for BootstrapCommand {
1580 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1581 self.program.hash(state);
1582 self.arg0.hash(state);
1583 self.args.hash(state);
1584 let mut pairs: Vec<_> = self.env.iter().collect();
1585 pairs.sort();
1586 pairs.hash(state);
1587 let mut removed: Vec<_> = self.env_remove.iter().collect();
1588 removed.sort();
1589 removed.hash(state);
1590 self.inherit_env.hash(state);
1591 }
1592}
1593
1594impl PartialEq for BootstrapCommand {
1595 fn eq(&self, other: &Self) -> bool {
1596 self.program == other.program
1597 && self.arg0 == other.arg0
1598 && self.args == other.args
1599 && self.env == other.env
1600 && self.env_remove == other.env_remove
1601 && self.inherit_env == other.inherit_env
1602 }
1603}
1604
1605impl Eq for BootstrapCommand {}
1606
1607impl BootstrapCommand {
1608 pub fn current() -> io::Result<Self> {
1611 let mut args: VecDeque<String> = std::env::args().collect();
1612 let arg0 = args.pop_front();
1613
1614 Ok(Self {
1615 program: std::env::current_exe()?,
1616 arg0,
1617 args: args.into(),
1618 ..Default::default()
1619 })
1620 }
1621
1622 pub fn new(&self) -> Command {
1625 let mut cmd = Command::new(&self.program);
1626 if let Some(arg0) = &self.arg0 {
1627 cmd.arg0(arg0);
1628 }
1629 for arg in &self.args {
1630 cmd.arg(arg);
1631 }
1632 if !self.inherit_env {
1633 cmd.env_clear();
1634 }
1635 for k in &self.env_remove {
1636 cmd.env_remove(k);
1637 }
1638 for (k, v) in &self.env {
1639 cmd.env(k, v);
1640 }
1641 cmd
1642 }
1643
1644 #[cfg(test)]
1651 #[cfg(fbcode_build)]
1652 pub(crate) fn test() -> Self {
1653 Self {
1654 program: crate::testresource::get("monarch/hyperactor_mesh/bootstrap"),
1655 ..Default::default()
1656 }
1657 }
1658}
1659
1660impl<T: Into<PathBuf>> From<T> for BootstrapCommand {
1661 fn from(s: T) -> Self {
1663 Self {
1664 program: s.into(),
1665 ..Default::default()
1666 }
1667 }
1668}
1669
1670#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1693pub(crate) enum LauncherKind {
1694 Native,
1697 #[cfg(target_os = "linux")]
1700 Systemd,
1701}
1702
1703impl FromStr for LauncherKind {
1704 type Err = io::Error;
1705
1706 fn from_str(s: &str) -> Result<Self, Self::Err> {
1715 match s.trim().to_ascii_lowercase().as_str() {
1716 "" | "native" => Ok(Self::Native),
1717 #[cfg(target_os = "linux")]
1718 "systemd" => Ok(Self::Systemd),
1719 other => Err(io::Error::new(
1720 io::ErrorKind::InvalidInput,
1721 format!(
1722 "unknown proc launcher kind {other:?}; expected 'native'{}",
1723 if cfg!(target_os = "linux") {
1724 " or 'systemd'"
1725 } else {
1726 ""
1727 }
1728 ),
1729 )),
1730 }
1731 }
1732}
1733
1734pub struct BootstrapProcManager {
1763 launcher: OnceLock<Arc<dyn ProcLauncher>>,
1766
1767 command: BootstrapCommand,
1769
1770 children: Arc<tokio::sync::Mutex<HashMap<ProcAddr, BootstrapProcHandle>>>,
1774
1775 file_appender: Option<Arc<crate::logging::FileAppender>>,
1778
1779 socket_dir: TempDir,
1783}
1784
1785impl BootstrapProcManager {
1786 pub(crate) fn new(command: BootstrapCommand) -> Result<Self, io::Error> {
1793 let file_appender = if hyperactor_config::global::get(MESH_ENABLE_FILE_CAPTURE) {
1794 match crate::logging::FileAppender::new() {
1795 Some(fm) => {
1796 tracing::info!("file appender created successfully");
1797 Some(Arc::new(fm))
1798 }
1799 None => {
1800 tracing::warn!("failed to create file appender");
1801 None
1802 }
1803 }
1804 } else {
1805 None
1806 };
1807
1808 Ok(Self {
1809 launcher: OnceLock::new(),
1810 command,
1811 children: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
1812 file_appender,
1813 socket_dir: runtime_dir()?,
1814 })
1815 }
1816
1817 pub fn set_launcher(&self, launcher: Arc<dyn ProcLauncher>) -> Result<(), ProcLauncherError> {
1826 self.launcher.set(launcher).map_err(|_| {
1827 ProcLauncherError::Other(
1828 "launcher already initialized; call set_proc_launcher before first spawn".into(),
1829 )
1830 })
1831 }
1832
1833 pub fn launcher(&self) -> &Arc<dyn ProcLauncher> {
1839 self.launcher.get_or_init(|| {
1840 let kind_str = hyperactor_config::global::get_cloned(MESH_PROC_LAUNCHER_KIND);
1841 let kind: LauncherKind = kind_str.parse().unwrap_or(LauncherKind::Native);
1842 tracing::info!(kind = ?kind, config_value = %kind_str, "using default proc launcher");
1843 match kind {
1844 LauncherKind::Native => Arc::new(NativeProcLauncher::new()),
1845 #[cfg(target_os = "linux")]
1846 LauncherKind::Systemd => Arc::new(SystemdProcLauncher::new()),
1847 }
1848 })
1849 }
1850
1851 pub fn command(&self) -> &BootstrapCommand {
1853 &self.command
1854 }
1855
1856 pub fn socket_dir(&self) -> &Path {
1858 self.socket_dir.path()
1859 }
1860
1861 pub async fn status(&self, proc_id: &ProcAddr) -> Option<ProcStatus> {
1872 self.children.lock().await.get(proc_id).map(|h| h.status())
1873 }
1874
1875 pub async fn watch(
1878 &self,
1879 proc_id: &ProcAddr,
1880 ) -> Option<tokio::sync::watch::Receiver<ProcStatus>> {
1881 self.children.lock().await.get(proc_id).map(|h| h.watch())
1882 }
1883
1884 pub(crate) async fn request_stop(
1891 &self,
1892 cx: &impl context::Actor,
1893 proc: &ProcAddr,
1894 timeout: Duration,
1895 reason: &str,
1896 ) {
1897 let handle = {
1898 let guard = self.children.lock().await;
1899 guard.get(proc).cloned()
1900 };
1901
1902 let Some(handle) = handle else { return };
1903
1904 let status = handle.status();
1905 if status.is_exit() || matches!(status, ProcStatus::Stopping { .. }) {
1906 return;
1907 }
1908
1909 if let Some(agent) = handle.agent_ref() {
1910 let mut agent_port = agent.port();
1911 agent_port.return_undeliverable(false);
1912 let _ = agent_port.post(
1913 cx,
1914 resource::StopAll {
1915 reason: reason.to_string(),
1916 },
1917 );
1918 }
1919
1920 let _ = handle.mark_stopping();
1921 tokio::spawn(async move {
1922 handle.wait_or_brutally_kill(timeout).await;
1923 });
1924 }
1925
1926 fn spawn_exit_monitor(
1927 &self,
1928 proc_id: ProcAddr,
1929 handle: BootstrapProcHandle,
1930 exit_rx: tokio::sync::oneshot::Receiver<ProcExitResult>,
1931 ) {
1932 tokio::spawn(async move {
1933 let exit_result = match exit_rx.await {
1935 Ok(res) => res,
1936 Err(_) => {
1937 let _ = handle.mark_failed("exit_rx sender dropped unexpectedly");
1939 tracing::error!(
1940 name = "ProcStatus",
1941 status = "Exited::ChannelDropped",
1942 %proc_id,
1943 "exit channel closed without result"
1944 );
1945 return;
1946 }
1947 };
1948
1949 let mut stderr_tail: Vec<String> = Vec::new();
1953 let (stdout_mon, stderr_mon) = handle.take_stream_monitors();
1954
1955 if let Some(t) = stderr_mon {
1956 let (lines, _bytes) = t.abort().await;
1957 stderr_tail = lines;
1958 }
1959 if let Some(t) = stdout_mon {
1960 let (_lines, _bytes) = t.abort().await;
1961 }
1962
1963 if stderr_tail.is_empty()
1965 && let Some(tail) = exit_result.stderr_tail
1966 {
1967 stderr_tail = tail;
1968 }
1969
1970 let tail_str = if stderr_tail.is_empty() {
1971 None
1972 } else {
1973 Some(stderr_tail.join("\n"))
1974 };
1975
1976 match exit_result.kind {
1977 ProcExitKind::Exited { code } => {
1978 let _ = handle.mark_stopped(code, stderr_tail);
1979 tracing::info!(
1980 name = "ProcStatus",
1981 status = "Exited::ExitWithCode",
1982 %proc_id,
1983 exit_code = code,
1984 tail = tail_str,
1985 "proc exited with code {code}"
1986 );
1987 }
1988 ProcExitKind::Signaled {
1989 signal,
1990 core_dumped,
1991 } => {
1992 let _ = handle.mark_killed(signal, core_dumped);
1993 tracing::info!(
1994 name = "ProcStatus",
1995 status = "Exited::KilledBySignal",
1996 %proc_id,
1997 tail = tail_str,
1998 "killed by signal {signal}"
1999 );
2000 }
2001 ProcExitKind::Failed { reason } => {
2002 let _ = handle.mark_failed(&reason);
2003 tracing::info!(
2004 name = "ProcStatus",
2005 status = "Exited::Failed",
2006 %proc_id,
2007 tail = tail_str,
2008 "proc failed: {reason}"
2009 );
2010 }
2011 }
2012 });
2013 }
2014}
2015
2016pub use crate::proc_launcher::ProcBind;
2017
2018pub struct BootstrapProcConfig {
2020 pub create_rank: usize,
2022
2023 pub client_config_override: Attrs,
2026
2027 pub proc_bind: Option<ProcBind>,
2031 pub bootstrap_command: Option<BootstrapCommand>,
2034}
2035
2036struct LaunchCleanup {
2037 launcher: Arc<dyn ProcLauncher>,
2038 proc_id: Option<ProcAddr>,
2039}
2040
2041impl LaunchCleanup {
2042 fn disarm(mut self) {
2043 self.proc_id = None;
2044 }
2045}
2046
2047impl Drop for LaunchCleanup {
2048 fn drop(&mut self) {
2049 let Some(proc_id) = self.proc_id.take() else {
2050 return;
2051 };
2052 let launcher = Arc::clone(&self.launcher);
2053
2054 tokio::spawn(async move {
2055 if let Err(error) = launcher.kill(&proc_id).await {
2056 tracing::warn!(%proc_id, %error, "failed to clean up interrupted proc launch");
2057 }
2058 });
2059 }
2060}
2061
2062#[async_trait]
2063impl ProcManager for BootstrapProcManager {
2064 type Handle = BootstrapProcHandle;
2065
2066 type Config = BootstrapProcConfig;
2067
2068 fn transport(&self) -> ChannelTransport {
2075 ChannelTransport::Unix
2076 }
2077
2078 #[hyperactor::instrument(fields(proc_id=proc_id.to_string(), addr=backend_addr.to_string()))]
2105 async fn spawn(
2106 &self,
2107 proc_id: ProcAddr,
2108 backend_addr: ChannelAddr,
2109 config: BootstrapProcConfig,
2110 ) -> Result<Self::Handle, HostError> {
2111 let (callback_addr, mut callback_rx) = channel::serve::<(ChannelAddr, ActorRef<ProcAgent>)>(
2112 ChannelAddr::any(ChannelTransport::Unix),
2113 )?;
2114
2115 let overrides = &config.client_config_override;
2117 let enable_forwarding = override_or_global(overrides, MESH_ENABLE_LOG_FORWARDING);
2118 let enable_file_capture = override_or_global(overrides, MESH_ENABLE_FILE_CAPTURE);
2119 let tail_size = override_or_global(overrides, MESH_TAIL_LOG_LINES);
2120 let need_stdio = enable_forwarding || enable_file_capture || tail_size > 0;
2121
2122 let mode = Bootstrap::Proc {
2123 proc_id: proc_id.clone(),
2124 backend_addr,
2125 callback_addr,
2126 socket_dir_path: self.socket_dir.path().to_owned(),
2127 config: Some(config.client_config_override.clone()),
2128 };
2129
2130 let bootstrap_payload = mode
2132 .to_env_safe_string()
2133 .map_err(|e| HostError::ProcessConfigurationFailure(proc_id.clone(), e.into()))?;
2134
2135 let opts = LaunchOptions {
2136 bootstrap_payload,
2137 process_name: format_process_name(&proc_id.clone()),
2138 command: config
2139 .bootstrap_command
2140 .as_ref()
2141 .unwrap_or(&self.command)
2142 .clone(),
2143 want_stdio: need_stdio,
2144 tail_lines: tail_size,
2145 log_channel: if enable_forwarding {
2146 Some(ChannelAddr::any(ChannelTransport::Unix))
2147 } else {
2148 None
2149 },
2150 proc_bind: config.proc_bind.clone(),
2151 };
2152
2153 tracing::info!(proc_id = %proc_id, "launching proc with opts={opts:?}");
2155 let ref_proc_id: ProcAddr = proc_id.clone();
2156 let launcher = Arc::clone(self.launcher());
2157 let launch_result = match AssertUnwindSafe(launcher.launch(&ref_proc_id, opts.clone()))
2158 .catch_unwind()
2159 .await
2160 {
2161 Ok(Ok(launch_result)) => launch_result,
2162 Ok(Err(error)) => {
2163 let io_err = match error {
2164 ProcLauncherError::Launch(io_err) => io_err,
2165 other => std::io::Error::other(other.to_string()),
2166 };
2167 return Err(HostError::ProcessSpawnFailure(
2168 proc_id.clone(),
2169 format!("{:?}", opts.command),
2170 io_err,
2171 ));
2172 }
2173 Err(panic) => {
2174 if let Err(error) = launcher.kill(&ref_proc_id).await {
2175 tracing::warn!(proc_id = %ref_proc_id, %error, "failed to clean up panicked proc launch");
2176 }
2177 std::panic::resume_unwind(panic);
2178 }
2179 };
2180 let cleanup = LaunchCleanup {
2181 launcher,
2182 proc_id: Some(proc_id.clone()),
2183 };
2184
2185 let (out_fwder, err_fwder) = match launch_result.stdio {
2187 StdioHandling::Captured { stdout, stderr } => {
2188 let (file_stdout, file_stderr) = if enable_file_capture {
2189 match self.file_appender.as_deref() {
2190 Some(fm) => (
2191 Some(fm.addr_for(OutputTarget::Stdout)),
2192 Some(fm.addr_for(OutputTarget::Stderr)),
2193 ),
2194 None => {
2195 tracing::warn!("enable_file_capture=true but no FileAppender");
2196 (None, None)
2197 }
2198 }
2199 } else {
2200 (None, None)
2201 };
2202
2203 let out = StreamFwder::start(
2204 stdout,
2205 file_stdout,
2206 OutputTarget::Stdout,
2207 tail_size,
2208 opts.log_channel.clone(),
2209 &ref_proc_id,
2210 config.create_rank,
2211 );
2212 let err = StreamFwder::start(
2213 stderr,
2214 file_stderr,
2215 OutputTarget::Stderr,
2216 tail_size,
2217 opts.log_channel.clone(),
2218 &ref_proc_id,
2219 config.create_rank,
2220 );
2221 (Some(out), Some(err))
2222 }
2223 StdioHandling::Inherited | StdioHandling::ManagedByLauncher => {
2224 if !need_stdio {
2225 tracing::info!(
2226 %proc_id, enable_forwarding, enable_file_capture, tail_size,
2227 "child stdio NOT captured (forwarding/file_capture/tail all disabled)"
2228 );
2229 }
2230 (None, None)
2231 }
2232 };
2233
2234 let handle = BootstrapProcHandle::new(proc_id.clone(), Arc::downgrade(self.launcher()));
2236 handle.mark_running(launch_result.started_at);
2237 handle.set_stream_monitors(out_fwder, err_fwder);
2238
2239 {
2241 let mut children = self.children.lock().await;
2242 children.insert(proc_id.clone(), handle.clone());
2243 }
2244
2245 self.spawn_exit_monitor(proc_id.clone(), handle.clone(), launch_result.exit_rx);
2248
2249 let h = handle.clone();
2251 tokio::spawn(async move {
2252 match callback_rx.recv().await {
2253 Ok((addr, agent)) => {
2254 let _ = h.mark_ready(addr, agent);
2255 }
2256 Err(e) => {
2257 let _ = h.mark_failed(format!("bootstrap callback failed: {e}"));
2259 }
2260 }
2261 });
2262
2263 cleanup.disarm();
2265 Ok(handle)
2266 }
2267}
2268
2269#[async_trait]
2270impl SingleTerminate for BootstrapProcManager {
2271 async fn terminate_proc(
2281 &self,
2282 cx: &impl context::Actor,
2283 proc: &ProcAddr,
2284 timeout: Duration,
2285 reason: &str,
2286 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
2287 let proc_handle: Option<BootstrapProcHandle> = {
2289 let mut guard = self.children.lock().await;
2290 guard.remove(proc)
2291 };
2292
2293 if let Some(h) = proc_handle {
2294 h.terminate(cx, timeout, reason)
2295 .await
2296 .map(|_| (Vec::new(), Vec::new()))
2297 .map_err(|e| e.into())
2298 } else {
2299 Err(anyhow::anyhow!("proc doesn't exist: {}", proc))
2300 }
2301 }
2302}
2303
2304#[async_trait]
2305impl BulkTerminate for BootstrapProcManager {
2306 async fn terminate_all(
2320 &self,
2321 cx: &impl context::Actor,
2322 timeout: Duration,
2323 max_in_flight: usize,
2324 reason: &str,
2325 ) -> TerminateSummary {
2326 let handles: Vec<BootstrapProcHandle> = {
2329 let mut guard = self.children.lock().await;
2330 guard.drain().map(|(_, v)| v).collect()
2331 };
2332
2333 let attempted = handles.len();
2334 let mut ok = 0usize;
2335
2336 let results = stream::iter(handles.into_iter().map(|h| async move {
2337 match h.terminate(cx, timeout, reason).await {
2338 Ok(_) | Err(TerminateError::AlreadyTerminated(_)) => {
2339 true
2341 }
2342 Err(e) => {
2343 tracing::warn!(error=%e, "terminate_all: failed to terminate child");
2344 false
2345 }
2346 }
2347 }))
2348 .buffer_unordered(max_in_flight.max(1))
2349 .collect::<Vec<bool>>()
2350 .await;
2351
2352 for r in results {
2353 if r {
2354 ok += 1;
2355 }
2356 }
2357
2358 TerminateSummary {
2359 attempted,
2360 ok,
2361 failed: attempted.saturating_sub(ok),
2362 }
2363 }
2364}
2365
2366pub async fn bootstrap() -> anyhow::Result<i32> {
2383 let Some(boot) = Bootstrap::get_from_env()? else {
2384 anyhow::bail!(
2385 "bootstrap: no bootstrap mode configured (HYPERACTOR_MESH_BOOTSTRAP_MODE unset)"
2386 );
2387 };
2388 boot.bootstrap().await
2389}
2390
2391pub async fn bootstrap_or_die() -> ! {
2394 match bootstrap().await {
2395 Ok(exit_code) => std::process::exit(exit_code),
2396 Err(err) => {
2397 let _ = writeln!(Debug, "failed to bootstrap mesh process: {}", err);
2398 tracing::error!("failed to bootstrap mesh process: {}", err);
2399 std::process::exit(1);
2400 }
2401 }
2402}
2403
2404#[derive(enum_as_inner::EnumAsInner)]
2405enum DebugSink {
2406 File(std::fs::File),
2407 Sink,
2408}
2409
2410impl DebugSink {
2411 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2412 match self {
2413 DebugSink::File(f) => f.write(buf),
2414 DebugSink::Sink => Ok(buf.len()),
2415 }
2416 }
2417 fn flush(&mut self) -> io::Result<()> {
2418 match self {
2419 DebugSink::File(f) => f.flush(),
2420 DebugSink::Sink => Ok(()),
2421 }
2422 }
2423}
2424
2425fn debug_sink() -> &'static Mutex<DebugSink> {
2426 static DEBUG_SINK: OnceLock<Mutex<DebugSink>> = OnceLock::new();
2427 DEBUG_SINK.get_or_init(|| {
2428 let debug_path = {
2429 let mut p = std::env::temp_dir();
2430 if let Ok(user) = std::env::var("USER") {
2431 p.push(user);
2432 }
2433 std::fs::create_dir_all(&p).ok();
2434 p.push("monarch-bootstrap-debug.log");
2435 p
2436 };
2437 let sink = if debug_path.exists() {
2438 match OpenOptions::new()
2439 .append(true)
2440 .create(true)
2441 .open(debug_path.clone())
2442 {
2443 Ok(f) => DebugSink::File(f),
2444 Err(_e) => {
2445 eprintln!(
2446 "failed to open {} for bootstrap debug logging",
2447 debug_path.display()
2448 );
2449 DebugSink::Sink
2450 }
2451 }
2452 } else {
2453 DebugSink::Sink
2454 };
2455 Mutex::new(sink)
2456 })
2457}
2458
2459const DEBUG_TO_STDERR: bool = false;
2461
2462struct Debug;
2465
2466impl Debug {
2467 fn is_active() -> bool {
2468 DEBUG_TO_STDERR || debug_sink().lock().unwrap().is_file()
2469 }
2470}
2471
2472impl Write for Debug {
2473 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2474 let res = debug_sink().lock().unwrap().write(buf);
2475 if DEBUG_TO_STDERR {
2476 let n = match res {
2477 Ok(n) => n,
2478 Err(_) => buf.len(),
2479 };
2480 let _ = io::stderr().write_all(&buf[..n]);
2481 }
2482
2483 res
2484 }
2485 fn flush(&mut self) -> io::Result<()> {
2486 let res = debug_sink().lock().unwrap().flush();
2487 if DEBUG_TO_STDERR {
2488 let _ = io::stderr().flush();
2489 }
2490 res
2491 }
2492}
2493
2494pub(crate) fn local_proc_addr(
2496 socket_dir: &Path,
2497 proc_id: &hyperactor::id::ProcId,
2498) -> anyhow::Result<(ChannelAddr, PathBuf)> {
2499 let path = proc_id.to_path_elem(socket_dir);
2500 let addr = std::os::unix::net::SocketAddr::from_pathname(path.clone())
2501 .with_context(|| {
2502 format!(
2503 "constructing unix socket address for proc {proc_id} \
2504 at {} ({} bytes); path must fit within SUN_LEN \
2505 (108 on Linux, 104 on macOS)",
2506 path.display(),
2507 path.as_os_str().len()
2508 )
2509 })?
2510 .into();
2511 Ok((addr, path))
2512}
2513
2514fn runtime_dir() -> io::Result<TempDir> {
2518 if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") {
2519 let path = PathBuf::from(runtime_dir);
2520 if path.is_dir() {
2521 return tempfile::tempdir_in(path);
2522 }
2523 }
2524 tempfile::tempdir()
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529 use std::path::PathBuf;
2530 use std::sync::Arc;
2531 use std::sync::atomic::AtomicBool;
2532 use std::sync::atomic::Ordering;
2533
2534 use async_trait::async_trait;
2535 use futures::FutureExt;
2536 use hyperactor::ActorEnvironment;
2537 use hyperactor::Location;
2538 use hyperactor::PortHandle;
2539 use hyperactor::ProcId;
2540 use hyperactor::RemoteSpawn;
2541 use hyperactor::channel::ChannelAddr;
2542 use hyperactor::channel::ChannelTransport;
2543 use hyperactor::channel::TcpMode;
2544 use hyperactor::mailbox::MailboxSender;
2545 use hyperactor::mailbox::MessageEnvelope;
2546 use hyperactor::mailbox::Undeliverable;
2547 use hyperactor::testing::ids::test_proc_id;
2548 use hyperactor::testing::ids::test_proc_id_with_addr;
2549 use timed_test::async_timed_test;
2550
2551 use super::*;
2552
2553 #[derive(Clone, Copy)]
2554 enum CleanupLaunchBehavior {
2555 Succeed,
2556 Panic,
2557 }
2558
2559 struct CleanupLauncher {
2560 behavior: CleanupLaunchBehavior,
2561 killed: tokio::sync::mpsc::UnboundedSender<ProcAddr>,
2562 }
2563
2564 #[async_trait]
2565 impl ProcLauncher for CleanupLauncher {
2566 async fn launch(
2567 &self,
2568 _proc_id: &ProcAddr,
2569 _opts: LaunchOptions,
2570 ) -> Result<crate::proc_launcher::LaunchResult, ProcLauncherError> {
2571 match self.behavior {
2572 CleanupLaunchBehavior::Succeed => {
2573 let (_exit_tx, exit_rx) = tokio::sync::oneshot::channel();
2574 Ok(LaunchResult {
2575 pid: None,
2576 started_at: std::time::SystemTime::now(),
2577 stdio: StdioHandling::ManagedByLauncher,
2578 exit_rx,
2579 })
2580 }
2581 CleanupLaunchBehavior::Panic => panic!("test launcher panic"),
2582 }
2583 }
2584
2585 async fn terminate(
2586 &self,
2587 _proc_id: &ProcAddr,
2588 _timeout: Duration,
2589 ) -> Result<(), ProcLauncherError> {
2590 unreachable!("cleanup guard test does not terminate a proc")
2591 }
2592
2593 async fn kill(&self, proc_id: &ProcAddr) -> Result<(), ProcLauncherError> {
2594 let _ = self.killed.send(proc_id.clone());
2595 Ok(())
2596 }
2597 }
2598
2599 fn manager_with_cleanup_launcher(
2600 behavior: CleanupLaunchBehavior,
2601 ) -> (
2602 BootstrapProcManager,
2603 tokio::sync::mpsc::UnboundedReceiver<ProcAddr>,
2604 ) {
2605 let (killed, killed_rx) = tokio::sync::mpsc::unbounded_channel();
2606
2607 let manager = BootstrapProcManager::new(BootstrapCommand::default())
2608 .expect("bootstrap proc manager should be created");
2609
2610 manager
2611 .set_launcher(Arc::new(CleanupLauncher { behavior, killed }))
2612 .expect("test launcher should be installed");
2613
2614 (manager, killed_rx)
2615 }
2616
2617 fn cleanup_test_config() -> BootstrapProcConfig {
2618 BootstrapProcConfig {
2619 create_rank: 0,
2620 client_config_override: Attrs::new(),
2621 proc_bind: None,
2622 bootstrap_command: None,
2623 }
2624 }
2625
2626 #[async_timed_test(timeout_secs = 30)]
2627 async fn cancelled_bootstrap_spawn_kills_launched_proc_before_registration() {
2628 let (manager, mut killed_rx) =
2629 manager_with_cleanup_launcher(CleanupLaunchBehavior::Succeed);
2630
2631 let proc_id = test_proc_id("cancelled-bootstrap-spawn");
2632 let _children_guard = manager.children.lock().await;
2636 let mut spawn = Box::pin(manager.spawn(
2637 proc_id.clone(),
2638 ChannelAddr::any(ChannelTransport::Unix),
2639 cleanup_test_config(),
2640 ));
2641
2642 assert!(
2643 futures::poll!(&mut spawn).is_pending(),
2644 "spawn should wait to register the launched proc",
2645 );
2646 drop(spawn);
2647
2648 assert_eq!(
2649 killed_rx
2650 .recv()
2651 .await
2652 .expect("cancelled spawn should kill the launched proc"),
2653 proc_id,
2654 );
2655 }
2656
2657 #[async_timed_test(timeout_secs = 30)]
2658 async fn panicked_bootstrap_spawn_kills_proc_and_resumes_unwind() {
2659 let (manager, mut killed_rx) = manager_with_cleanup_launcher(CleanupLaunchBehavior::Panic);
2660 let proc_id = test_proc_id("panicked-bootstrap-spawn");
2661
2662 let result = AssertUnwindSafe(manager.spawn(
2663 proc_id.clone(),
2664 ChannelAddr::any(ChannelTransport::Unix),
2665 cleanup_test_config(),
2666 ))
2667 .catch_unwind()
2668 .await;
2669
2670 assert!(result.is_err(), "launcher panic should resume unwinding");
2671 assert_eq!(
2672 killed_rx
2673 .recv()
2674 .await
2675 .expect("panicked spawn should kill the launched proc"),
2676 proc_id,
2677 );
2678 }
2679
2680 fn env_command() -> BootstrapCommand {
2681 BootstrapCommand {
2682 program: PathBuf::from("/usr/bin/env"),
2683 env: HashMap::from([("BOOTSTRAP_COMMAND_TEST".to_string(), "present".to_string())]),
2684 ..Default::default()
2685 }
2686 }
2687
2688 async fn env_output(command: BootstrapCommand) -> Vec<String> {
2689 let output = command
2690 .new()
2691 .output()
2692 .await
2693 .expect("run env with the configured environment");
2694 String::from_utf8(output.stdout)
2695 .expect("env output should be UTF-8")
2696 .lines()
2697 .map(String::from)
2698 .collect()
2699 }
2700
2701 #[tokio::test]
2702 async fn bootstrap_command_inherits_environment() {
2703 let path = std::env::var("PATH").expect("test runner should set PATH");
2704
2705 let lines = env_output(env_command()).await;
2706
2707 assert!(lines.contains(&"BOOTSTRAP_COMMAND_TEST=present".to_string()));
2708 assert!(
2709 lines.contains(&format!("PATH={path}")),
2710 "bootstrap command should inherit the spawning process's environment"
2711 );
2712 }
2713
2714 #[tokio::test]
2715 async fn bootstrap_command_removes_inherited_environment() {
2716 assert!(
2717 std::env::var_os("PATH").is_some(),
2718 "test runner should set PATH"
2719 );
2720
2721 let lines = env_output(BootstrapCommand {
2722 env_remove: HashSet::from(["PATH".to_string(), "BOOTSTRAP_COMMAND_TEST".to_string()]),
2723 ..env_command()
2724 })
2725 .await;
2726
2727 assert!(
2728 !lines.iter().any(|line| line.starts_with("PATH=")),
2729 "removed variables should not be inherited"
2730 );
2731 assert!(
2732 lines.contains(&"BOOTSTRAP_COMMAND_TEST=present".to_string()),
2733 "env should take precedence over env_remove"
2734 );
2735 }
2736
2737 #[tokio::test]
2738 async fn bootstrap_command_uses_exact_environment() {
2739 assert!(
2740 std::env::var_os("PATH").is_some(),
2741 "test runner should set PATH"
2742 );
2743
2744 assert_eq!(
2745 env_output(BootstrapCommand {
2746 inherit_env: false,
2747 ..env_command()
2748 })
2749 .await,
2750 ["BOOTSTRAP_COMMAND_TEST=present"],
2751 "bootstrap command should use only its configured environment"
2752 );
2753 }
2754
2755 struct ShutdownFlushProbe {
2756 gateway: Gateway,
2757 expected_location: Location,
2758 flushed: Arc<AtomicBool>,
2759 }
2760
2761 #[async_trait]
2762 impl MailboxSender for ShutdownFlushProbe {
2763 fn post_unchecked(
2764 &self,
2765 _envelope: MessageEnvelope,
2766 _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
2767 ) {
2768 }
2769
2770 async fn flush(&self) -> Result<(), anyhow::Error> {
2771 assert_eq!(
2772 self.gateway.default_location(),
2773 self.expected_location,
2774 "shutdown transports must remain live while the root proc flushes"
2775 );
2776 self.flushed.store(true, Ordering::SeqCst);
2777 Ok(())
2778 }
2779 }
2780
2781 #[tokio::test]
2782 async fn test_host_shutdown_flushes_before_joining_frontend() {
2783 let relay_gateway = Gateway::new();
2784 let mut relay = relay_gateway
2785 .serve_duplex(ChannelAddr::any(ChannelTransport::Unix))
2786 .unwrap();
2787 let relay_addr = relay_gateway.default_location().addr().clone();
2788
2789 let gateway = Gateway::isolated();
2790 let mut first = gateway
2791 .serve(ChannelAddr::any(ChannelTransport::Local))
2792 .unwrap();
2793 let first_location = gateway.default_location();
2794 let frontend = gateway
2795 .serve(ChannelAddr::any(ChannelTransport::Local))
2796 .unwrap();
2797 let frontend_location = gateway.default_location();
2798 assert_ne!(first_location, frontend_location);
2799 let via = gateway.serve_via(relay_addr).await.unwrap();
2800 let via_location = gateway.default_location();
2801 assert!(via_location.as_via().is_some());
2802
2803 let proc = Proc::builder()
2804 .proc_id(ProcId::instance(Label::strip("shutdown_flush_probe")))
2805 .shared_gateway(gateway.clone())
2806 .build()
2807 .unwrap();
2808 let flushed = Arc::new(AtomicBool::new(false));
2809 let probe_id = proc
2810 .proc_addr()
2811 .actor_addr("shutdown_flush_probe")
2812 .id()
2813 .clone();
2814 assert!(proc.muxer().bind(
2815 probe_id,
2816 ShutdownFlushProbe {
2817 gateway: gateway.clone(),
2818 expected_location: via_location,
2819 flushed: flushed.clone(),
2820 }
2821 ));
2822
2823 let (tx, rx) = tokio::sync::oneshot::channel();
2824 tx.send(HostShutdownHandles::new(frontend, Some(via)))
2825 .unwrap();
2826 let shutdown = HostShutdownHandle {
2827 rx,
2828 exit_on_shutdown: false,
2829 };
2830
2831 shutdown
2832 .stop_and_join_after_drain(async {
2833 proc.flush().await.expect("flush probe should succeed");
2834 })
2835 .await;
2836
2837 assert!(flushed.load(Ordering::SeqCst));
2838 assert_eq!(gateway.default_location(), first_location);
2839
2840 first.stop("test complete");
2841 first.join().await.unwrap();
2842 relay.stop("test complete");
2843 relay.join().await.unwrap();
2844 }
2845
2846 #[tokio::test]
2847 async fn test_cancelled_pre_join_stops_frontend() {
2848 let gateway = Gateway::isolated();
2849 let mut first = gateway
2850 .serve(ChannelAddr::any(ChannelTransport::Local))
2851 .unwrap();
2852 let first_location = gateway.default_location();
2853 let frontend = gateway
2854 .serve(ChannelAddr::any(ChannelTransport::Local))
2855 .unwrap();
2856 assert_ne!(gateway.default_location(), first_location);
2857
2858 let (tx, rx) = tokio::sync::oneshot::channel();
2859 tx.send(HostShutdownHandles::new(frontend, None)).unwrap();
2860 let shutdown = HostShutdownHandle {
2861 rx,
2862 exit_on_shutdown: false,
2863 };
2864 let (entered_tx, entered_rx) = tokio::sync::oneshot::channel();
2865 let shutdown_task = tokio::spawn(shutdown.stop_and_join_after_drain(async move {
2866 entered_tx
2867 .send(())
2868 .expect("pre-join notification receiver must remain live");
2869 future::pending::<()>().await;
2870 }));
2871
2872 entered_rx
2873 .await
2874 .expect("pre-join future must signal before cancellation");
2875 shutdown_task.abort();
2876 assert!(
2877 shutdown_task
2878 .await
2879 .expect_err("aborted join task must report cancellation")
2880 .is_cancelled()
2881 );
2882 assert_eq!(gateway.default_location(), first_location);
2883
2884 first.stop("test complete");
2885 first.join().await.unwrap();
2886 }
2887
2888 #[tokio::test]
2889 async fn test_dropped_shutdown_receiver_stops_frontend() {
2890 let gateway = Gateway::isolated();
2891 let mut first = gateway
2892 .serve(ChannelAddr::any(ChannelTransport::Local))
2893 .unwrap();
2894 let first_location = gateway.default_location();
2895 let frontend = gateway
2896 .serve(ChannelAddr::any(ChannelTransport::Local))
2897 .unwrap();
2898 assert_ne!(gateway.default_location(), first_location);
2899
2900 let (tx, rx) = tokio::sync::oneshot::channel();
2901 tx.send(HostShutdownHandles::new(frontend, None)).unwrap();
2902 drop(rx);
2903 assert_eq!(gateway.default_location(), first_location);
2904
2905 first.stop("test complete");
2906 first.join().await.unwrap();
2907 }
2908
2909 #[tokio::test]
2910 async fn test_host_bootstrap_ready_callback() {
2911 let host_addr = ChannelAddr::any(ChannelTransport::Unix);
2912 let ready = HostBootstrapReady::new(host_addr).expect("open readiness callback");
2913 let callback_addr = ready.callback_addr();
2914 let sender = tokio::spawn(async move {
2915 channel::dial(callback_addr)
2916 .expect("dial readiness callback")
2917 .send(())
2918 .await
2919 .expect("send readiness callback");
2920 });
2921
2922 ready.wait().await.expect("receive readiness callback");
2923 sender.await.expect("readiness sender task");
2924 }
2925
2926 #[tokio::test]
2927 async fn test_host_bootstrap_ready_times_out_without_callback() {
2928 let config = hyperactor_config::global::lock();
2929 let _timeout = config.override_key(
2930 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2931 Duration::from_millis(20),
2932 );
2933 let host_addr = ChannelAddr::any(ChannelTransport::Unix);
2934 let ready = HostBootstrapReady::new(host_addr).expect("open readiness callback");
2935
2936 let error = ready
2937 .wait()
2938 .await
2939 .expect_err("missing callback must fail closed");
2940 assert!(
2941 error.to_string().contains("did not become ready"),
2942 "unexpected readiness error: {error:#}"
2943 );
2944 }
2945
2946 #[test]
2947 fn test_bootstrap_mode_env_string_none_config_proc() {
2948 let value = Bootstrap::Proc {
2949 proc_id: test_proc_id("foo_0"),
2950 backend_addr: ChannelAddr::any(ChannelTransport::Tcp(TcpMode::Hostname)),
2951 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
2952 socket_dir_path: PathBuf::from("notexist"),
2953 config: None,
2954 };
2955
2956 let safe = value.to_env_safe_string().unwrap();
2957 let round = Bootstrap::from_env_safe_string(&safe).unwrap();
2958
2959 let safe2 = round.to_env_safe_string().unwrap();
2962 assert_eq!(safe, safe2, "env-safe round-trip should be stable");
2963
2964 match round {
2966 Bootstrap::Proc { config: None, .. } => {}
2967 other => panic!("expected Proc with None config, got {:?}", other),
2968 }
2969 }
2970
2971 #[test]
2972 fn test_bootstrap_mode_env_string_none_config_host() {
2973 let value = Bootstrap::Host {
2974 addr: ChannelAddr::any(ChannelTransport::Unix),
2975 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
2976 command: None,
2977 config: None,
2978 exit_on_shutdown: false,
2979 };
2980
2981 let safe = value.to_env_safe_string().unwrap();
2982 let round = Bootstrap::from_env_safe_string(&safe).unwrap();
2983
2984 let safe2 = round.to_env_safe_string().unwrap();
2986 assert_eq!(safe, safe2);
2987
2988 match round {
2990 Bootstrap::Host { config: None, .. } => {}
2991 other => panic!("expected Host with None config, got {:?}", other),
2992 }
2993 }
2994
2995 #[test]
2996 fn test_bootstrap_mode_env_string_invalid() {
2997 assert!(Bootstrap::from_env_safe_string("!!!").is_err());
2999 }
3000
3001 #[test]
3002 fn test_bootstrap_config_snapshot_roundtrip() {
3003 let mut attrs = Attrs::new();
3005 attrs[MESH_TAIL_LOG_LINES] = 123;
3006 attrs[MESH_BOOTSTRAP_ENABLE_PDEATHSIG] = false;
3007
3008 let socket_dir = runtime_dir().unwrap();
3009
3010 {
3012 let original = Bootstrap::Proc {
3013 proc_id: test_proc_id("foo_42"),
3014 backend_addr: ChannelAddr::any(ChannelTransport::Unix),
3015 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
3016 config: Some(attrs.clone()),
3017 socket_dir_path: socket_dir.path().to_owned(),
3018 };
3019 let env_str = original.to_env_safe_string().expect("encode bootstrap");
3020 let decoded = Bootstrap::from_env_safe_string(&env_str).expect("decode bootstrap");
3021 match &decoded {
3022 Bootstrap::Proc { config, .. } => {
3023 let cfg = config.as_ref().expect("expected Some(attrs)");
3024 assert_eq!(cfg[MESH_TAIL_LOG_LINES], 123);
3025 assert!(!cfg[MESH_BOOTSTRAP_ENABLE_PDEATHSIG]);
3026 }
3027 other => panic!("unexpected variant after roundtrip: {:?}", other),
3028 }
3029 }
3030
3031 {
3033 let original = Bootstrap::Host {
3034 addr: ChannelAddr::any(ChannelTransport::Unix),
3035 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
3036 command: None,
3037 config: Some(attrs.clone()),
3038 exit_on_shutdown: false,
3039 };
3040 let env_str = original.to_env_safe_string().expect("encode bootstrap");
3041 let decoded = Bootstrap::from_env_safe_string(&env_str).expect("decode bootstrap");
3042 match &decoded {
3043 Bootstrap::Host { config, .. } => {
3044 let cfg = config.as_ref().expect("expected Some(attrs)");
3045 assert_eq!(cfg[MESH_TAIL_LOG_LINES], 123);
3046 assert!(!cfg[MESH_BOOTSTRAP_ENABLE_PDEATHSIG]);
3047 }
3048 other => panic!("unexpected variant after roundtrip: {:?}", other),
3049 }
3050 }
3051 }
3052
3053 #[tokio::test]
3054 async fn test_v1_child_logging() {
3055 use hyperactor::channel;
3056 use hyperactor::mailbox::BoxedMailboxSender;
3057 use hyperactor::mailbox::DialMailboxRouter;
3058 use hyperactor::mailbox::MailboxServer;
3059 use hyperactor::proc::Proc;
3060
3061 use crate::bootstrap::BOOTSTRAP_LOG_CHANNEL;
3062 use crate::logging::LogClientActor;
3063 use crate::logging::LogClientMessageClient;
3064 use crate::logging::LogForwardActor;
3065 use crate::logging::LogMessage;
3066 use crate::logging::OutputTarget;
3067 use crate::logging::test_tap;
3068
3069 let router = DialMailboxRouter::new();
3070 let (proc_addr, proc_rx) =
3071 channel::serve(ChannelAddr::any(ChannelTransport::Unix)).unwrap();
3072 let proc = Proc::configured(
3073 test_proc_id("client_0"),
3074 BoxedMailboxSender::new(router.clone()),
3075 );
3076 proc.clone().serve(proc_rx);
3077 let proc_ref: ProcAddr = test_proc_id("client_0");
3078 router.bind(proc_ref, proc_addr.clone());
3079 let client = proc.client("client");
3080
3081 let (tap_tx, mut tap_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
3082 test_tap::install(tap_tx);
3083
3084 let log_channel = ChannelAddr::any(ChannelTransport::Unix);
3085 unsafe {
3087 std::env::set_var(BOOTSTRAP_LOG_CHANNEL, log_channel.to_string());
3088 }
3089
3090 let log_client_actor = LogClientActor::new((), &ActorEnvironment::default())
3093 .await
3094 .unwrap();
3095 let log_client: ActorRef<LogClientActor> = proc.spawn(log_client_actor).bind();
3096 log_client.set_aggregate(&client, None).await.unwrap();
3097
3098 let log_forwarder_actor =
3101 LogForwardActor::new(log_client.clone(), &ActorEnvironment::default())
3102 .await
3103 .unwrap();
3104 let _log_forwarder: ActorRef<LogForwardActor> = proc.spawn(log_forwarder_actor).bind();
3105
3106 let tx = channel::dial::<LogMessage>(log_channel.clone()).unwrap();
3109
3110 tx.post(LogMessage::Log {
3113 hostname: "testhost".into(),
3114 proc_id: "testproc[0]".into(),
3115 output_target: OutputTarget::Stdout,
3116 payload: wirevalue::Any::serialize(&"hello from child".to_string()).unwrap(),
3117 });
3118
3119 let line = tokio::time::timeout(Duration::from_secs(2), tap_rx.recv())
3121 .await
3122 .expect("timed out waiting for log line")
3123 .expect("tap channel closed unexpectedly");
3124 assert!(
3125 line.contains("hello from child"),
3126 "log line did not appear via LogClientActor; got: {line}"
3127 );
3128 }
3129
3130 mod proc_handle {
3131
3132 use std::sync::Arc;
3133 use std::time::Duration;
3134
3135 use async_trait::async_trait;
3136 use hyperactor::ActorRef;
3137 use hyperactor::ProcAddr;
3138 use hyperactor::testing::ids::test_proc_id;
3139
3140 use super::super::*;
3141 use crate::host::ProcHandle;
3142 use crate::proc_launcher::LaunchOptions;
3143 use crate::proc_launcher::LaunchResult;
3144 use crate::proc_launcher::ProcLauncher;
3145 use crate::proc_launcher::ProcLauncherError;
3146
3147 struct TestProcLauncher;
3155
3156 #[async_trait]
3157 impl ProcLauncher for TestProcLauncher {
3158 async fn launch(
3159 &self,
3160 _proc_id: &ProcAddr,
3161 _opts: LaunchOptions,
3162 ) -> Result<LaunchResult, ProcLauncherError> {
3163 panic!("TestProcLauncher::launch should not be called in unit tests");
3164 }
3165
3166 async fn terminate(
3167 &self,
3168 _proc_id: &ProcAddr,
3169 _timeout: Duration,
3170 ) -> Result<(), ProcLauncherError> {
3171 panic!("TestProcLauncher::terminate should not be called in unit tests");
3172 }
3173
3174 async fn kill(&self, _proc_id: &ProcAddr) -> Result<(), ProcLauncherError> {
3175 panic!("TestProcLauncher::kill should not be called in unit tests");
3176 }
3177 }
3178
3179 fn handle_for_test() -> BootstrapProcHandle {
3186 let proc_id: ProcAddr = test_proc_id("0");
3187 let launcher: Arc<dyn ProcLauncher> = Arc::new(TestProcLauncher);
3188 BootstrapProcHandle::new(proc_id, Arc::downgrade(&launcher))
3189 }
3190
3191 #[tokio::test]
3192 async fn starting_to_running_ok() {
3193 let h = handle_for_test();
3194 assert!(matches!(h.status(), ProcStatus::Starting));
3195 let child_started_at = std::time::SystemTime::now();
3196 assert!(h.mark_running(child_started_at));
3197 match h.status() {
3198 ProcStatus::Running { started_at } => {
3199 assert_eq!(started_at, child_started_at);
3200 }
3201 other => panic!("expected Running, got {other:?}"),
3202 }
3203 }
3204
3205 #[tokio::test]
3206 async fn running_to_stopping_to_stopped_ok() {
3207 let h = handle_for_test();
3208 let child_started_at = std::time::SystemTime::now();
3209 assert!(h.mark_running(child_started_at));
3210 assert!(h.mark_stopping());
3211 assert!(matches!(h.status(), ProcStatus::Stopping { .. }));
3212 assert!(h.mark_stopped(0, Vec::new()));
3213 assert!(matches!(
3214 h.status(),
3215 ProcStatus::Stopped { exit_code: 0, .. }
3216 ));
3217 }
3218
3219 #[tokio::test]
3220 async fn running_to_killed_ok() {
3221 let h = handle_for_test();
3222 let child_started_at = std::time::SystemTime::now();
3223 assert!(h.mark_running(child_started_at));
3224 assert!(h.mark_killed(9, true));
3225 assert!(matches!(
3226 h.status(),
3227 ProcStatus::Killed {
3228 signal: 9,
3229 core_dumped: true
3230 }
3231 ));
3232 }
3233
3234 #[tokio::test]
3235 async fn running_to_failed_ok() {
3236 let h = handle_for_test();
3237 let child_started_at = std::time::SystemTime::now();
3238 assert!(h.mark_running(child_started_at));
3239 assert!(h.mark_failed("bootstrap error"));
3240 match h.status() {
3241 ProcStatus::Failed { reason } => {
3242 assert_eq!(reason, "bootstrap error");
3243 }
3244 other => panic!("expected Failed(\"bootstrap error\"), got {other:?}"),
3245 }
3246 }
3247
3248 #[tokio::test]
3249 async fn illegal_transitions_are_rejected() {
3250 let h = handle_for_test();
3251 let child_started_at = std::time::SystemTime::now();
3252 assert!(h.mark_running(child_started_at));
3254 assert!(!h.mark_running(std::time::SystemTime::now()));
3255 assert!(matches!(h.status(), ProcStatus::Running { .. }));
3256 assert!(h.mark_stopping());
3258 assert!(h.mark_stopped(0, Vec::new()));
3259 assert!(!h.mark_running(child_started_at));
3260 assert!(!h.mark_killed(9, false));
3261 assert!(!h.mark_failed("nope"));
3262
3263 assert!(matches!(
3264 h.status(),
3265 ProcStatus::Stopped { exit_code: 0, .. }
3266 ));
3267 }
3268
3269 #[tokio::test]
3270 async fn transitions_from_ready_are_legal() {
3271 let h = handle_for_test();
3272 let addr = ChannelAddr::any(ChannelTransport::Unix);
3273 let t0 = std::time::SystemTime::now();
3275 assert!(h.mark_running(t0));
3276 let proc_id = <BootstrapProcHandle as ProcHandle>::proc_addr(&h);
3279 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
3280 let agent_ref: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
3281 assert!(h.mark_ready(addr, agent_ref));
3283 assert!(h.mark_stopping());
3284 assert!(h.mark_stopped(0, Vec::new()));
3285 }
3286
3287 #[tokio::test]
3288 async fn ready_to_killed_is_legal() {
3289 let h = handle_for_test();
3290 let addr = ChannelAddr::any(ChannelTransport::Unix);
3291 let t0 = std::time::SystemTime::now();
3293 assert!(h.mark_running(t0));
3294 let proc_id = <BootstrapProcHandle as ProcHandle>::proc_addr(&h);
3297 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
3298 let agent: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
3299 assert!(h.mark_ready(addr, agent));
3301 assert!(h.mark_killed(9, false));
3303 }
3304
3305 #[tokio::test]
3306 async fn mark_failed_from_stopping_is_allowed() {
3307 let h = handle_for_test();
3308
3309 assert!(h.mark_stopping(), "precondition: to Stopping");
3311
3312 assert!(
3314 h.mark_failed("boom"),
3315 "mark_failed() should succeed from Stopping"
3316 );
3317 match h.status() {
3318 ProcStatus::Failed { reason } => assert_eq!(reason, "boom"),
3319 other => panic!("expected Failed(\"boom\"), got {other:?}"),
3320 }
3321 }
3322 }
3323
3324 struct TestLauncher;
3330
3331 #[async_trait::async_trait]
3332 impl crate::proc_launcher::ProcLauncher for TestLauncher {
3333 async fn launch(
3334 &self,
3335 _proc_id: &ProcAddr,
3336 _opts: crate::proc_launcher::LaunchOptions,
3337 ) -> Result<crate::proc_launcher::LaunchResult, crate::proc_launcher::ProcLauncherError>
3338 {
3339 panic!("TestLauncher::launch should not be called in unit tests");
3340 }
3341
3342 async fn terminate(
3343 &self,
3344 _proc_id: &ProcAddr,
3345 _timeout: std::time::Duration,
3346 ) -> Result<(), crate::proc_launcher::ProcLauncherError> {
3347 panic!("TestLauncher::terminate should not be called in unit tests");
3348 }
3349
3350 async fn kill(
3351 &self,
3352 _proc_id: &ProcAddr,
3353 ) -> Result<(), crate::proc_launcher::ProcLauncherError> {
3354 panic!("TestLauncher::kill should not be called in unit tests");
3355 }
3356 }
3357
3358 fn test_handle(proc_id: ProcAddr) -> BootstrapProcHandle {
3359 let launcher: std::sync::Arc<dyn crate::proc_launcher::ProcLauncher> =
3360 std::sync::Arc::new(TestLauncher);
3361 BootstrapProcHandle::new(proc_id, std::sync::Arc::downgrade(&launcher))
3362 }
3363
3364 #[tokio::test]
3365 async fn watch_notifies_on_status_changes() {
3366 let proc_id = test_proc_id("1");
3367 let handle = test_handle(proc_id);
3368 let mut rx = handle.watch();
3369
3370 let now = std::time::SystemTime::now();
3372 assert!(handle.mark_running(now));
3373 rx.changed().await.ok(); match &*rx.borrow() {
3375 ProcStatus::Running { started_at } => {
3376 assert_eq!(*started_at, now);
3377 }
3378 s => panic!("expected Running, got {s:?}"),
3379 }
3380
3381 assert!(handle.mark_stopped(0, Vec::new()));
3383 rx.changed().await.ok(); assert!(matches!(
3385 &*rx.borrow(),
3386 ProcStatus::Stopped { exit_code: 0, .. }
3387 ));
3388 }
3389
3390 #[tokio::test]
3391 async fn ready_errs_if_process_exits_before_running() {
3392 let proc_id =
3393 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "early-exit");
3394 let handle = test_handle(proc_id);
3395
3396 assert!(handle.mark_stopped(7, Vec::new()));
3399
3400 match handle.ready_inner().await {
3402 Ok(()) => panic!("ready() unexpectedly succeeded"),
3403 Err(ReadyError::Terminal(ProcStatus::Stopped { exit_code, .. })) => {
3404 assert_eq!(exit_code, 7)
3405 }
3406 Err(other) => panic!("expected Stopped(7), got {other:?}"),
3407 }
3408 }
3409
3410 #[tokio::test]
3411 async fn status_unknown_proc_is_none() {
3412 let manager = BootstrapProcManager::new(BootstrapCommand {
3413 program: PathBuf::from("/bin/true"),
3414 ..Default::default()
3415 })
3416 .unwrap();
3417 let unknown = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "nope");
3418 assert!(manager.status(&unknown).await.is_none());
3419 }
3420
3421 #[tokio::test]
3422 async fn handle_ready_allows_waiters() {
3423 let proc_id = test_proc_id("42");
3424 let handle = test_handle(proc_id.clone());
3425
3426 let started_at = std::time::SystemTime::now();
3427 assert!(handle.mark_running(started_at));
3428
3429 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
3430 let agent_ref: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
3431
3432 let ready_addr = ChannelAddr::any(ChannelTransport::Unix);
3435
3436 assert!(handle.mark_ready(ready_addr.clone(), agent_ref));
3438 handle
3439 .ready_inner()
3440 .await
3441 .expect("ready_inner() should complete after Ready");
3442
3443 match handle.status() {
3446 ProcStatus::Ready {
3447 started_at: t,
3448 addr: a,
3449 ..
3450 } => {
3451 assert_eq!(t, started_at);
3452 assert_eq!(a, ready_addr);
3453 }
3454 other => panic!("expected Ready, got {other:?}"),
3455 }
3456 }
3457
3458 #[test]
3459 fn display_running_includes_uptime() {
3460 let started_at = std::time::SystemTime::now() - Duration::from_secs(42);
3461 let st = ProcStatus::Running { started_at };
3462
3463 let s = format!("{}", st);
3464 assert!(s.contains("Running"));
3465 assert!(s.contains("42s"));
3466 }
3467
3468 #[test]
3469 fn display_ready_includes_addr() {
3470 let started_at = std::time::SystemTime::now() - Duration::from_secs(5);
3471 let addr = ChannelAddr::any(ChannelTransport::Unix);
3472 let agent = ActorRef::attest(
3473 test_proc_id_with_addr(addr.clone(), "proc")
3474 .actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME),
3475 );
3476
3477 let st = ProcStatus::Ready {
3478 started_at,
3479 addr: addr.clone(),
3480 agent,
3481 };
3482
3483 let s = format!("{}", st);
3484 assert!(s.contains(&addr.to_string())); assert!(s.contains("Ready"));
3486 }
3487
3488 #[test]
3489 fn display_stopped_includes_exit_code() {
3490 let st = ProcStatus::Stopped {
3491 exit_code: 7,
3492 stderr_tail: Vec::new(),
3493 };
3494 let s = format!("{}", st);
3495 assert!(s.contains("Stopped"));
3496 assert!(s.contains("7"));
3497 }
3498
3499 #[test]
3500 fn display_other_variants_does_not_panic() {
3501 let samples = vec![
3502 ProcStatus::Starting,
3503 ProcStatus::Stopping {
3504 started_at: std::time::SystemTime::now(),
3505 },
3506 ProcStatus::Ready {
3507 started_at: std::time::SystemTime::now(),
3508 addr: ChannelAddr::any(ChannelTransport::Unix),
3509 agent: ActorRef::attest(
3510 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "x")
3511 .actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME),
3512 ),
3513 },
3514 ProcStatus::Killed {
3515 signal: 9,
3516 core_dumped: false,
3517 },
3518 ProcStatus::Failed {
3519 reason: "boom".into(),
3520 },
3521 ];
3522
3523 for st in samples {
3524 let _ = format!("{}", st); }
3526 }
3527
3528 #[tokio::test]
3529 async fn proc_handle_ready_ok_through_trait() {
3530 let proc_id =
3531 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "ph-ready-ok");
3532 let handle = test_handle(proc_id.clone());
3533
3534 let t0 = std::time::SystemTime::now();
3536 assert!(handle.mark_running(t0));
3537
3538 let addr = ChannelAddr::any(ChannelTransport::Unix);
3540 let agent: ActorRef<ProcAgent> =
3541 ActorRef::attest(proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME));
3542 assert!(handle.mark_ready(addr, agent));
3543
3544 let r = <BootstrapProcHandle as ProcHandle>::ready(&handle).await;
3546 assert!(r.is_ok(), "expected Ok(()), got {r:?}");
3547 }
3548
3549 #[tokio::test]
3550 async fn proc_handle_wait_returns_terminal_status() {
3551 let proc_id = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "ph-wait");
3552 let handle = test_handle(proc_id);
3553
3554 assert!(handle.mark_stopped(0, Vec::new()));
3556
3557 let st = <BootstrapProcHandle as ProcHandle>::wait(&handle)
3559 .await
3560 .expect("wait should return Ok(terminal)");
3561
3562 match st {
3563 ProcStatus::Stopped { exit_code, .. } => assert_eq!(exit_code, 0),
3564 other => panic!("expected Stopped(0), got {other:?}"),
3565 }
3566 }
3567
3568 #[tokio::test]
3569 async fn ready_wrapper_maps_terminal_to_trait_error() {
3570 let proc_id = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "wrap");
3571 let handle = test_handle(proc_id);
3572
3573 assert!(handle.mark_stopped(7, Vec::new()));
3574
3575 match <BootstrapProcHandle as ProcHandle>::ready(&handle).await {
3576 Ok(()) => panic!("expected Err"),
3577 Err(HostReadyError::Terminal(ProcStatus::Stopped { exit_code, .. })) => {
3578 assert_eq!(exit_code, 7);
3579 }
3580 Err(e) => panic!("unexpected error: {e:?}"),
3581 }
3582 }
3583
3584 #[cfg(fbcode_build)]
3594 async fn make_proc_id_and_backend_addr(
3595 instance: &hyperactor::Client,
3596 _tag: &str,
3597 ) -> (ProcAddr, ChannelAddr) {
3598 let (backend_addr, rx) = channel::serve(ChannelAddr::any(ChannelTransport::Unix)).unwrap();
3601
3602 hyperactor::mailbox::MailboxServer::serve(instance.proc().clone(), rx);
3606
3607 let proc_id = test_proc_id_with_addr(ChannelTransport::Unix.any(), "proc");
3610 (proc_id, backend_addr)
3611 }
3612
3613 #[tokio::test]
3614 #[cfg(fbcode_build)]
3615 async fn bootstrap_handle_terminate_graceful() {
3616 let root =
3618 hyperactor::Proc::direct(ChannelTransport::Unix.any(), "root".to_string()).unwrap();
3619 let instance = root.client("client");
3620
3621 let mgr = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3622 let (proc_id, backend_addr) = make_proc_id_and_backend_addr(&instance, "t_term").await;
3623 let handle = mgr
3624 .spawn(
3625 proc_id.clone(),
3626 backend_addr.clone(),
3627 BootstrapProcConfig {
3628 create_rank: 0,
3629 client_config_override: Attrs::new(),
3630 proc_bind: None,
3631 bootstrap_command: None,
3632 },
3633 )
3634 .await
3635 .expect("spawn bootstrap child");
3636
3637 handle.ready().await.expect("ready");
3638
3639 let deadline = Duration::from_secs(2);
3640 match tokio::time::timeout(
3641 deadline * 2,
3642 handle.terminate(&instance, deadline, "test terminate"),
3643 )
3644 .await
3645 {
3646 Err(_) => panic!("terminate() future hung"),
3647 Ok(Ok(st)) => {
3648 match st {
3649 ProcStatus::Stopped { exit_code, .. } => {
3650 assert_eq!(exit_code, 0, "expected clean exit; got {exit_code}");
3652 }
3653 ProcStatus::Killed { signal, .. } => {
3654 assert_eq!(signal, libc::SIGTERM, "expected SIGTERM; got {signal}");
3672 }
3673 other => panic!("expected Stopped or Killed(SIGTERM); got {other:?}"),
3674 }
3675 }
3676 Ok(Err(e)) => panic!("terminate() failed: {e:?}"),
3677 }
3678 }
3679
3680 #[tokio::test]
3681 #[cfg(fbcode_build)]
3682 async fn bootstrap_handle_kill_forced() {
3683 let root =
3685 hyperactor::Proc::direct(ChannelTransport::Unix.any(), "root".to_string()).unwrap();
3686 let instance = root.client("client");
3687
3688 let mgr = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3689
3690 let (proc_id, backend_addr) = make_proc_id_and_backend_addr(&instance, "t_kill").await;
3692
3693 let handle = mgr
3695 .spawn(
3696 proc_id.clone(),
3697 backend_addr.clone(),
3698 BootstrapProcConfig {
3699 create_rank: 0,
3700 client_config_override: Attrs::new(),
3701 proc_bind: None,
3702 bootstrap_command: None,
3703 },
3704 )
3705 .await
3706 .expect("spawn bootstrap child");
3707
3708 handle.ready().await.expect("ready");
3711
3712 let deadline = Duration::from_secs(5);
3715 match tokio::time::timeout(deadline, handle.kill()).await {
3716 Err(_) => panic!("kill() future hung"),
3717 Ok(Ok(st)) => {
3718 match st {
3720 ProcStatus::Killed { signal, .. } => {
3721 assert_eq!(signal, libc::SIGKILL, "expected SIGKILL; got {}", signal);
3723 }
3724 other => panic!("expected Killed status after kill(); got: {other:?}"),
3725 }
3726 }
3727 Ok(Err(e)) => panic!("kill() failed: {e:?}"),
3728 }
3729 }
3730
3731 #[tokio::test]
3732 #[cfg(fbcode_build)]
3733 async fn test_host_bootstrap() {
3734 use crate::host_mesh::host_agent::GetLocalProcClient;
3735 use crate::proc_agent::NewClientInstanceClient;
3736
3737 let temp_proc = Proc::isolated();
3740 let temp_instance = temp_proc.client("temp");
3741
3742 let handle = host(
3743 ProcAddr::new(
3744 legacy_service_proc_id(),
3745 ChannelAddr::any(ChannelTransport::Unix).into(),
3746 ),
3747 Some(BootstrapCommand::test()),
3748 None,
3749 false,
3750 None,
3751 Gateway::global().clone(),
3752 None,
3753 )
3754 .await
3755 .unwrap();
3756
3757 let local_proc = handle.0.get_local_proc(&temp_instance).await.unwrap();
3758 let _local_instance = local_proc
3759 .new_client_instance(&temp_instance)
3760 .await
3761 .unwrap();
3762 }
3763
3764 use std::time::Duration;
3770
3771 use crate::proc_launcher::LaunchOptions;
3772 use crate::proc_launcher::LaunchResult;
3773 use crate::proc_launcher::ProcExitKind;
3774 use crate::proc_launcher::ProcExitResult;
3775 use crate::proc_launcher::ProcLauncher;
3776 use crate::proc_launcher::ProcLauncherError;
3777 use crate::proc_launcher::StdioHandling;
3778
3779 #[allow(dead_code)]
3782 struct DummyLauncher {
3783 marker: u64,
3785 }
3786
3787 impl DummyLauncher {
3788 #[allow(dead_code)]
3789 fn new(marker: u64) -> Self {
3790 Self { marker }
3791 }
3792
3793 #[allow(dead_code)]
3794 fn marker(&self) -> u64 {
3795 self.marker
3796 }
3797 }
3798
3799 #[async_trait::async_trait]
3800 impl ProcLauncher for DummyLauncher {
3801 async fn launch(
3802 &self,
3803 _proc_id: &ProcAddr,
3804 _opts: LaunchOptions,
3805 ) -> Result<LaunchResult, ProcLauncherError> {
3806 let (tx, rx) = tokio::sync::oneshot::channel();
3807 let _ = tx.send(ProcExitResult {
3809 kind: ProcExitKind::Exited { code: 0 },
3810 stderr_tail: Some(vec![]),
3811 });
3812 Ok(LaunchResult {
3813 pid: None,
3814 started_at: std::time::SystemTime::now(),
3815 stdio: StdioHandling::ManagedByLauncher,
3816 exit_rx: rx,
3817 })
3818 }
3819
3820 async fn terminate(
3821 &self,
3822 _proc_id: &ProcAddr,
3823 _timeout: Duration,
3824 ) -> Result<(), ProcLauncherError> {
3825 Ok(())
3826 }
3827
3828 async fn kill(&self, _proc_id: &ProcAddr) -> Result<(), ProcLauncherError> {
3829 Ok(())
3830 }
3831 }
3832
3833 #[test]
3835 #[cfg(fbcode_build)]
3836 fn test_set_launcher_then_get() {
3837 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3838
3839 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(42));
3840 let custom_ptr = Arc::as_ptr(&custom);
3841
3842 manager.set_launcher(custom).unwrap();
3844
3845 let got = manager.launcher();
3847 let got_ptr = Arc::as_ptr(got);
3848
3849 assert_eq!(
3850 custom_ptr, got_ptr,
3851 "launcher() should return the same Arc that was set"
3852 );
3853 }
3854
3855 #[test]
3858 #[cfg(fbcode_build)]
3859 fn test_get_launcher_then_set_fails() {
3860 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3861
3862 let _ = manager.launcher();
3864
3865 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(99));
3867 let result = manager.set_launcher(custom);
3868
3869 assert!(
3870 result.is_err(),
3871 "set_launcher should fail after launcher() was called"
3872 );
3873
3874 let err = result.unwrap_err();
3876 let err_msg = err.to_string();
3877 assert!(
3878 err_msg.contains("already initialized"),
3879 "error should mention 'already initialized', got: {}",
3880 err_msg
3881 );
3882 }
3883
3884 #[test]
3886 #[cfg(fbcode_build)]
3887 fn test_set_launcher_twice_fails() {
3888 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3889
3890 let first: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(1));
3891 let second: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(2));
3892
3893 manager.set_launcher(first).unwrap();
3895
3896 let result = manager.set_launcher(second);
3898 assert!(result.is_err(), "second set_launcher should fail");
3899
3900 let err = result.unwrap_err();
3902 let err_msg = err.to_string();
3903 assert!(
3904 err_msg.contains("already initialized"),
3905 "error should mention 'already initialized', got: {}",
3906 err_msg
3907 );
3908 }
3909
3910 #[test]
3912 #[cfg(fbcode_build)]
3913 fn test_launcher_initially_empty() {
3914 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3915
3916 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(123));
3920 let result = manager.set_launcher(custom);
3921
3922 assert!(
3923 result.is_ok(),
3924 "set_launcher should succeed on fresh manager"
3925 );
3926 }
3927
3928 #[test]
3930 #[cfg(fbcode_build)]
3931 fn test_launcher_idempotent() {
3932 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3933
3934 let first = manager.launcher();
3936 let second = manager.launcher();
3937
3938 assert!(
3940 Arc::ptr_eq(first, second),
3941 "launcher() should return the same Arc on repeated calls"
3942 );
3943 }
3944}