1use std::collections::HashMap;
16use std::collections::VecDeque;
17use std::env::VarError;
18use std::fmt;
19use std::fs::OpenOptions;
20use std::future;
21use std::io;
22use std::io::Write;
23use std::path::Path;
24use std::path::PathBuf;
25use std::str::FromStr;
26use std::sync::Arc;
27use std::sync::Mutex;
28use std::sync::OnceLock;
29use std::sync::Weak;
30use std::time::Duration;
31use std::time::SystemTime;
32
33use anyhow::Context;
34use async_trait::async_trait;
35use base64::prelude::*;
36use futures::StreamExt;
37use futures::stream;
38use humantime::format_duration;
39use hyperactor::ActorAddr;
40use hyperactor::ActorHandle;
41use hyperactor::ActorRef;
42use hyperactor::Endpoint as _;
43use hyperactor::Gateway;
44use hyperactor::Label;
45use hyperactor::ProcAddr;
46use hyperactor::channel;
47use hyperactor::channel::ChannelAddr;
48use hyperactor::channel::ChannelError;
49use hyperactor::channel::ChannelTransport;
50use hyperactor::channel::Rx;
51use hyperactor::channel::Tx;
52use hyperactor::context;
53use hyperactor::id::Uid;
54use hyperactor::mailbox::IntoBoxedMailboxSender;
55use hyperactor::mailbox::MailboxClient;
56use hyperactor::mailbox::MailboxServer;
57use hyperactor::proc::Proc;
58use hyperactor_cast::cast_actor::CAST_ACTOR_NAME;
59use hyperactor_config::CONFIG;
60use hyperactor_config::ConfigAttr;
61use hyperactor_config::attrs::Attrs;
62use hyperactor_config::attrs::declare_attrs;
63use hyperactor_config::global::override_or_global;
64use serde::Deserialize;
65use serde::Serialize;
66use tempfile::TempDir;
67use tokio::process::Command;
68use tokio::sync::watch;
69use tracing::Instrument;
70use tracing::Level;
71use typeuri::Named;
72
73use crate::config::MESH_PROC_LAUNCHER_KIND;
74use crate::host::BulkTerminate;
75use crate::host::Host;
76use crate::host::HostError;
77use crate::host::ProcHandle;
78use crate::host::ProcManager;
79use crate::host::ReadyError as HostReadyError;
80use crate::host::SingleTerminate;
81use crate::host::TerminateError;
82use crate::host::TerminateSummary;
83use crate::host::WaitError;
84use crate::host_mesh::host_agent::HOST_MESH_AGENT_ACTOR_NAME;
85use crate::host_mesh::host_agent::HostAgent;
86use crate::logging::OutputTarget;
87use crate::logging::StreamFwder;
88use crate::proc_agent::ProcAgent;
89use crate::proc_launcher::LaunchOptions;
90use crate::proc_launcher::NativeProcLauncher;
91use crate::proc_launcher::ProcExitKind;
92use crate::proc_launcher::ProcExitResult;
93use crate::proc_launcher::ProcLauncher;
94use crate::proc_launcher::ProcLauncherError;
95use crate::proc_launcher::StdioHandling;
96#[cfg(target_os = "linux")]
97use crate::proc_launcher::SystemdProcLauncher;
98use crate::proc_launcher::format_process_name;
99use crate::resource;
100
101mod mailbox;
102
103declare_attrs! {
104 @meta(CONFIG = ConfigAttr::new(
120 Some("HYPERACTOR_MESH_ENABLE_LOG_FORWARDING".to_string()),
121 Some("enable_log_forwarding".to_string()),
122 ))
123 pub attr MESH_ENABLE_LOG_FORWARDING: bool = false;
124
125 @meta(CONFIG = ConfigAttr::new(
145 Some("HYPERACTOR_MESH_ENABLE_FILE_CAPTURE".to_string()),
146 Some("enable_file_capture".to_string()),
147 ))
148 pub attr MESH_ENABLE_FILE_CAPTURE: bool = false;
149
150 @meta(CONFIG = ConfigAttr::new(
154 Some("HYPERACTOR_MESH_TAIL_LOG_LINES".to_string()),
155 Some("tail_log_lines".to_string()),
156 ))
157 pub attr MESH_TAIL_LOG_LINES: usize = 0;
158
159 @meta(CONFIG = ConfigAttr::new(
166 Some("HYPERACTOR_MESH_BOOTSTRAP_ENABLE_PDEATHSIG".to_string()),
167 Some("mesh_bootstrap_enable_pdeathsig".to_string()),
168 ))
169 pub attr MESH_BOOTSTRAP_ENABLE_PDEATHSIG: bool = true;
170
171 @meta(CONFIG = ConfigAttr::new(
176 Some("HYPERACTOR_MESH_TERMINATE_CONCURRENCY".to_string()),
177 Some("mesh_terminate_concurrency".to_string()),
178 ))
179 pub attr MESH_TERMINATE_CONCURRENCY: usize = 16;
180
181 @meta(CONFIG = ConfigAttr::new(
185 Some("HYPERACTOR_MESH_TERMINATE_TIMEOUT".to_string()),
186 Some("mesh_terminate_timeout".to_string()),
187 ))
188 pub attr MESH_TERMINATE_TIMEOUT: Duration = Duration::from_secs(10);
189}
190
191pub const CLIENT_TRACE_ID_ENV: &str = "MONARCH_CLIENT_TRACE_ID";
192
193pub(crate) const BOOTSTRAP_LOG_CHANNEL: &str = "BOOTSTRAP_LOG_CHANNEL";
197
198pub(crate) const BOOTSTRAP_MODE_ENV: &str = "HYPERACTOR_MESH_BOOTSTRAP_MODE";
199pub(crate) const PROCESS_NAME_ENV: &str = "HYPERACTOR_PROCESS_NAME";
200
201#[macro_export]
202macro_rules! ok {
203 ($expr:expr $(,)?) => {
204 match $expr {
205 Ok(value) => value,
206 Err(e) => return ::anyhow::Error::from(e),
207 }
208 };
209}
210
211pub async fn halt<R>() -> R {
212 future::pending::<()>().await;
213 unreachable!()
214}
215
216pub struct HostShutdownHandle {
226 rx: tokio::sync::oneshot::Receiver<hyperactor::gateway::GatewayServeHandle>,
227 exit_on_shutdown: bool,
228}
229
230impl HostShutdownHandle {
231 pub async fn join(self) {
234 match self.rx.await {
235 Ok(mut serve_handle) => {
236 serve_handle.stop("host shutdown: draining frontend mailbox server");
240 let _ = serve_handle.join().await;
241 }
242 Err(_) => {} }
244 if self.exit_on_shutdown {
245 std::process::exit(0);
246 }
247 }
248}
249
250pub async fn host(
282 addr: ChannelAddr,
283 command: Option<BootstrapCommand>,
284 config: Option<Attrs>,
285 exit_on_shutdown: bool,
286 listener: Option<std::net::TcpListener>,
287 gateway: Gateway,
288 via: Option<ChannelAddr>,
289) -> anyhow::Result<(ActorHandle<HostAgent>, HostShutdownHandle)> {
290 if let Some(attrs) = config {
291 hyperactor_config::global::set(hyperactor_config::global::Source::Runtime, attrs);
292 tracing::debug!("bootstrap: installed Runtime config snapshot (Host)");
293 } else {
294 tracing::debug!("bootstrap: no config snapshot provided (Host)");
295 }
296
297 let command = match command {
298 Some(command) => command,
299 None => BootstrapCommand::current()?,
300 };
301 let manager = BootstrapProcManager::new(command)?;
302
303 let host = Host::new_with_gateway(manager, addr, listener, gateway, via).await?;
304 let addr = host.addr().clone();
305
306 let (shutdown_tx, shutdown_rx) =
310 tokio::sync::oneshot::channel::<hyperactor::gateway::GatewayServeHandle>();
311
312 let system_proc = host.system_proc().clone();
313 let host_mesh_agent = system_proc.spawn_with_uid(
314 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
315 HostAgent::new_process(host, Some(shutdown_tx)),
316 )?;
317 HostAgent::wait_initialized(&host_mesh_agent).await?;
318
319 let cast_handle = system_proc.spawn_with_uid(
320 Uid::singleton(Label::strip(CAST_ACTOR_NAME)),
321 hyperactor_cast::cast_actor::CastActor::default(),
322 )?;
323
324 cast_handle.bind::<hyperactor_cast::cast_actor::CastActor>();
325
326 tracing::info!(
327 "serving host at {}, agent: {}",
328 addr,
329 host_mesh_agent.bind::<HostAgent>()
330 );
331
332 Ok((
333 host_mesh_agent,
334 HostShutdownHandle {
335 rx: shutdown_rx,
336 exit_on_shutdown,
337 },
338 ))
339}
340
341#[derive(Clone, Debug, Serialize, Deserialize)]
350pub enum Bootstrap {
351 Proc {
353 proc_id: ProcAddr,
355 backend_addr: ChannelAddr,
358 callback_addr: ChannelAddr,
360 socket_dir_path: PathBuf,
364 config: Option<Attrs>,
369 },
370
371 Host {
374 addr: ChannelAddr,
376 command: Option<BootstrapCommand>,
379 config: Option<Attrs>,
384 exit_on_shutdown: bool,
386 },
387}
388
389impl Bootstrap {
390 #[allow(clippy::result_large_err)]
393 pub(crate) fn to_env_safe_string(&self) -> crate::Result<String> {
394 Ok(BASE64_STANDARD.encode(serde_json::to_string(&self)?))
395 }
396
397 #[allow(clippy::result_large_err)]
399 pub(crate) fn from_env_safe_string(str: &str) -> crate::Result<Self> {
400 let data = BASE64_STANDARD.decode(str)?;
401 let data = std::str::from_utf8(&data)?;
402 Ok(serde_json::from_str(data)?)
403 }
404
405 pub fn get_from_env() -> anyhow::Result<Option<Self>> {
408 match std::env::var("HYPERACTOR_MESH_BOOTSTRAP_MODE") {
409 Ok(mode) => match Bootstrap::from_env_safe_string(&mode) {
410 Ok(mode) => Ok(Some(mode)),
411 Err(e) => {
412 Err(anyhow::Error::from(e).context("parsing HYPERACTOR_MESH_BOOTSTRAP_MODE"))
413 }
414 },
415 Err(VarError::NotPresent) => Ok(None),
416 Err(e) => Err(anyhow::Error::from(e).context("reading HYPERACTOR_MESH_BOOTSTRAP_MODE")),
417 }
418 }
419
420 pub fn to_env(&self, cmd: &mut Command) {
422 cmd.env(
423 "HYPERACTOR_MESH_BOOTSTRAP_MODE",
424 self.to_env_safe_string().unwrap(),
425 );
426 }
427
428 pub async fn bootstrap(self) -> anyhow::Result<i32> {
432 tracing::info!(
433 "bootstrapping mesh process: {}",
434 serde_json::to_string(&self).unwrap()
435 );
436
437 if Debug::is_active() {
438 let mut buf = Vec::new();
439 writeln!(&mut buf, "bootstrapping {}:", std::process::id()).unwrap();
440 #[cfg(unix)]
441 writeln!(
442 &mut buf,
443 "\tparent pid: {}",
444 std::os::unix::process::parent_id()
445 )
446 .unwrap();
447 writeln!(
448 &mut buf,
449 "\tconfig: {}",
450 serde_json::to_string(&self).unwrap()
451 )
452 .unwrap();
453 match std::env::current_exe() {
454 Ok(path) => writeln!(&mut buf, "\tcurrent_exe: {}", path.display()).unwrap(),
455 Err(e) => writeln!(&mut buf, "\tcurrent_exe: error<{}>", e).unwrap(),
456 }
457 writeln!(&mut buf, "\targs:").unwrap();
458 for arg in std::env::args() {
459 writeln!(&mut buf, "\t\t{}", arg).unwrap();
460 }
461 writeln!(&mut buf, "\tenv:").unwrap();
462 for (key, val) in std::env::vars() {
463 writeln!(&mut buf, "\t\t{}={}", key, val).unwrap();
464 }
465 let _ = Debug.write(&buf);
466 if let Ok(s) = std::str::from_utf8(&buf) {
467 tracing::info!("{}", s);
468 } else {
469 tracing::info!("{:?}", buf);
470 }
471 }
472
473 match self {
474 Bootstrap::Proc {
475 proc_id,
476 backend_addr,
477 callback_addr,
478 socket_dir_path,
479 config,
480 } => {
481 let entered = tracing::span!(
482 Level::INFO,
483 "proc_bootstrap",
484 %proc_id,
485 %backend_addr,
486 %callback_addr,
487 socket_dir_path = %socket_dir_path.display(),
488 )
489 .entered();
490 if let Some(attrs) = config {
491 hyperactor_config::global::set(
492 hyperactor_config::global::Source::ClientOverride,
493 attrs,
494 );
495 tracing::debug!("bootstrap: installed ClientOverride config snapshot (Proc)");
496 } else {
497 tracing::debug!("bootstrap: no config snapshot provided (Proc)");
498 }
499
500 if hyperactor_config::global::get(MESH_BOOTSTRAP_ENABLE_PDEATHSIG) {
501 let _ = install_pdeathsig_kill();
506 } else {
507 eprintln!("(bootstrap) PDEATHSIG disabled via config");
508 }
509
510 let local_addr = proc_id.addr().clone();
511 let (serve_addr, _) = local_proc_addr(&socket_dir_path, proc_id.id())?;
512
513 let proc_sender = mailbox::LocalProcDialer::new(
518 local_addr.clone(),
519 socket_dir_path,
520 MailboxClient::dial(backend_addr)?,
521 );
522
523 let proc = Proc::configured(proc_id.clone(), proc_sender.into_boxed());
524
525 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<i32>();
526 let agent_handle = ProcAgent::boot_v1(proc.clone(), Some(shutdown_tx))
527 .map_err(|e| HostError::AgentSpawnFailure(proc_id, e))?;
528
529 let span = entered.exit();
530
531 let (proc_addr, proc_rx) = channel::serve(serve_addr)?;
534 let mailbox_handle = proc.clone().serve(proc_rx);
535 channel::dial(callback_addr)?
536 .send((proc_addr, agent_handle.bind::<ProcAgent>()))
537 .instrument(span)
538 .await
539 .map_err(ChannelError::from)?;
540
541 let exit_code = shutdown_rx.await.unwrap_or(1);
544 mailbox_handle.stop("process shutting down");
545 let _ = mailbox_handle.await;
546 tracing::info!("bootstrap shutting down with exit code {}", exit_code);
547 Ok(exit_code)
550 }
551 Bootstrap::Host {
552 addr,
553 command,
554 config,
555 exit_on_shutdown,
556 } => {
557 let (_agent_handle, shutdown) = host(
558 addr,
559 command,
560 config,
561 exit_on_shutdown,
562 None,
563 Gateway::global().clone(),
564 None,
565 )
566 .await?;
567 shutdown.join().await;
568 halt().await
569 }
570 }
571 }
572
573 pub async fn bootstrap_or_die(self) -> ! {
576 let exit_code = match self.bootstrap().await {
577 Ok(exit_code) => exit_code,
578 Err(err) => {
579 tracing::error!("failed to bootstrap mesh process: {}", err);
580 1
581 }
582 };
583 std::process::exit(exit_code);
584 }
585}
586
587pub fn install_pdeathsig_kill() -> io::Result<()> {
589 #[cfg(target_os = "linux")]
590 {
591 let ppid_before = unsafe { libc::getppid() };
594
595 let rc = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_int) };
598 if rc != 0 {
599 return Err(io::Error::last_os_error());
600 }
601
602 let ppid_after = unsafe { libc::getppid() };
612 if ppid_before != ppid_after {
613 std::process::exit(0);
614 }
615 }
616 Ok(())
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
634pub enum ProcStatus {
635 Starting,
638 Running { started_at: SystemTime },
641 Ready {
644 started_at: SystemTime,
645 addr: ChannelAddr,
646 agent: ActorRef<ProcAgent>,
647 },
648 Stopping { started_at: SystemTime },
652 Stopped {
655 exit_code: i32,
656 stderr_tail: Vec<String>,
657 },
658 Killed { signal: i32, core_dumped: bool },
661 Failed { reason: String },
665}
666
667impl ProcStatus {
668 #[inline]
672 pub fn is_exit(&self) -> bool {
673 matches!(
674 self,
675 ProcStatus::Stopped { .. } | ProcStatus::Killed { .. } | ProcStatus::Failed { .. }
676 )
677 }
678}
679
680impl std::fmt::Display for ProcStatus {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 match self {
683 ProcStatus::Starting => write!(f, "Starting"),
684 ProcStatus::Running { started_at } => {
685 let uptime = started_at
686 .elapsed()
687 .map(|d| format!(" up {}", format_duration(d)))
688 .unwrap_or_default();
689 write!(f, "Running{uptime}")
690 }
691 ProcStatus::Ready {
692 started_at, addr, ..
693 } => {
694 let uptime = started_at
695 .elapsed()
696 .map(|d| format!(" up {}", format_duration(d)))
697 .unwrap_or_default();
698 write!(f, "Ready at {addr}{uptime}")
699 }
700 ProcStatus::Stopping { started_at } => {
701 let uptime = started_at
702 .elapsed()
703 .map(|d| format!(" up {}", format_duration(d)))
704 .unwrap_or_default();
705 write!(f, "Stopping{uptime}")
706 }
707 ProcStatus::Stopped { exit_code, .. } => write!(f, "Stopped(exit={exit_code})"),
708 ProcStatus::Killed {
709 signal,
710 core_dumped,
711 } => {
712 if *core_dumped {
713 write!(f, "Killed(sig={signal}, core)")
714 } else {
715 write!(f, "Killed(sig={signal})")
716 }
717 }
718 ProcStatus::Failed { reason } => write!(f, "Failed({reason})"),
719 }
720 }
721}
722
723#[derive(Debug, Clone)]
725pub enum ReadyError {
726 Terminal(ProcStatus),
728 ChannelClosed,
730}
731
732impl std::fmt::Display for ReadyError {
733 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734 match self {
735 ReadyError::Terminal(st) => write!(f, "proc terminated before running: {st:?}"),
736 ReadyError::ChannelClosed => write!(f, "status channel closed"),
737 }
738 }
739}
740impl std::error::Error for ReadyError {}
741
742#[derive(Clone)]
781pub struct BootstrapProcHandle {
782 proc_id: ProcAddr,
784
785 status: Arc<std::sync::Mutex<ProcStatus>>,
791
792 launcher: Weak<dyn ProcLauncher>,
801
802 stdout_fwder: Arc<std::sync::Mutex<Option<StreamFwder>>>,
807
808 stderr_fwder: Arc<std::sync::Mutex<Option<StreamFwder>>>,
811
812 tx: tokio::sync::watch::Sender<ProcStatus>,
817
818 rx: tokio::sync::watch::Receiver<ProcStatus>,
822}
823
824impl fmt::Debug for BootstrapProcHandle {
825 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
826 let status = self.status.lock().expect("status mutex poisoned").clone();
827 f.debug_struct("BootstrapProcHandle")
828 .field("proc_id", &self.proc_id)
829 .field("status", &status)
830 .field("launcher", &"<dyn ProcLauncher>")
831 .field("tx", &"<watch::Sender>")
832 .field("rx", &"<watch::Receiver>")
833 .finish()
836 }
837}
838
839impl BootstrapProcHandle {
841 pub(crate) fn new(proc_id: ProcAddr, launcher: Weak<dyn ProcLauncher>) -> Self {
852 let (tx, rx) = watch::channel(ProcStatus::Starting);
853 Self {
854 proc_id,
855 status: Arc::new(std::sync::Mutex::new(ProcStatus::Starting)),
856 launcher,
857 stdout_fwder: Arc::new(std::sync::Mutex::new(None)),
858 stderr_fwder: Arc::new(std::sync::Mutex::new(None)),
859 tx,
860 rx,
861 }
862 }
863
864 #[inline]
866 pub fn proc_addr(&self) -> &ProcAddr {
867 &self.proc_id
868 }
869
870 #[inline]
883 pub fn watch(&self) -> tokio::sync::watch::Receiver<ProcStatus> {
884 self.rx.clone()
885 }
886
887 #[inline]
905 pub async fn changed(&self) {
906 let _ = self.watch().changed().await;
907 }
908
909 #[must_use]
922 pub fn status(&self) -> ProcStatus {
923 self.status.lock().expect("status mutex poisoned").clone()
927 }
928
929 fn transition<F>(&self, f: F) -> bool
935 where
936 F: FnOnce(&mut ProcStatus) -> bool,
937 {
938 let mut guard = self.status.lock().expect("status mutex poisoned");
939 let _before = guard.clone();
940 let changed = f(&mut guard);
941 if changed {
942 let _ = self.tx.send(guard.clone());
944 }
945 changed
946 }
947
948 pub(crate) fn mark_running(&self, started_at: SystemTime) -> bool {
958 self.transition(|st| match *st {
959 ProcStatus::Starting => {
960 *st = ProcStatus::Running { started_at };
961 true
962 }
963 _ => {
964 tracing::warn!(
965 "illegal transition: {:?} -> Running; leaving status unchanged",
966 *st
967 );
968 false
969 }
970 })
971 }
972
973 pub(crate) fn mark_ready(&self, addr: ChannelAddr, agent: ActorRef<ProcAgent>) -> bool {
985 tracing::info!(proc_id = %self.proc_id, %addr, "{} ready at {}", self.proc_id, addr);
986 self.transition(|st| match st {
987 ProcStatus::Starting => {
988 *st = ProcStatus::Ready {
991 started_at: std::time::SystemTime::now(),
992 addr,
993 agent,
994 };
995 true
996 }
997 ProcStatus::Running { started_at } => {
998 let started_at = *started_at;
999 *st = ProcStatus::Ready {
1000 started_at,
1001 addr,
1002 agent,
1003 };
1004 true
1005 }
1006 _ => {
1007 tracing::warn!(
1008 "illegal transition: {:?} -> Ready; leaving status unchanged",
1009 st
1010 );
1011 false
1012 }
1013 })
1014 }
1015
1016 pub(crate) fn mark_stopping(&self) -> bool {
1020 let now = std::time::SystemTime::now();
1021
1022 self.transition(|st| match *st {
1023 ProcStatus::Running { started_at } => {
1024 *st = ProcStatus::Stopping { started_at };
1025 true
1026 }
1027 ProcStatus::Ready { started_at, .. } => {
1028 *st = ProcStatus::Stopping { started_at };
1029 true
1030 }
1031 ProcStatus::Starting => {
1032 *st = ProcStatus::Stopping { started_at: now };
1033 true
1034 }
1035 _ => false,
1036 })
1037 }
1038
1039 pub(crate) fn mark_stopped(&self, exit_code: i32, stderr_tail: Vec<String>) -> bool {
1042 self.transition(|st| match *st {
1043 ProcStatus::Starting
1044 | ProcStatus::Running { .. }
1045 | ProcStatus::Ready { .. }
1046 | ProcStatus::Stopping { .. } => {
1047 *st = ProcStatus::Stopped {
1048 exit_code,
1049 stderr_tail,
1050 };
1051 true
1052 }
1053 _ => {
1054 tracing::warn!(
1055 "illegal transition: {:?} -> Stopped; leaving status unchanged",
1056 *st
1057 );
1058 false
1059 }
1060 })
1061 }
1062
1063 pub(crate) fn mark_killed(&self, signal: i32, core_dumped: bool) -> bool {
1066 self.transition(|st| match *st {
1067 ProcStatus::Starting
1068 | ProcStatus::Running { .. }
1069 | ProcStatus::Ready { .. }
1070 | ProcStatus::Stopping { .. } => {
1071 *st = ProcStatus::Killed {
1072 signal,
1073 core_dumped,
1074 };
1075 true
1076 }
1077 _ => {
1078 tracing::warn!(
1079 "illegal transition: {:?} -> Killed; leaving status unchanged",
1080 *st
1081 );
1082 false
1083 }
1084 })
1085 }
1086
1087 pub(crate) fn mark_failed<S: Into<String>>(&self, reason: S) -> bool {
1090 self.transition(|st| match *st {
1091 ProcStatus::Starting
1092 | ProcStatus::Running { .. }
1093 | ProcStatus::Ready { .. }
1094 | ProcStatus::Stopping { .. } => {
1095 *st = ProcStatus::Failed {
1096 reason: reason.into(),
1097 };
1098 true
1099 }
1100 _ => {
1101 tracing::warn!(
1102 "illegal transition: {:?} -> Failed; leaving status unchanged",
1103 *st
1104 );
1105 false
1106 }
1107 })
1108 }
1109
1110 #[must_use]
1129 pub async fn wait_inner(&self) -> ProcStatus {
1130 let mut rx = self.watch();
1131 loop {
1132 let st = rx.borrow().clone();
1133 if st.is_exit() {
1134 return st;
1135 }
1136 if rx.changed().await.is_err() {
1138 return st;
1139 }
1140 }
1141 }
1142
1143 pub async fn ready_inner(&self) -> Result<(), ReadyError> {
1162 let mut rx = self.watch();
1163 loop {
1164 let st = rx.borrow().clone();
1165 match &st {
1166 ProcStatus::Ready { .. } => return Ok(()),
1167 s if s.is_exit() => return Err(ReadyError::Terminal(st)),
1168 _non_terminal => {
1169 if rx.changed().await.is_err() {
1170 return Err(ReadyError::ChannelClosed);
1171 }
1172 }
1173 }
1174 }
1175 }
1176
1177 pub fn set_stream_monitors(&self, out: Option<StreamFwder>, err: Option<StreamFwder>) {
1178 *self
1179 .stdout_fwder
1180 .lock()
1181 .expect("stdout_tailer mutex poisoned") = out;
1182 *self
1183 .stderr_fwder
1184 .lock()
1185 .expect("stderr_tailer mutex poisoned") = err;
1186 }
1187
1188 fn take_stream_monitors(&self) -> (Option<StreamFwder>, Option<StreamFwder>) {
1189 let out = self
1190 .stdout_fwder
1191 .lock()
1192 .expect("stdout_tailer mutex poisoned")
1193 .take();
1194 let err = self
1195 .stderr_fwder
1196 .lock()
1197 .expect("stderr_tailer mutex poisoned")
1198 .take();
1199 (out, err)
1200 }
1201
1202 pub(crate) async fn wait_or_brutally_kill(&self, timeout: Duration) {
1209 match tokio::time::timeout(timeout, self.wait_inner()).await {
1210 Ok(st) if st.is_exit() => return,
1211 _ => {}
1212 }
1213
1214 let _ = self.mark_stopping();
1215
1216 if let Some(launcher) = self.launcher.upgrade() {
1217 let ref_proc_id: ProcAddr = self.proc_id.clone();
1218 if let Err(e) = launcher.terminate(&ref_proc_id, timeout).await {
1219 tracing::warn!(
1220 proc_id = %self.proc_id,
1221 error = %e,
1222 "wait_or_brutally_kill: launcher terminate failed, trying kill"
1223 );
1224 let _ = launcher.kill(&ref_proc_id).await;
1225 }
1226 }
1227
1228 let _ = self.wait_inner().await;
1229 }
1230
1231 async fn send_stop_all(
1235 &self,
1236 cx: &impl context::Actor,
1237 agent: ActorRef<ProcAgent>,
1238 timeout: Duration,
1239 reason: &str,
1240 ) -> anyhow::Result<ProcStatus> {
1241 let mut agent_port = agent.port();
1248 agent_port.return_undeliverable(false);
1249 agent_port.post(
1250 cx,
1251 resource::StopAll {
1252 reason: reason.to_string(),
1253 },
1254 );
1255 match tokio::time::timeout(timeout, self.wait()).await {
1258 Ok(Ok(st)) => Ok(st),
1259 Ok(Err(e)) => Err(anyhow::anyhow!("agent did not exit the process: {:?}", e)),
1260 Err(_) => Err(anyhow::anyhow!("agent did not exit the process in time")),
1261 }
1262 }
1263}
1264
1265#[async_trait]
1266impl ProcHandle for BootstrapProcHandle {
1267 type Agent = ProcAgent;
1268 type TerminalStatus = ProcStatus;
1269
1270 #[inline]
1271 fn proc_addr(&self) -> &ProcAddr {
1272 &self.proc_id
1273 }
1274
1275 #[inline]
1276 fn addr(&self) -> Option<ChannelAddr> {
1277 match &*self.status.lock().expect("status mutex poisoned") {
1278 ProcStatus::Ready { addr, .. } => Some(addr.clone()),
1279 _ => None,
1280 }
1281 }
1282
1283 #[inline]
1284 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>> {
1285 match &*self.status.lock().expect("status mutex poisoned") {
1286 ProcStatus::Ready { agent, .. } => Some(agent.clone()),
1287 _ => None,
1288 }
1289 }
1290
1291 async fn ready(&self) -> Result<(), HostReadyError<Self::TerminalStatus>> {
1302 match self.ready_inner().await {
1303 Ok(()) => Ok(()),
1304 Err(ReadyError::Terminal(status)) => Err(HostReadyError::Terminal(status)),
1305 Err(ReadyError::ChannelClosed) => Err(HostReadyError::ChannelClosed),
1306 }
1307 }
1308
1309 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError> {
1317 let status = self.wait_inner().await;
1318 if status.is_exit() {
1319 Ok(status)
1320 } else {
1321 Err(WaitError::ChannelClosed)
1322 }
1323 }
1324
1325 async fn terminate(
1346 &self,
1347 cx: &impl context::Actor,
1348 timeout: Duration,
1349 reason: &str,
1350 ) -> Result<ProcStatus, TerminateError<Self::TerminalStatus>> {
1351 let st0 = self.status();
1353 if st0.is_exit() {
1354 tracing::debug!(?st0, "terminate(): already terminal");
1355 return Err(TerminateError::AlreadyTerminated(st0));
1356 }
1357
1358 let agent = self.agent_ref();
1361 if let Some(agent) = agent {
1362 match self.send_stop_all(cx, agent.clone(), timeout, reason).await {
1363 Ok(st) => return Ok(st),
1364 Err(e) => {
1365 tracing::warn!(
1367 "ProcAgent {} could not successfully stop all actors: {}",
1368 agent.actor_addr(),
1369 e,
1370 );
1371 }
1372 }
1373 }
1374
1375 let _ = self.mark_stopping();
1377
1378 tracing::info!(proc_id = %self.proc_id, ?timeout, "terminate(): delegating to launcher");
1380 let ref_proc_id: ProcAddr = self.proc_id.clone();
1381 if let Some(launcher) = self.launcher.upgrade() {
1382 if let Err(e) = launcher.terminate(&ref_proc_id, timeout).await {
1383 tracing::warn!(proc_id = %self.proc_id, error=%e, "terminate(): launcher termination failed");
1384 return Err(TerminateError::Io(anyhow::anyhow!(
1385 "launcher termination failed: {}",
1386 e
1387 )));
1388 }
1389 } else {
1390 tracing::debug!(proc_id = %self.proc_id, "terminate(): launcher gone, proc cleanup in progress");
1392 }
1393
1394 let st = self.wait_inner().await;
1396 if st.is_exit() {
1397 tracing::info!(proc_id = %self.proc_id, ?st, "terminate(): exited");
1398 Ok(st)
1399 } else {
1400 Err(TerminateError::ChannelClosed)
1401 }
1402 }
1403
1404 async fn kill(&self) -> Result<ProcStatus, TerminateError<Self::TerminalStatus>> {
1422 let st0 = self.status();
1424 if st0.is_exit() {
1425 return Err(TerminateError::AlreadyTerminated(st0));
1426 }
1427
1428 tracing::info!(proc_id = %self.proc_id, "kill(): delegating to launcher");
1430 let ref_proc_id: ProcAddr = self.proc_id.clone();
1431 if let Some(launcher) = self.launcher.upgrade() {
1432 if let Err(e) = launcher.kill(&ref_proc_id).await {
1433 tracing::warn!(proc_id = %self.proc_id, error=%e, "kill(): launcher kill failed");
1434 return Err(TerminateError::Io(anyhow::anyhow!(
1435 "launcher kill failed: {}",
1436 e
1437 )));
1438 }
1439 } else {
1440 tracing::debug!(proc_id = %self.proc_id, "kill(): launcher gone, proc cleanup in progress");
1442 }
1443
1444 let st = self.wait_inner().await;
1446 if st.is_exit() {
1447 Ok(st)
1448 } else {
1449 Err(TerminateError::ChannelClosed)
1450 }
1451 }
1452}
1453
1454#[derive(Debug, Named, Serialize, Deserialize, Clone, Default)]
1456pub struct BootstrapCommand {
1457 pub program: PathBuf,
1458 pub arg0: Option<String>,
1459 pub args: Vec<String>,
1460 pub env: HashMap<String, String>,
1461}
1462wirevalue::register_type!(BootstrapCommand);
1463
1464impl std::hash::Hash for BootstrapCommand {
1465 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1466 self.program.hash(state);
1467 self.arg0.hash(state);
1468 self.args.hash(state);
1469 let mut pairs: Vec<_> = self.env.iter().collect();
1470 pairs.sort();
1471 pairs.hash(state);
1472 }
1473}
1474
1475impl PartialEq for BootstrapCommand {
1476 fn eq(&self, other: &Self) -> bool {
1477 self.program == other.program
1478 && self.arg0 == other.arg0
1479 && self.args == other.args
1480 && self.env == other.env
1481 }
1482}
1483
1484impl Eq for BootstrapCommand {}
1485
1486impl BootstrapCommand {
1487 pub fn current() -> io::Result<Self> {
1490 let mut args: VecDeque<String> = std::env::args().collect();
1491 let arg0 = args.pop_front();
1492
1493 Ok(Self {
1494 program: std::env::current_exe()?,
1495 arg0,
1496 args: args.into(),
1497 env: std::env::vars().collect(),
1498 })
1499 }
1500
1501 pub fn new(&self) -> Command {
1504 let mut cmd = Command::new(&self.program);
1505 if let Some(arg0) = &self.arg0 {
1506 cmd.arg0(arg0);
1507 }
1508 for arg in &self.args {
1509 cmd.arg(arg);
1510 }
1511 for (k, v) in &self.env {
1512 cmd.env(k, v);
1513 }
1514 cmd
1515 }
1516
1517 #[cfg(test)]
1524 #[cfg(fbcode_build)]
1525 pub(crate) fn test() -> Self {
1526 Self {
1527 program: crate::testresource::get("monarch/hyperactor_mesh/bootstrap"),
1528 arg0: None,
1529 args: vec![],
1530 env: HashMap::new(),
1531 }
1532 }
1533}
1534
1535impl<T: Into<PathBuf>> From<T> for BootstrapCommand {
1536 fn from(s: T) -> Self {
1538 Self {
1539 program: s.into(),
1540 arg0: None,
1541 args: vec![],
1542 env: HashMap::new(),
1543 }
1544 }
1545}
1546
1547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1570pub(crate) enum LauncherKind {
1571 Native,
1574 #[cfg(target_os = "linux")]
1577 Systemd,
1578}
1579
1580impl FromStr for LauncherKind {
1581 type Err = io::Error;
1582
1583 fn from_str(s: &str) -> Result<Self, Self::Err> {
1592 match s.trim().to_ascii_lowercase().as_str() {
1593 "" | "native" => Ok(Self::Native),
1594 #[cfg(target_os = "linux")]
1595 "systemd" => Ok(Self::Systemd),
1596 other => Err(io::Error::new(
1597 io::ErrorKind::InvalidInput,
1598 format!(
1599 "unknown proc launcher kind {other:?}; expected 'native'{}",
1600 if cfg!(target_os = "linux") {
1601 " or 'systemd'"
1602 } else {
1603 ""
1604 }
1605 ),
1606 )),
1607 }
1608 }
1609}
1610
1611pub struct BootstrapProcManager {
1640 launcher: OnceLock<Arc<dyn ProcLauncher>>,
1643
1644 command: BootstrapCommand,
1646
1647 children: Arc<tokio::sync::Mutex<HashMap<ProcAddr, BootstrapProcHandle>>>,
1651
1652 file_appender: Option<Arc<crate::logging::FileAppender>>,
1655
1656 socket_dir: TempDir,
1660}
1661
1662impl BootstrapProcManager {
1663 pub(crate) fn new(command: BootstrapCommand) -> Result<Self, io::Error> {
1670 let file_appender = if hyperactor_config::global::get(MESH_ENABLE_FILE_CAPTURE) {
1671 match crate::logging::FileAppender::new() {
1672 Some(fm) => {
1673 tracing::info!("file appender created successfully");
1674 Some(Arc::new(fm))
1675 }
1676 None => {
1677 tracing::warn!("failed to create file appender");
1678 None
1679 }
1680 }
1681 } else {
1682 None
1683 };
1684
1685 Ok(Self {
1686 launcher: OnceLock::new(),
1687 command,
1688 children: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
1689 file_appender,
1690 socket_dir: runtime_dir()?,
1691 })
1692 }
1693
1694 pub fn set_launcher(&self, launcher: Arc<dyn ProcLauncher>) -> Result<(), ProcLauncherError> {
1703 self.launcher.set(launcher).map_err(|_| {
1704 ProcLauncherError::Other(
1705 "launcher already initialized; call set_proc_launcher before first spawn".into(),
1706 )
1707 })
1708 }
1709
1710 pub fn launcher(&self) -> &Arc<dyn ProcLauncher> {
1716 self.launcher.get_or_init(|| {
1717 let kind_str = hyperactor_config::global::get_cloned(MESH_PROC_LAUNCHER_KIND);
1718 let kind: LauncherKind = kind_str.parse().unwrap_or(LauncherKind::Native);
1719 tracing::info!(kind = ?kind, config_value = %kind_str, "using default proc launcher");
1720 match kind {
1721 LauncherKind::Native => Arc::new(NativeProcLauncher::new()),
1722 #[cfg(target_os = "linux")]
1723 LauncherKind::Systemd => Arc::new(SystemdProcLauncher::new()),
1724 }
1725 })
1726 }
1727
1728 pub fn command(&self) -> &BootstrapCommand {
1730 &self.command
1731 }
1732
1733 pub fn socket_dir(&self) -> &Path {
1735 self.socket_dir.path()
1736 }
1737
1738 pub async fn status(&self, proc_id: &ProcAddr) -> Option<ProcStatus> {
1749 self.children.lock().await.get(proc_id).map(|h| h.status())
1750 }
1751
1752 pub async fn watch(
1755 &self,
1756 proc_id: &ProcAddr,
1757 ) -> Option<tokio::sync::watch::Receiver<ProcStatus>> {
1758 self.children.lock().await.get(proc_id).map(|h| h.watch())
1759 }
1760
1761 pub(crate) async fn request_stop(
1768 &self,
1769 cx: &impl context::Actor,
1770 proc: &ProcAddr,
1771 timeout: Duration,
1772 reason: &str,
1773 ) {
1774 let handle = {
1775 let guard = self.children.lock().await;
1776 guard.get(proc).cloned()
1777 };
1778
1779 let Some(handle) = handle else { return };
1780
1781 let status = handle.status();
1782 if status.is_exit() || matches!(status, ProcStatus::Stopping { .. }) {
1783 return;
1784 }
1785
1786 if let Some(agent) = handle.agent_ref() {
1787 let mut agent_port = agent.port();
1788 agent_port.return_undeliverable(false);
1789 let _ = agent_port.post(
1790 cx,
1791 resource::StopAll {
1792 reason: reason.to_string(),
1793 },
1794 );
1795 }
1796
1797 let _ = handle.mark_stopping();
1798 tokio::spawn(async move {
1799 handle.wait_or_brutally_kill(timeout).await;
1800 });
1801 }
1802
1803 fn spawn_exit_monitor(
1804 &self,
1805 proc_id: ProcAddr,
1806 handle: BootstrapProcHandle,
1807 exit_rx: tokio::sync::oneshot::Receiver<ProcExitResult>,
1808 ) {
1809 tokio::spawn(async move {
1810 let exit_result = match exit_rx.await {
1812 Ok(res) => res,
1813 Err(_) => {
1814 let _ = handle.mark_failed("exit_rx sender dropped unexpectedly");
1816 tracing::error!(
1817 name = "ProcStatus",
1818 status = "Exited::ChannelDropped",
1819 %proc_id,
1820 "exit channel closed without result"
1821 );
1822 return;
1823 }
1824 };
1825
1826 let mut stderr_tail: Vec<String> = Vec::new();
1830 let (stdout_mon, stderr_mon) = handle.take_stream_monitors();
1831
1832 if let Some(t) = stderr_mon {
1833 let (lines, _bytes) = t.abort().await;
1834 stderr_tail = lines;
1835 }
1836 if let Some(t) = stdout_mon {
1837 let (_lines, _bytes) = t.abort().await;
1838 }
1839
1840 if stderr_tail.is_empty()
1842 && let Some(tail) = exit_result.stderr_tail
1843 {
1844 stderr_tail = tail;
1845 }
1846
1847 let tail_str = if stderr_tail.is_empty() {
1848 None
1849 } else {
1850 Some(stderr_tail.join("\n"))
1851 };
1852
1853 match exit_result.kind {
1854 ProcExitKind::Exited { code } => {
1855 let _ = handle.mark_stopped(code, stderr_tail);
1856 tracing::info!(
1857 name = "ProcStatus",
1858 status = "Exited::ExitWithCode",
1859 %proc_id,
1860 exit_code = code,
1861 tail = tail_str,
1862 "proc exited with code {code}"
1863 );
1864 }
1865 ProcExitKind::Signaled {
1866 signal,
1867 core_dumped,
1868 } => {
1869 let _ = handle.mark_killed(signal, core_dumped);
1870 tracing::info!(
1871 name = "ProcStatus",
1872 status = "Exited::KilledBySignal",
1873 %proc_id,
1874 tail = tail_str,
1875 "killed by signal {signal}"
1876 );
1877 }
1878 ProcExitKind::Failed { reason } => {
1879 let _ = handle.mark_failed(&reason);
1880 tracing::info!(
1881 name = "ProcStatus",
1882 status = "Exited::Failed",
1883 %proc_id,
1884 tail = tail_str,
1885 "proc failed: {reason}"
1886 );
1887 }
1888 }
1889 });
1890 }
1891}
1892
1893pub use crate::proc_launcher::ProcBind;
1894
1895pub struct BootstrapProcConfig {
1897 pub create_rank: usize,
1899
1900 pub client_config_override: Attrs,
1903
1904 pub proc_bind: Option<ProcBind>,
1908 pub bootstrap_command: Option<BootstrapCommand>,
1911}
1912
1913#[async_trait]
1914impl ProcManager for BootstrapProcManager {
1915 type Handle = BootstrapProcHandle;
1916
1917 type Config = BootstrapProcConfig;
1918
1919 fn transport(&self) -> ChannelTransport {
1926 ChannelTransport::Unix
1927 }
1928
1929 #[hyperactor::instrument(fields(proc_id=proc_id.to_string(), addr=backend_addr.to_string()))]
1956 async fn spawn(
1957 &self,
1958 proc_id: ProcAddr,
1959 backend_addr: ChannelAddr,
1960 config: BootstrapProcConfig,
1961 ) -> Result<Self::Handle, HostError> {
1962 let (callback_addr, mut callback_rx) = channel::serve::<(ChannelAddr, ActorRef<ProcAgent>)>(
1963 ChannelAddr::any(ChannelTransport::Unix),
1964 )?;
1965
1966 let overrides = &config.client_config_override;
1968 let enable_forwarding = override_or_global(overrides, MESH_ENABLE_LOG_FORWARDING);
1969 let enable_file_capture = override_or_global(overrides, MESH_ENABLE_FILE_CAPTURE);
1970 let tail_size = override_or_global(overrides, MESH_TAIL_LOG_LINES);
1971 let need_stdio = enable_forwarding || enable_file_capture || tail_size > 0;
1972
1973 let mode = Bootstrap::Proc {
1974 proc_id: proc_id.clone(),
1975 backend_addr,
1976 callback_addr,
1977 socket_dir_path: self.socket_dir.path().to_owned(),
1978 config: Some(config.client_config_override.clone()),
1979 };
1980
1981 let bootstrap_payload = mode
1983 .to_env_safe_string()
1984 .map_err(|e| HostError::ProcessConfigurationFailure(proc_id.clone(), e.into()))?;
1985
1986 let opts = LaunchOptions {
1987 bootstrap_payload,
1988 process_name: format_process_name(&proc_id.clone()),
1989 command: config
1990 .bootstrap_command
1991 .as_ref()
1992 .unwrap_or(&self.command)
1993 .clone(),
1994 want_stdio: need_stdio,
1995 tail_lines: tail_size,
1996 log_channel: if enable_forwarding {
1997 Some(ChannelAddr::any(ChannelTransport::Unix))
1998 } else {
1999 None
2000 },
2001 proc_bind: config.proc_bind.clone(),
2002 };
2003
2004 tracing::info!(proc_id = %proc_id, "launching proc with opts={opts:?}");
2006 let ref_proc_id: ProcAddr = proc_id.clone();
2007 let launch_result = self
2008 .launcher()
2009 .launch(&ref_proc_id, opts.clone())
2010 .await
2011 .map_err(|e| {
2012 let io_err = match e {
2013 ProcLauncherError::Launch(io_err) => io_err,
2014 other => std::io::Error::other(other.to_string()),
2015 };
2016 HostError::ProcessSpawnFailure(
2017 proc_id.clone(),
2018 format!("{:?}", opts.command),
2019 io_err,
2020 )
2021 })?;
2022
2023 let (out_fwder, err_fwder) = match launch_result.stdio {
2025 StdioHandling::Captured { stdout, stderr } => {
2026 let (file_stdout, file_stderr) = if enable_file_capture {
2027 match self.file_appender.as_deref() {
2028 Some(fm) => (
2029 Some(fm.addr_for(OutputTarget::Stdout)),
2030 Some(fm.addr_for(OutputTarget::Stderr)),
2031 ),
2032 None => {
2033 tracing::warn!("enable_file_capture=true but no FileAppender");
2034 (None, None)
2035 }
2036 }
2037 } else {
2038 (None, None)
2039 };
2040
2041 let out = StreamFwder::start(
2042 stdout,
2043 file_stdout,
2044 OutputTarget::Stdout,
2045 tail_size,
2046 opts.log_channel.clone(),
2047 &ref_proc_id,
2048 config.create_rank,
2049 );
2050 let err = StreamFwder::start(
2051 stderr,
2052 file_stderr,
2053 OutputTarget::Stderr,
2054 tail_size,
2055 opts.log_channel.clone(),
2056 &ref_proc_id,
2057 config.create_rank,
2058 );
2059 (Some(out), Some(err))
2060 }
2061 StdioHandling::Inherited | StdioHandling::ManagedByLauncher => {
2062 if !need_stdio {
2063 tracing::info!(
2064 %proc_id, enable_forwarding, enable_file_capture, tail_size,
2065 "child stdio NOT captured (forwarding/file_capture/tail all disabled)"
2066 );
2067 }
2068 (None, None)
2069 }
2070 };
2071
2072 let handle = BootstrapProcHandle::new(proc_id.clone(), Arc::downgrade(self.launcher()));
2074 handle.mark_running(launch_result.started_at);
2075 handle.set_stream_monitors(out_fwder, err_fwder);
2076
2077 {
2079 let mut children = self.children.lock().await;
2080 children.insert(proc_id.clone(), handle.clone());
2081 }
2082
2083 self.spawn_exit_monitor(proc_id.clone(), handle.clone(), launch_result.exit_rx);
2086
2087 let h = handle.clone();
2089 tokio::spawn(async move {
2090 match callback_rx.recv().await {
2091 Ok((addr, agent)) => {
2092 let _ = h.mark_ready(addr, agent);
2093 }
2094 Err(e) => {
2095 let _ = h.mark_failed(format!("bootstrap callback failed: {e}"));
2097 }
2098 }
2099 });
2100
2101 Ok(handle)
2103 }
2104}
2105
2106#[async_trait]
2107impl SingleTerminate for BootstrapProcManager {
2108 async fn terminate_proc(
2118 &self,
2119 cx: &impl context::Actor,
2120 proc: &ProcAddr,
2121 timeout: Duration,
2122 reason: &str,
2123 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
2124 let proc_handle: Option<BootstrapProcHandle> = {
2126 let mut guard = self.children.lock().await;
2127 guard.remove(proc)
2128 };
2129
2130 if let Some(h) = proc_handle {
2131 h.terminate(cx, timeout, reason)
2132 .await
2133 .map(|_| (Vec::new(), Vec::new()))
2134 .map_err(|e| e.into())
2135 } else {
2136 Err(anyhow::anyhow!("proc doesn't exist: {}", proc))
2137 }
2138 }
2139}
2140
2141#[async_trait]
2142impl BulkTerminate for BootstrapProcManager {
2143 async fn terminate_all(
2157 &self,
2158 cx: &impl context::Actor,
2159 timeout: Duration,
2160 max_in_flight: usize,
2161 reason: &str,
2162 ) -> TerminateSummary {
2163 let handles: Vec<BootstrapProcHandle> = {
2166 let mut guard = self.children.lock().await;
2167 guard.drain().map(|(_, v)| v).collect()
2168 };
2169
2170 let attempted = handles.len();
2171 let mut ok = 0usize;
2172
2173 let results = stream::iter(handles.into_iter().map(|h| async move {
2174 match h.terminate(cx, timeout, reason).await {
2175 Ok(_) | Err(TerminateError::AlreadyTerminated(_)) => {
2176 true
2178 }
2179 Err(e) => {
2180 tracing::warn!(error=%e, "terminate_all: failed to terminate child");
2181 false
2182 }
2183 }
2184 }))
2185 .buffer_unordered(max_in_flight.max(1))
2186 .collect::<Vec<bool>>()
2187 .await;
2188
2189 for r in results {
2190 if r {
2191 ok += 1;
2192 }
2193 }
2194
2195 TerminateSummary {
2196 attempted,
2197 ok,
2198 failed: attempted.saturating_sub(ok),
2199 }
2200 }
2201}
2202
2203pub async fn bootstrap() -> anyhow::Result<i32> {
2220 let Some(boot) = Bootstrap::get_from_env()? else {
2221 anyhow::bail!(
2222 "bootstrap: no bootstrap mode configured (HYPERACTOR_MESH_BOOTSTRAP_MODE unset)"
2223 );
2224 };
2225 boot.bootstrap().await
2226}
2227
2228pub async fn bootstrap_or_die() -> ! {
2231 match bootstrap().await {
2232 Ok(exit_code) => std::process::exit(exit_code),
2233 Err(err) => {
2234 let _ = writeln!(Debug, "failed to bootstrap mesh process: {}", err);
2235 tracing::error!("failed to bootstrap mesh process: {}", err);
2236 std::process::exit(1);
2237 }
2238 }
2239}
2240
2241#[derive(enum_as_inner::EnumAsInner)]
2242enum DebugSink {
2243 File(std::fs::File),
2244 Sink,
2245}
2246
2247impl DebugSink {
2248 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2249 match self {
2250 DebugSink::File(f) => f.write(buf),
2251 DebugSink::Sink => Ok(buf.len()),
2252 }
2253 }
2254 fn flush(&mut self) -> io::Result<()> {
2255 match self {
2256 DebugSink::File(f) => f.flush(),
2257 DebugSink::Sink => Ok(()),
2258 }
2259 }
2260}
2261
2262fn debug_sink() -> &'static Mutex<DebugSink> {
2263 static DEBUG_SINK: OnceLock<Mutex<DebugSink>> = OnceLock::new();
2264 DEBUG_SINK.get_or_init(|| {
2265 let debug_path = {
2266 let mut p = std::env::temp_dir();
2267 if let Ok(user) = std::env::var("USER") {
2268 p.push(user);
2269 }
2270 std::fs::create_dir_all(&p).ok();
2271 p.push("monarch-bootstrap-debug.log");
2272 p
2273 };
2274 let sink = if debug_path.exists() {
2275 match OpenOptions::new()
2276 .append(true)
2277 .create(true)
2278 .open(debug_path.clone())
2279 {
2280 Ok(f) => DebugSink::File(f),
2281 Err(_e) => {
2282 eprintln!(
2283 "failed to open {} for bootstrap debug logging",
2284 debug_path.display()
2285 );
2286 DebugSink::Sink
2287 }
2288 }
2289 } else {
2290 DebugSink::Sink
2291 };
2292 Mutex::new(sink)
2293 })
2294}
2295
2296const DEBUG_TO_STDERR: bool = false;
2298
2299struct Debug;
2302
2303impl Debug {
2304 fn is_active() -> bool {
2305 DEBUG_TO_STDERR || debug_sink().lock().unwrap().is_file()
2306 }
2307}
2308
2309impl Write for Debug {
2310 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2311 let res = debug_sink().lock().unwrap().write(buf);
2312 if DEBUG_TO_STDERR {
2313 let n = match res {
2314 Ok(n) => n,
2315 Err(_) => buf.len(),
2316 };
2317 let _ = io::stderr().write_all(&buf[..n]);
2318 }
2319
2320 res
2321 }
2322 fn flush(&mut self) -> io::Result<()> {
2323 let res = debug_sink().lock().unwrap().flush();
2324 if DEBUG_TO_STDERR {
2325 let _ = io::stderr().flush();
2326 }
2327 res
2328 }
2329}
2330
2331pub(crate) fn local_proc_addr(
2333 socket_dir: &Path,
2334 proc_id: &hyperactor::id::ProcId,
2335) -> anyhow::Result<(ChannelAddr, PathBuf)> {
2336 let path = proc_id.to_path_elem(socket_dir);
2337 let addr = std::os::unix::net::SocketAddr::from_pathname(path.clone())
2338 .with_context(|| {
2339 format!(
2340 "constructing unix socket address for proc {proc_id} \
2341 at {} ({} bytes); path must fit within SUN_LEN \
2342 (108 on Linux, 104 on macOS)",
2343 path.display(),
2344 path.as_os_str().len()
2345 )
2346 })?
2347 .into();
2348 Ok((addr, path))
2349}
2350
2351fn runtime_dir() -> io::Result<TempDir> {
2355 if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") {
2356 let path = PathBuf::from(runtime_dir);
2357 if path.is_dir() {
2358 return tempfile::tempdir_in(path);
2359 }
2360 }
2361 tempfile::tempdir()
2362}
2363
2364#[cfg(test)]
2365mod tests {
2366 use std::path::PathBuf;
2367
2368 use hyperactor::RemoteSpawn;
2369 use hyperactor::channel::ChannelAddr;
2370 use hyperactor::channel::ChannelTransport;
2371 use hyperactor::channel::TcpMode;
2372 use hyperactor::testing::ids::test_proc_id;
2373 use hyperactor::testing::ids::test_proc_id_with_addr;
2374 use hyperactor_config::Flattrs;
2375
2376 use super::*;
2377
2378 #[test]
2379 fn test_bootstrap_mode_env_string_none_config_proc() {
2380 let value = Bootstrap::Proc {
2381 proc_id: test_proc_id("foo_0"),
2382 backend_addr: ChannelAddr::any(ChannelTransport::Tcp(TcpMode::Hostname)),
2383 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
2384 socket_dir_path: PathBuf::from("notexist"),
2385 config: None,
2386 };
2387
2388 let safe = value.to_env_safe_string().unwrap();
2389 let round = Bootstrap::from_env_safe_string(&safe).unwrap();
2390
2391 let safe2 = round.to_env_safe_string().unwrap();
2394 assert_eq!(safe, safe2, "env-safe round-trip should be stable");
2395
2396 match round {
2398 Bootstrap::Proc { config: None, .. } => {}
2399 other => panic!("expected Proc with None config, got {:?}", other),
2400 }
2401 }
2402
2403 #[test]
2404 fn test_bootstrap_mode_env_string_none_config_host() {
2405 let value = Bootstrap::Host {
2406 addr: ChannelAddr::any(ChannelTransport::Unix),
2407 command: None,
2408 config: None,
2409 exit_on_shutdown: false,
2410 };
2411
2412 let safe = value.to_env_safe_string().unwrap();
2413 let round = Bootstrap::from_env_safe_string(&safe).unwrap();
2414
2415 let safe2 = round.to_env_safe_string().unwrap();
2417 assert_eq!(safe, safe2);
2418
2419 match round {
2421 Bootstrap::Host { config: None, .. } => {}
2422 other => panic!("expected Host with None config, got {:?}", other),
2423 }
2424 }
2425
2426 #[test]
2427 fn test_bootstrap_mode_env_string_invalid() {
2428 assert!(Bootstrap::from_env_safe_string("!!!").is_err());
2430 }
2431
2432 #[test]
2433 fn test_bootstrap_config_snapshot_roundtrip() {
2434 let mut attrs = Attrs::new();
2436 attrs[MESH_TAIL_LOG_LINES] = 123;
2437 attrs[MESH_BOOTSTRAP_ENABLE_PDEATHSIG] = false;
2438
2439 let socket_dir = runtime_dir().unwrap();
2440
2441 {
2443 let original = Bootstrap::Proc {
2444 proc_id: test_proc_id("foo_42"),
2445 backend_addr: ChannelAddr::any(ChannelTransport::Unix),
2446 callback_addr: ChannelAddr::any(ChannelTransport::Unix),
2447 config: Some(attrs.clone()),
2448 socket_dir_path: socket_dir.path().to_owned(),
2449 };
2450 let env_str = original.to_env_safe_string().expect("encode bootstrap");
2451 let decoded = Bootstrap::from_env_safe_string(&env_str).expect("decode bootstrap");
2452 match &decoded {
2453 Bootstrap::Proc { config, .. } => {
2454 let cfg = config.as_ref().expect("expected Some(attrs)");
2455 assert_eq!(cfg[MESH_TAIL_LOG_LINES], 123);
2456 assert!(!cfg[MESH_BOOTSTRAP_ENABLE_PDEATHSIG]);
2457 }
2458 other => panic!("unexpected variant after roundtrip: {:?}", other),
2459 }
2460 }
2461
2462 {
2464 let original = Bootstrap::Host {
2465 addr: ChannelAddr::any(ChannelTransport::Unix),
2466 command: None,
2467 config: Some(attrs.clone()),
2468 exit_on_shutdown: false,
2469 };
2470 let env_str = original.to_env_safe_string().expect("encode bootstrap");
2471 let decoded = Bootstrap::from_env_safe_string(&env_str).expect("decode bootstrap");
2472 match &decoded {
2473 Bootstrap::Host { config, .. } => {
2474 let cfg = config.as_ref().expect("expected Some(attrs)");
2475 assert_eq!(cfg[MESH_TAIL_LOG_LINES], 123);
2476 assert!(!cfg[MESH_BOOTSTRAP_ENABLE_PDEATHSIG]);
2477 }
2478 other => panic!("unexpected variant after roundtrip: {:?}", other),
2479 }
2480 }
2481 }
2482
2483 #[tokio::test]
2484 async fn test_v1_child_logging() {
2485 use hyperactor::channel;
2486 use hyperactor::mailbox::BoxedMailboxSender;
2487 use hyperactor::mailbox::DialMailboxRouter;
2488 use hyperactor::mailbox::MailboxServer;
2489 use hyperactor::proc::Proc;
2490
2491 use crate::bootstrap::BOOTSTRAP_LOG_CHANNEL;
2492 use crate::logging::LogClientActor;
2493 use crate::logging::LogClientMessageClient;
2494 use crate::logging::LogForwardActor;
2495 use crate::logging::LogMessage;
2496 use crate::logging::OutputTarget;
2497 use crate::logging::test_tap;
2498
2499 let router = DialMailboxRouter::new();
2500 let (proc_addr, proc_rx) =
2501 channel::serve(ChannelAddr::any(ChannelTransport::Unix)).unwrap();
2502 let proc = Proc::configured(
2503 test_proc_id("client_0"),
2504 BoxedMailboxSender::new(router.clone()),
2505 );
2506 proc.clone().serve(proc_rx);
2507 let proc_ref: ProcAddr = test_proc_id("client_0");
2508 router.bind(proc_ref, proc_addr.clone());
2509 let client = proc.client("client");
2510
2511 let (tap_tx, mut tap_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
2512 test_tap::install(tap_tx);
2513
2514 let log_channel = ChannelAddr::any(ChannelTransport::Unix);
2515 unsafe {
2517 std::env::set_var(BOOTSTRAP_LOG_CHANNEL, log_channel.to_string());
2518 }
2519
2520 let log_client_actor = LogClientActor::new((), Flattrs::default()).await.unwrap();
2523 let log_client: ActorRef<LogClientActor> = proc.spawn(log_client_actor).bind();
2524 log_client.set_aggregate(&client, None).await.unwrap();
2525
2526 let log_forwarder_actor = LogForwardActor::new(log_client.clone(), Flattrs::default())
2529 .await
2530 .unwrap();
2531 let _log_forwarder: ActorRef<LogForwardActor> = proc.spawn(log_forwarder_actor).bind();
2532
2533 let tx = channel::dial::<LogMessage>(log_channel.clone()).unwrap();
2536
2537 tx.post(LogMessage::Log {
2540 hostname: "testhost".into(),
2541 proc_id: "testproc[0]".into(),
2542 output_target: OutputTarget::Stdout,
2543 payload: wirevalue::Any::serialize(&"hello from child".to_string()).unwrap(),
2544 });
2545
2546 let line = tokio::time::timeout(Duration::from_secs(2), tap_rx.recv())
2548 .await
2549 .expect("timed out waiting for log line")
2550 .expect("tap channel closed unexpectedly");
2551 assert!(
2552 line.contains("hello from child"),
2553 "log line did not appear via LogClientActor; got: {line}"
2554 );
2555 }
2556
2557 mod proc_handle {
2558
2559 use std::sync::Arc;
2560 use std::time::Duration;
2561
2562 use async_trait::async_trait;
2563 use hyperactor::ActorRef;
2564 use hyperactor::ProcAddr;
2565 use hyperactor::testing::ids::test_proc_id;
2566
2567 use super::super::*;
2568 use crate::host::ProcHandle;
2569 use crate::proc_launcher::LaunchOptions;
2570 use crate::proc_launcher::LaunchResult;
2571 use crate::proc_launcher::ProcLauncher;
2572 use crate::proc_launcher::ProcLauncherError;
2573
2574 struct TestProcLauncher;
2582
2583 #[async_trait]
2584 impl ProcLauncher for TestProcLauncher {
2585 async fn launch(
2586 &self,
2587 _proc_id: &ProcAddr,
2588 _opts: LaunchOptions,
2589 ) -> Result<LaunchResult, ProcLauncherError> {
2590 panic!("TestProcLauncher::launch should not be called in unit tests");
2591 }
2592
2593 async fn terminate(
2594 &self,
2595 _proc_id: &ProcAddr,
2596 _timeout: Duration,
2597 ) -> Result<(), ProcLauncherError> {
2598 panic!("TestProcLauncher::terminate should not be called in unit tests");
2599 }
2600
2601 async fn kill(&self, _proc_id: &ProcAddr) -> Result<(), ProcLauncherError> {
2602 panic!("TestProcLauncher::kill should not be called in unit tests");
2603 }
2604 }
2605
2606 fn handle_for_test() -> BootstrapProcHandle {
2613 let proc_id: ProcAddr = test_proc_id("0");
2614 let launcher: Arc<dyn ProcLauncher> = Arc::new(TestProcLauncher);
2615 BootstrapProcHandle::new(proc_id, Arc::downgrade(&launcher))
2616 }
2617
2618 #[tokio::test]
2619 async fn starting_to_running_ok() {
2620 let h = handle_for_test();
2621 assert!(matches!(h.status(), ProcStatus::Starting));
2622 let child_started_at = std::time::SystemTime::now();
2623 assert!(h.mark_running(child_started_at));
2624 match h.status() {
2625 ProcStatus::Running { started_at } => {
2626 assert_eq!(started_at, child_started_at);
2627 }
2628 other => panic!("expected Running, got {other:?}"),
2629 }
2630 }
2631
2632 #[tokio::test]
2633 async fn running_to_stopping_to_stopped_ok() {
2634 let h = handle_for_test();
2635 let child_started_at = std::time::SystemTime::now();
2636 assert!(h.mark_running(child_started_at));
2637 assert!(h.mark_stopping());
2638 assert!(matches!(h.status(), ProcStatus::Stopping { .. }));
2639 assert!(h.mark_stopped(0, Vec::new()));
2640 assert!(matches!(
2641 h.status(),
2642 ProcStatus::Stopped { exit_code: 0, .. }
2643 ));
2644 }
2645
2646 #[tokio::test]
2647 async fn running_to_killed_ok() {
2648 let h = handle_for_test();
2649 let child_started_at = std::time::SystemTime::now();
2650 assert!(h.mark_running(child_started_at));
2651 assert!(h.mark_killed(9, true));
2652 assert!(matches!(
2653 h.status(),
2654 ProcStatus::Killed {
2655 signal: 9,
2656 core_dumped: true
2657 }
2658 ));
2659 }
2660
2661 #[tokio::test]
2662 async fn running_to_failed_ok() {
2663 let h = handle_for_test();
2664 let child_started_at = std::time::SystemTime::now();
2665 assert!(h.mark_running(child_started_at));
2666 assert!(h.mark_failed("bootstrap error"));
2667 match h.status() {
2668 ProcStatus::Failed { reason } => {
2669 assert_eq!(reason, "bootstrap error");
2670 }
2671 other => panic!("expected Failed(\"bootstrap error\"), got {other:?}"),
2672 }
2673 }
2674
2675 #[tokio::test]
2676 async fn illegal_transitions_are_rejected() {
2677 let h = handle_for_test();
2678 let child_started_at = std::time::SystemTime::now();
2679 assert!(h.mark_running(child_started_at));
2681 assert!(!h.mark_running(std::time::SystemTime::now()));
2682 assert!(matches!(h.status(), ProcStatus::Running { .. }));
2683 assert!(h.mark_stopping());
2685 assert!(h.mark_stopped(0, Vec::new()));
2686 assert!(!h.mark_running(child_started_at));
2687 assert!(!h.mark_killed(9, false));
2688 assert!(!h.mark_failed("nope"));
2689
2690 assert!(matches!(
2691 h.status(),
2692 ProcStatus::Stopped { exit_code: 0, .. }
2693 ));
2694 }
2695
2696 #[tokio::test]
2697 async fn transitions_from_ready_are_legal() {
2698 let h = handle_for_test();
2699 let addr = ChannelAddr::any(ChannelTransport::Unix);
2700 let t0 = std::time::SystemTime::now();
2702 assert!(h.mark_running(t0));
2703 let proc_id = <BootstrapProcHandle as ProcHandle>::proc_addr(&h);
2706 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
2707 let agent_ref: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
2708 assert!(h.mark_ready(addr, agent_ref));
2710 assert!(h.mark_stopping());
2711 assert!(h.mark_stopped(0, Vec::new()));
2712 }
2713
2714 #[tokio::test]
2715 async fn ready_to_killed_is_legal() {
2716 let h = handle_for_test();
2717 let addr = ChannelAddr::any(ChannelTransport::Unix);
2718 let t0 = std::time::SystemTime::now();
2720 assert!(h.mark_running(t0));
2721 let proc_id = <BootstrapProcHandle as ProcHandle>::proc_addr(&h);
2724 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
2725 let agent: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
2726 assert!(h.mark_ready(addr, agent));
2728 assert!(h.mark_killed(9, false));
2730 }
2731
2732 #[tokio::test]
2733 async fn mark_failed_from_stopping_is_allowed() {
2734 let h = handle_for_test();
2735
2736 assert!(h.mark_stopping(), "precondition: to Stopping");
2738
2739 assert!(
2741 h.mark_failed("boom"),
2742 "mark_failed() should succeed from Stopping"
2743 );
2744 match h.status() {
2745 ProcStatus::Failed { reason } => assert_eq!(reason, "boom"),
2746 other => panic!("expected Failed(\"boom\"), got {other:?}"),
2747 }
2748 }
2749 }
2750
2751 struct TestLauncher;
2757
2758 #[async_trait::async_trait]
2759 impl crate::proc_launcher::ProcLauncher for TestLauncher {
2760 async fn launch(
2761 &self,
2762 _proc_id: &ProcAddr,
2763 _opts: crate::proc_launcher::LaunchOptions,
2764 ) -> Result<crate::proc_launcher::LaunchResult, crate::proc_launcher::ProcLauncherError>
2765 {
2766 panic!("TestLauncher::launch should not be called in unit tests");
2767 }
2768
2769 async fn terminate(
2770 &self,
2771 _proc_id: &ProcAddr,
2772 _timeout: std::time::Duration,
2773 ) -> Result<(), crate::proc_launcher::ProcLauncherError> {
2774 panic!("TestLauncher::terminate should not be called in unit tests");
2775 }
2776
2777 async fn kill(
2778 &self,
2779 _proc_id: &ProcAddr,
2780 ) -> Result<(), crate::proc_launcher::ProcLauncherError> {
2781 panic!("TestLauncher::kill should not be called in unit tests");
2782 }
2783 }
2784
2785 fn test_handle(proc_id: ProcAddr) -> BootstrapProcHandle {
2786 let launcher: std::sync::Arc<dyn crate::proc_launcher::ProcLauncher> =
2787 std::sync::Arc::new(TestLauncher);
2788 BootstrapProcHandle::new(proc_id, std::sync::Arc::downgrade(&launcher))
2789 }
2790
2791 #[tokio::test]
2792 async fn watch_notifies_on_status_changes() {
2793 let proc_id = test_proc_id("1");
2794 let handle = test_handle(proc_id);
2795 let mut rx = handle.watch();
2796
2797 let now = std::time::SystemTime::now();
2799 assert!(handle.mark_running(now));
2800 rx.changed().await.ok(); match &*rx.borrow() {
2802 ProcStatus::Running { started_at } => {
2803 assert_eq!(*started_at, now);
2804 }
2805 s => panic!("expected Running, got {s:?}"),
2806 }
2807
2808 assert!(handle.mark_stopped(0, Vec::new()));
2810 rx.changed().await.ok(); assert!(matches!(
2812 &*rx.borrow(),
2813 ProcStatus::Stopped { exit_code: 0, .. }
2814 ));
2815 }
2816
2817 #[tokio::test]
2818 async fn ready_errs_if_process_exits_before_running() {
2819 let proc_id =
2820 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "early-exit");
2821 let handle = test_handle(proc_id);
2822
2823 assert!(handle.mark_stopped(7, Vec::new()));
2826
2827 match handle.ready_inner().await {
2829 Ok(()) => panic!("ready() unexpectedly succeeded"),
2830 Err(ReadyError::Terminal(ProcStatus::Stopped { exit_code, .. })) => {
2831 assert_eq!(exit_code, 7)
2832 }
2833 Err(other) => panic!("expected Stopped(7), got {other:?}"),
2834 }
2835 }
2836
2837 #[tokio::test]
2838 async fn status_unknown_proc_is_none() {
2839 let manager = BootstrapProcManager::new(BootstrapCommand {
2840 program: PathBuf::from("/bin/true"),
2841 ..Default::default()
2842 })
2843 .unwrap();
2844 let unknown = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "nope");
2845 assert!(manager.status(&unknown).await.is_none());
2846 }
2847
2848 #[tokio::test]
2849 async fn handle_ready_allows_waiters() {
2850 let proc_id = test_proc_id("42");
2851 let handle = test_handle(proc_id.clone());
2852
2853 let started_at = std::time::SystemTime::now();
2854 assert!(handle.mark_running(started_at));
2855
2856 let actor_id = proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME);
2857 let agent_ref: ActorRef<ProcAgent> = ActorRef::attest(actor_id);
2858
2859 let ready_addr = ChannelAddr::any(ChannelTransport::Unix);
2862
2863 assert!(handle.mark_ready(ready_addr.clone(), agent_ref));
2865 handle
2866 .ready_inner()
2867 .await
2868 .expect("ready_inner() should complete after Ready");
2869
2870 match handle.status() {
2873 ProcStatus::Ready {
2874 started_at: t,
2875 addr: a,
2876 ..
2877 } => {
2878 assert_eq!(t, started_at);
2879 assert_eq!(a, ready_addr);
2880 }
2881 other => panic!("expected Ready, got {other:?}"),
2882 }
2883 }
2884
2885 #[test]
2886 fn display_running_includes_uptime() {
2887 let started_at = std::time::SystemTime::now() - Duration::from_secs(42);
2888 let st = ProcStatus::Running { started_at };
2889
2890 let s = format!("{}", st);
2891 assert!(s.contains("Running"));
2892 assert!(s.contains("42s"));
2893 }
2894
2895 #[test]
2896 fn display_ready_includes_addr() {
2897 let started_at = std::time::SystemTime::now() - Duration::from_secs(5);
2898 let addr = ChannelAddr::any(ChannelTransport::Unix);
2899 let agent = ActorRef::attest(
2900 test_proc_id_with_addr(addr.clone(), "proc")
2901 .actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME),
2902 );
2903
2904 let st = ProcStatus::Ready {
2905 started_at,
2906 addr: addr.clone(),
2907 agent,
2908 };
2909
2910 let s = format!("{}", st);
2911 assert!(s.contains(&addr.to_string())); assert!(s.contains("Ready"));
2913 }
2914
2915 #[test]
2916 fn display_stopped_includes_exit_code() {
2917 let st = ProcStatus::Stopped {
2918 exit_code: 7,
2919 stderr_tail: Vec::new(),
2920 };
2921 let s = format!("{}", st);
2922 assert!(s.contains("Stopped"));
2923 assert!(s.contains("7"));
2924 }
2925
2926 #[test]
2927 fn display_other_variants_does_not_panic() {
2928 let samples = vec![
2929 ProcStatus::Starting,
2930 ProcStatus::Stopping {
2931 started_at: std::time::SystemTime::now(),
2932 },
2933 ProcStatus::Ready {
2934 started_at: std::time::SystemTime::now(),
2935 addr: ChannelAddr::any(ChannelTransport::Unix),
2936 agent: ActorRef::attest(
2937 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "x")
2938 .actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME),
2939 ),
2940 },
2941 ProcStatus::Killed {
2942 signal: 9,
2943 core_dumped: false,
2944 },
2945 ProcStatus::Failed {
2946 reason: "boom".into(),
2947 },
2948 ];
2949
2950 for st in samples {
2951 let _ = format!("{}", st); }
2953 }
2954
2955 #[tokio::test]
2956 async fn proc_handle_ready_ok_through_trait() {
2957 let proc_id =
2958 test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "ph-ready-ok");
2959 let handle = test_handle(proc_id.clone());
2960
2961 let t0 = std::time::SystemTime::now();
2963 assert!(handle.mark_running(t0));
2964
2965 let addr = ChannelAddr::any(ChannelTransport::Unix);
2967 let agent: ActorRef<ProcAgent> =
2968 ActorRef::attest(proc_id.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME));
2969 assert!(handle.mark_ready(addr, agent));
2970
2971 let r = <BootstrapProcHandle as ProcHandle>::ready(&handle).await;
2973 assert!(r.is_ok(), "expected Ok(()), got {r:?}");
2974 }
2975
2976 #[tokio::test]
2977 async fn proc_handle_wait_returns_terminal_status() {
2978 let proc_id = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "ph-wait");
2979 let handle = test_handle(proc_id);
2980
2981 assert!(handle.mark_stopped(0, Vec::new()));
2983
2984 let st = <BootstrapProcHandle as ProcHandle>::wait(&handle)
2986 .await
2987 .expect("wait should return Ok(terminal)");
2988
2989 match st {
2990 ProcStatus::Stopped { exit_code, .. } => assert_eq!(exit_code, 0),
2991 other => panic!("expected Stopped(0), got {other:?}"),
2992 }
2993 }
2994
2995 #[tokio::test]
2996 async fn ready_wrapper_maps_terminal_to_trait_error() {
2997 let proc_id = test_proc_id_with_addr(ChannelAddr::any(ChannelTransport::Unix), "wrap");
2998 let handle = test_handle(proc_id);
2999
3000 assert!(handle.mark_stopped(7, Vec::new()));
3001
3002 match <BootstrapProcHandle as ProcHandle>::ready(&handle).await {
3003 Ok(()) => panic!("expected Err"),
3004 Err(HostReadyError::Terminal(ProcStatus::Stopped { exit_code, .. })) => {
3005 assert_eq!(exit_code, 7);
3006 }
3007 Err(e) => panic!("unexpected error: {e:?}"),
3008 }
3009 }
3010
3011 #[cfg(fbcode_build)]
3021 async fn make_proc_id_and_backend_addr(
3022 instance: &hyperactor::Client,
3023 _tag: &str,
3024 ) -> (ProcAddr, ChannelAddr) {
3025 let (backend_addr, rx) = channel::serve(ChannelAddr::any(ChannelTransport::Unix)).unwrap();
3028
3029 instance.proc().clone().serve(rx);
3033
3034 let proc_id = test_proc_id_with_addr(ChannelTransport::Unix.any(), "proc");
3037 (proc_id, backend_addr)
3038 }
3039
3040 #[tokio::test]
3041 #[cfg(fbcode_build)]
3042 async fn bootstrap_handle_terminate_graceful() {
3043 let root =
3045 hyperactor::Proc::direct(ChannelTransport::Unix.any(), "root".to_string()).unwrap();
3046 let instance = root.client("client");
3047
3048 let mgr = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3049 let (proc_id, backend_addr) = make_proc_id_and_backend_addr(&instance, "t_term").await;
3050 let handle = mgr
3051 .spawn(
3052 proc_id.clone(),
3053 backend_addr.clone(),
3054 BootstrapProcConfig {
3055 create_rank: 0,
3056 client_config_override: Attrs::new(),
3057 proc_bind: None,
3058 bootstrap_command: None,
3059 },
3060 )
3061 .await
3062 .expect("spawn bootstrap child");
3063
3064 handle.ready().await.expect("ready");
3065
3066 let deadline = Duration::from_secs(2);
3067 match tokio::time::timeout(
3068 deadline * 2,
3069 handle.terminate(&instance, deadline, "test terminate"),
3070 )
3071 .await
3072 {
3073 Err(_) => panic!("terminate() future hung"),
3074 Ok(Ok(st)) => {
3075 match st {
3076 ProcStatus::Stopped { exit_code, .. } => {
3077 assert_eq!(exit_code, 0, "expected clean exit; got {exit_code}");
3079 }
3080 ProcStatus::Killed { signal, .. } => {
3081 assert_eq!(signal, libc::SIGTERM, "expected SIGTERM; got {signal}");
3099 }
3100 other => panic!("expected Stopped or Killed(SIGTERM); got {other:?}"),
3101 }
3102 }
3103 Ok(Err(e)) => panic!("terminate() failed: {e:?}"),
3104 }
3105 }
3106
3107 #[tokio::test]
3108 #[cfg(fbcode_build)]
3109 async fn bootstrap_handle_kill_forced() {
3110 let root =
3112 hyperactor::Proc::direct(ChannelTransport::Unix.any(), "root".to_string()).unwrap();
3113 let instance = root.client("client");
3114
3115 let mgr = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3116
3117 let (proc_id, backend_addr) = make_proc_id_and_backend_addr(&instance, "t_kill").await;
3119
3120 let handle = mgr
3122 .spawn(
3123 proc_id.clone(),
3124 backend_addr.clone(),
3125 BootstrapProcConfig {
3126 create_rank: 0,
3127 client_config_override: Attrs::new(),
3128 proc_bind: None,
3129 bootstrap_command: None,
3130 },
3131 )
3132 .await
3133 .expect("spawn bootstrap child");
3134
3135 handle.ready().await.expect("ready");
3138
3139 let deadline = Duration::from_secs(5);
3142 match tokio::time::timeout(deadline, handle.kill()).await {
3143 Err(_) => panic!("kill() future hung"),
3144 Ok(Ok(st)) => {
3145 match st {
3147 ProcStatus::Killed { signal, .. } => {
3148 assert_eq!(signal, libc::SIGKILL, "expected SIGKILL; got {}", signal);
3150 }
3151 other => panic!("expected Killed status after kill(); got: {other:?}"),
3152 }
3153 }
3154 Ok(Err(e)) => panic!("kill() failed: {e:?}"),
3155 }
3156 }
3157
3158 #[tokio::test]
3159 #[cfg(fbcode_build)]
3160 async fn test_host_bootstrap() {
3161 use crate::host_mesh::host_agent::GetLocalProcClient;
3162 use crate::proc_agent::NewClientInstanceClient;
3163
3164 let temp_proc = Proc::isolated();
3167 let temp_instance = temp_proc.client("temp");
3168
3169 let handle = host(
3170 ChannelAddr::any(ChannelTransport::Unix),
3171 Some(BootstrapCommand::test()),
3172 None,
3173 false,
3174 None,
3175 Gateway::global().clone(),
3176 None,
3177 )
3178 .await
3179 .unwrap();
3180
3181 let local_proc = handle.0.get_local_proc(&temp_instance).await.unwrap();
3182 let _local_instance = local_proc
3183 .new_client_instance(&temp_instance)
3184 .await
3185 .unwrap();
3186 }
3187
3188 use std::time::Duration;
3194
3195 use crate::proc_launcher::LaunchOptions;
3196 use crate::proc_launcher::LaunchResult;
3197 use crate::proc_launcher::ProcExitKind;
3198 use crate::proc_launcher::ProcExitResult;
3199 use crate::proc_launcher::ProcLauncher;
3200 use crate::proc_launcher::ProcLauncherError;
3201 use crate::proc_launcher::StdioHandling;
3202
3203 #[allow(dead_code)]
3206 struct DummyLauncher {
3207 marker: u64,
3209 }
3210
3211 impl DummyLauncher {
3212 #[allow(dead_code)]
3213 fn new(marker: u64) -> Self {
3214 Self { marker }
3215 }
3216
3217 #[allow(dead_code)]
3218 fn marker(&self) -> u64 {
3219 self.marker
3220 }
3221 }
3222
3223 #[async_trait::async_trait]
3224 impl ProcLauncher for DummyLauncher {
3225 async fn launch(
3226 &self,
3227 _proc_id: &ProcAddr,
3228 _opts: LaunchOptions,
3229 ) -> Result<LaunchResult, ProcLauncherError> {
3230 let (tx, rx) = tokio::sync::oneshot::channel();
3231 let _ = tx.send(ProcExitResult {
3233 kind: ProcExitKind::Exited { code: 0 },
3234 stderr_tail: Some(vec![]),
3235 });
3236 Ok(LaunchResult {
3237 pid: None,
3238 started_at: std::time::SystemTime::now(),
3239 stdio: StdioHandling::ManagedByLauncher,
3240 exit_rx: rx,
3241 })
3242 }
3243
3244 async fn terminate(
3245 &self,
3246 _proc_id: &ProcAddr,
3247 _timeout: Duration,
3248 ) -> Result<(), ProcLauncherError> {
3249 Ok(())
3250 }
3251
3252 async fn kill(&self, _proc_id: &ProcAddr) -> Result<(), ProcLauncherError> {
3253 Ok(())
3254 }
3255 }
3256
3257 #[test]
3259 #[cfg(fbcode_build)]
3260 fn test_set_launcher_then_get() {
3261 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3262
3263 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(42));
3264 let custom_ptr = Arc::as_ptr(&custom);
3265
3266 manager.set_launcher(custom).unwrap();
3268
3269 let got = manager.launcher();
3271 let got_ptr = Arc::as_ptr(got);
3272
3273 assert_eq!(
3274 custom_ptr, got_ptr,
3275 "launcher() should return the same Arc that was set"
3276 );
3277 }
3278
3279 #[test]
3282 #[cfg(fbcode_build)]
3283 fn test_get_launcher_then_set_fails() {
3284 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3285
3286 let _ = manager.launcher();
3288
3289 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(99));
3291 let result = manager.set_launcher(custom);
3292
3293 assert!(
3294 result.is_err(),
3295 "set_launcher should fail after launcher() was called"
3296 );
3297
3298 let err = result.unwrap_err();
3300 let err_msg = err.to_string();
3301 assert!(
3302 err_msg.contains("already initialized"),
3303 "error should mention 'already initialized', got: {}",
3304 err_msg
3305 );
3306 }
3307
3308 #[test]
3310 #[cfg(fbcode_build)]
3311 fn test_set_launcher_twice_fails() {
3312 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3313
3314 let first: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(1));
3315 let second: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(2));
3316
3317 manager.set_launcher(first).unwrap();
3319
3320 let result = manager.set_launcher(second);
3322 assert!(result.is_err(), "second set_launcher should fail");
3323
3324 let err = result.unwrap_err();
3326 let err_msg = err.to_string();
3327 assert!(
3328 err_msg.contains("already initialized"),
3329 "error should mention 'already initialized', got: {}",
3330 err_msg
3331 );
3332 }
3333
3334 #[test]
3336 #[cfg(fbcode_build)]
3337 fn test_launcher_initially_empty() {
3338 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3339
3340 let custom: Arc<dyn ProcLauncher> = Arc::new(DummyLauncher::new(123));
3344 let result = manager.set_launcher(custom);
3345
3346 assert!(
3347 result.is_ok(),
3348 "set_launcher should succeed on fresh manager"
3349 );
3350 }
3351
3352 #[test]
3354 #[cfg(fbcode_build)]
3355 fn test_launcher_idempotent() {
3356 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
3357
3358 let first = manager.launcher();
3360 let second = manager.launcher();
3361
3362 assert!(
3364 Arc::ptr_eq(first, second),
3365 "launcher() should return the same Arc on repeated calls"
3366 );
3367 }
3368}