1use std::collections::HashMap;
56use std::ffi::OsString;
57use std::fmt;
58use std::path::PathBuf;
59use std::process::ExitStatus;
60use std::str::FromStr;
61use std::time::Duration;
62
63use async_trait::async_trait;
64use hyperactor::Actor;
65use hyperactor::ActorHandle;
66use hyperactor::ActorRef;
67use hyperactor::AnyActorHandle;
68use hyperactor::Context;
69use hyperactor::Endpoint;
70use hyperactor::Gateway;
71use hyperactor::Handler;
72use hyperactor::Instance;
73use hyperactor::Location;
74use hyperactor::PortRef;
75use hyperactor::Proc;
76use hyperactor::ProcAddr;
77use hyperactor::Uid;
78use hyperactor::actor::ActorStatus;
79use hyperactor::actor::StopMode;
80use hyperactor::channel::ChannelAddr;
81use hyperactor::channel::ChannelTransport;
82use hyperactor::context;
83use hyperactor::context::Actor as _;
84use hyperactor::gateway::GatewayServeHandle;
85use hyperactor::gateway::PeerAttachGuard;
86use hyperactor::mailbox::IntoBoxedMailboxSender as _;
87use hyperactor::mailbox::MailboxClient;
88use hyperactor::supervision::ActorSupervisionEvent;
89use nix::errno::Errno;
90use nix::sys::signal;
91use nix::sys::signal::Signal;
92use nix::unistd::Pid;
93use serde::Deserialize;
94use serde::Serialize;
95use tokio::process::Child;
96use tokio::process::Command;
97use tokio::sync::oneshot;
98use typeuri::Named;
99
100use super::SpawnProc;
101use super::actor_spawner_ref;
102use super::actor_spawner_uid;
103use super::spawned_proc_addr_for_spawner_addr;
104use crate::ActorSpawner;
105use crate::OrphanPolicy;
106use crate::RemoteActorDisposition;
107use crate::Supervise;
108use crate::SupervisionOptions;
109use crate::SupervisorEvent;
110use crate::TokenOptions;
111use crate::TokenPolicy;
112use crate::WorkerCommand;
113use crate::actor_spawner::SpawnActor;
114use crate::proto::SpawnProc as SpawnProcMessage;
115use crate::token;
116
117pub const UNIX_PROC_TOKEN_ENV: &str = "HYPERACTOR_REMOTE_PROC_TOKEN";
119
120#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
122pub struct UnixProcToken {
123 proc_addr: ProcAddr,
124 supervisor_events: PortRef<ActorSupervisionEvent>,
128 rendezvous: token::Token<(), UnixProcJoin>,
129}
130
131impl UnixProcToken {
132 pub fn new(
134 proc_addr: ProcAddr,
135 supervisor_events: PortRef<ActorSupervisionEvent>,
136 rendezvous: token::Token<(), UnixProcJoin>,
137 ) -> Self {
138 Self {
139 proc_addr,
140 supervisor_events,
141 rendezvous,
142 }
143 }
144
145 pub fn proc_addr(&self) -> &ProcAddr {
147 &self.proc_addr
148 }
149
150 pub fn supervisor_events(&self) -> &PortRef<ActorSupervisionEvent> {
152 &self.supervisor_events
153 }
154
155 fn join(
156 &self,
157 cx: &impl context::Actor,
158 joiner: UnixProcJoin,
159 result: PortRef<token::JoinResult<()>>,
160 ) -> anyhow::Result<()> {
161 self.rendezvous.join(cx, joiner, result)
162 }
163}
164
165impl fmt::Display for UnixProcToken {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 let token = serde_json::to_string(self).map_err(|_| fmt::Error)?;
168 f.write_str(&token)
169 }
170}
171
172impl FromStr for UnixProcToken {
173 type Err = anyhow::Error;
174
175 fn from_str(token: &str) -> Result<Self, Self::Err> {
176 Ok(serde_json::from_str(token)?)
177 }
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
184pub struct StopProc {
185 mode: StopMode,
186 reason: String,
187}
188wirevalue::register_type!(StopProc);
189
190#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
192pub struct UnixProcJoin {
193 pub gateway_addr: Location,
195 pub actor_spawner: ActorRef<SpawnActor>,
197 pub control: PortRef<StopProc>,
199}
200wirevalue::register_type!(UnixProcJoin);
201
202#[derive(Clone, Debug)]
204pub struct UnixProcCommand {
205 program: PathBuf,
206 args: Vec<OsString>,
207 env: Vec<(OsString, OsString)>,
208}
209
210impl UnixProcCommand {
211 pub fn new(program: impl Into<PathBuf>) -> Self {
213 Self {
214 program: program.into(),
215 args: Vec::new(),
216 env: Vec::new(),
217 }
218 }
219
220 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
222 self.args.push(arg.into());
223 self
224 }
225
226 pub fn args<I, S>(mut self, args: I) -> Self
228 where
229 I: IntoIterator<Item = S>,
230 S: Into<OsString>,
231 {
232 self.args.extend(args.into_iter().map(Into::into));
233 self
234 }
235
236 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
238 self.env.push((key.into(), value.into()));
239 self
240 }
241
242 fn spawn(&self, token: &UnixProcToken) -> std::io::Result<Child> {
247 let mut command = Command::new(&self.program);
248 command.args(&self.args);
249 for (key, value) in &self.env {
250 command.env(key, value);
251 }
252 command.env(UNIX_PROC_TOKEN_ENV, token.to_string());
253 command.kill_on_drop(true);
254 unsafe {
259 command.pre_exec(|| {
260 if libc::setpgid(0, 0) != 0 {
261 return Err(std::io::Error::last_os_error());
262 }
263 Ok(())
264 });
265 }
266 command.spawn()
267 }
268}
269
270#[derive(Debug)]
278#[hyperactor::export(SpawnProcMessage)]
279pub struct UnixProcSpawner {
280 command: UnixProcCommand,
281 procs: HashMap<Uid, ActorHandle<UnixProcWorker>>,
282}
283
284impl UnixProcSpawner {
285 pub fn new(program: impl Into<PathBuf>) -> Self {
287 Self::new_with_command(UnixProcCommand::new(program))
288 }
289
290 pub fn new_with_command(command: UnixProcCommand) -> Self {
292 Self {
293 command,
294 procs: HashMap::new(),
295 }
296 }
297}
298
299#[async_trait]
300impl Actor for UnixProcSpawner {
301 async fn handle_supervision_event(
302 &mut self,
303 _this: &Instance<Self>,
304 event: &ActorSupervisionEvent,
305 ) -> anyhow::Result<bool> {
306 let exited_uid = self.procs.iter().find_map(|(uid, worker)| {
307 (worker.actor_addr() == &event.actor_id).then(|| uid.clone())
308 });
309 if let Some(uid) = exited_uid {
310 self.procs.remove(&uid);
311 }
312 Ok(!event.is_error())
313 }
314
315 async fn handle_stop(
316 &mut self,
317 this: &Instance<Self>,
318 mode: StopMode,
319 reason: &str,
320 ) -> anyhow::Result<()> {
321 for worker in self.procs.values() {
325 let _ = match mode {
326 StopMode::Stop => worker.stop(reason),
327 StopMode::DrainAndStop => worker.drain_and_stop(reason),
328 };
329 }
330 this.close();
331 match mode {
332 StopMode::Stop => this.exit(reason)?,
333 StopMode::DrainAndStop => this.exit_after_drain(reason)?,
334 }
335 Ok(())
336 }
337}
338
339#[async_trait]
340impl Handler<SpawnProcMessage> for UnixProcSpawner {
341 async fn handle(
342 &mut self,
343 cx: &Context<Self>,
344 message: SpawnProcMessage,
345 ) -> anyhow::Result<()> {
346 if !message.uid.is_instance() {
347 reject_supervise(cx, &message.supervise, "spawned procs cannot be singletons");
348 return Ok(());
349 }
350 let existing_is_active = self.procs.get(&message.uid).is_some_and(|worker| {
351 let status = worker.status();
352 !status.borrow().is_terminal()
353 });
354 if existing_is_active {
355 reject_supervise(
356 cx,
357 &message.supervise,
358 format!("proc {} already exists", message.uid),
359 );
360 return Ok(());
361 }
362 self.procs.remove(&message.uid);
363
364 let spawner_addr = cx.self_addr().proc_addr();
365 let proc_addr = spawned_proc_addr_for_spawner_addr(&spawner_addr, &message.uid);
366 let actor_spawner = actor_spawner_ref(&proc_addr);
367 let worker = cx.spawn(UnixProcWorker::new(
368 message.uid.clone(),
369 proc_addr,
370 actor_spawner,
371 self.command.clone(),
372 message.supervise,
373 ));
374 self.procs.insert(message.uid, worker);
375 Ok(())
376 }
377}
378
379hyperactor::assert_behaves!(UnixProcSpawner as SpawnProc);
380
381pub struct UnixProc;
383
384impl UnixProc {
385 pub async fn boot_from_env() -> anyhow::Result<ActorStatus> {
387 let token = parse_env::<UnixProcToken>(UNIX_PROC_TOKEN_ENV)?;
388 Self::boot(token).await
389 }
390
391 pub async fn boot(token: UnixProcToken) -> anyhow::Result<ActorStatus> {
393 let _ = install_pdeathsig_kill();
395 let proc_addr = token.proc_addr().clone();
396 let advertised_location = proc_addr.location().clone();
397 Gateway::global().set_default_location(advertised_location.clone());
398 let proc = Proc::builder().proc_id(proc_addr.id().clone()).build()?;
399 proc.set_supervision_coordinator(token.supervisor_events().clone())?;
403 let actor_spawner_handle = proc.spawn_with_uid(actor_spawner_uid(), ActorSpawner)?;
404 let actor_spawner = actor_spawner_handle.bind::<SpawnActor>();
405
406 let (mut serve, gateway_addr) = Self::serve_own_socket(&proc)?;
407 let client = proc.client("unix-proc-bootstrap");
408 let (result, mut result_rx) = client.open_port::<token::JoinResult<()>>();
409 let (control, mut control_rx) = client.open_port::<StopProc>();
410 proc.set_default_location(advertised_location);
413 token.join(
414 &client,
415 UnixProcJoin {
416 gateway_addr,
417 actor_spawner,
418 control: control.bind(),
419 },
420 result.bind(),
421 )?;
422 match result_rx.recv().await? {
423 token::JoinResult::Joined { peer: _ } => {}
424 token::JoinResult::Rejected { reason } => {
425 anyhow::bail!("unix proc token join rejected: {}", reason);
426 }
427 }
428
429 let mut status = actor_spawner_handle.status();
434 let mut control_open = true;
435 let final_status = loop {
436 let current = status.borrow().clone();
437 if current.is_terminal() {
438 break current;
439 }
440 tokio::select! {
441 changed = status.changed() => {
442 if changed.is_err() {
443 break status.borrow().clone();
444 }
445 }
446 stop = control_rx.recv(), if control_open => {
447 match stop {
448 Ok(StopProc { mode, reason }) => {
449 let _ = match mode {
450 StopMode::Stop => actor_spawner_handle.stop(&reason),
451 StopMode::DrainAndStop => actor_spawner_handle.drain_and_stop(&reason),
452 };
453 }
454 Err(_) => control_open = false,
455 }
456 }
457 }
458 };
459
460 let mut proc = proc;
461 let _ = proc
462 .destroy_and_wait(Duration::from_secs(5), "actor-spawn endpoint exited")
463 .await;
464 serve.stop("actor-spawn endpoint exited");
465 let _ = serve.join().await;
466 Ok(final_status)
467 }
468
469 fn serve_own_socket(proc: &Proc) -> anyhow::Result<(GatewayServeHandle, Location)> {
477 let serve = proc
478 .gateway()
479 .serve(ChannelAddr::any(ChannelTransport::Unix))?;
480 let gateway_addr = proc.gateway().default_location();
483 Ok((serve, gateway_addr))
484 }
485}
486
487#[derive(Debug)]
488struct UnixProcSession {
489 session_id: Uid,
490 supervisor: PortRef<SupervisorEvent>,
491 options: SupervisionOptions,
492 liveness_handle: AnyActorHandle,
493}
494
495#[derive(Clone, Debug, Serialize, Deserialize, Named)]
499enum UnixProcExited {
500 Exited { code: i32 },
501 Signalled { signal: i32 },
502 WaitFailed { error: String },
503}
504wirevalue::register_type!(UnixProcExited);
505
506impl UnixProcExited {
507 fn from_wait_result(result: std::io::Result<ExitStatus>) -> Self {
508 let status = match result {
509 Ok(status) => status,
510 Err(error) => {
511 return Self::WaitFailed {
512 error: error.to_string(),
513 };
514 }
515 };
516 match (status.code(), exit_signal(&status)) {
517 (Some(code), None) => Self::Exited { code },
518 (None, Some(signal)) => Self::Signalled { signal },
519 (code, signal) => panic!(
520 "unix proc wait returned an impossible status (code={code:?}, signal={signal:?})"
521 ),
522 }
523 }
524
525 fn actor_status(&self, stop_reason: Option<&str>) -> ActorStatus {
526 match (self, stop_reason) {
527 (Self::WaitFailed { error }, _) => {
528 ActorStatus::generic_failure(format!("unix proc wait failed: {error}"))
529 }
530 (Self::Exited { .. } | Self::Signalled { .. }, Some(reason)) => {
532 ActorStatus::Stopped(reason.to_string())
533 }
534 (Self::Exited { code: 0 }, None) => {
535 ActorStatus::Stopped("unix proc exited".to_string())
536 }
537 (Self::Exited { .. } | Self::Signalled { .. }, None) => {
538 ActorStatus::generic_failure(self.describe())
539 }
540 }
541 }
542
543 fn describe(&self) -> String {
544 match self {
545 Self::WaitFailed { error } => format!("wait failed: {error}"),
546 Self::Exited { code } => format!("exited with code {code}"),
547 Self::Signalled { signal } => match Signal::try_from(*signal) {
548 Ok(sig) => format!("terminated by signal {signal} ({})", sig.as_str()),
549 Err(_) => format!("terminated by signal {signal}"),
550 },
551 }
552 }
553}
554
555#[derive(Debug)]
556#[hyperactor::export(WorkerCommand, UnixProcExited, ActorSupervisionEvent)]
557struct UnixProcWorker {
558 uid: Uid,
559 proc_addr: ProcAddr,
560 actor_spawner: ActorRef<SpawnActor>,
561 command: UnixProcCommand,
562 supervise: Option<Supervise>,
563 session: Option<UnixProcSession>,
564 peer_guard: Option<PeerAttachGuard>,
565 control: Option<PortRef<StopProc>>,
566 pid: Option<u32>,
567 stop_reason: Option<String>,
568 child_exit: Option<oneshot::Receiver<()>>,
571}
572
573impl UnixProcWorker {
574 fn new(
575 uid: Uid,
576 proc_addr: ProcAddr,
577 actor_spawner: ActorRef<SpawnActor>,
578 command: UnixProcCommand,
579 supervise: Supervise,
580 ) -> Self {
581 Self {
582 uid,
583 proc_addr,
584 actor_spawner,
585 command,
586 supervise: Some(supervise),
587 session: None,
588 peer_guard: None,
589 control: None,
590 pid: None,
591 stop_reason: None,
592 child_exit: None,
593 }
594 }
595
596 fn launch(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
597 let rendezvous = token::create(
598 this,
599 (),
600 this.port::<token::Joined<UnixProcJoin>>().bind(),
601 TokenOptions {
602 policy: TokenPolicy::Once,
603 },
604 )?;
605 let token = UnixProcToken::new(
606 self.proc_addr.clone(),
607 this.port::<ActorSupervisionEvent>().bind(),
608 rendezvous,
609 );
610
611 let mut child = self.command.spawn(&token)?;
612 self.pid = child.id();
613 let notify = this.port::<UnixProcExited>();
614 let client = Instance::<()>::self_client();
615 let (exit_tx, exit_rx) = oneshot::channel();
616 self.child_exit = Some(exit_rx);
617 tokio::spawn(async move {
618 let exited = UnixProcExited::from_wait_result(child.wait().await);
619 let _ = exit_tx.send(());
622 let _ = notify.post(client, exited);
623 });
624 Ok(())
625 }
626
627 async fn await_child_exit(&mut self, grace: Duration) -> bool {
632 let Some(exit) = self.child_exit.take() else {
633 return true;
634 };
635 matches!(tokio::time::timeout(grace, exit).await, Ok(Ok(())))
636 }
637
638 fn reject_and_stop(
639 &mut self,
640 cx: &Context<Self>,
641 supervise: &Supervise,
642 reason: impl Into<String>,
643 ) {
644 reject_supervise(cx, supervise, reason);
645 self.signal_child(Signal::SIGKILL, "rejected proc supervision");
646 }
647
648 fn signal_child(&mut self, sig: Signal, reason: &str) {
649 let Some(pid) = self.pid else {
650 return;
651 };
652 if let Err(error) = signal_proc_group(pid, sig) {
653 let _ = (error, reason);
654 }
655 }
656
657 fn ensure_session(&self, session_id: Uid) -> anyhow::Result<Uid> {
658 let Some(session) = &self.session else {
659 anyhow::bail!("unix proc worker is not supervised");
660 };
661 if session.session_id != session_id {
662 anyhow::bail!("remote supervision session id mismatch");
663 }
664 Ok(session_id)
665 }
666
667 async fn link_proc(
668 &mut self,
669 cx: &Context<'_, Self>,
670 joined: UnixProcJoin,
671 ) -> anyhow::Result<()> {
672 let Some(supervise) = self.supervise.take() else {
673 self.signal_child(Signal::SIGKILL, "duplicate unix proc join");
674 return Ok(());
675 };
676 if joined.actor_spawner != self.actor_spawner {
677 self.reject_and_stop(
678 cx,
679 &supervise,
680 format!(
681 "proc joined with unexpected actor-spawn endpoint {}; expected {}",
682 joined.actor_spawner.actor_addr(),
683 self.actor_spawner.actor_addr()
684 ),
685 );
686 return Ok(());
687 }
688
689 let gateway_addr = joined.gateway_addr.addr().clone();
690 let child_sender = match MailboxClient::dial(gateway_addr.clone()) {
691 Ok(sender) => sender,
692 Err(error) => {
693 self.reject_and_stop(
694 cx,
695 &supervise,
696 format!("failed to dial spawned proc at {}: {}", gateway_addr, error),
697 );
698 return Ok(());
699 }
700 };
701 let peer_guard = match cx
702 .instance()
703 .proc()
704 .gateway()
705 .attach_peer(self.uid.clone(), child_sender.into_boxed())
706 {
707 Ok(guard) => guard,
708 Err(error) => {
709 self.reject_and_stop(
710 cx,
711 &supervise,
712 format!("failed to attach spawned proc route: {error}"),
713 );
714 return Ok(());
715 }
716 };
717
718 let liveness_handle = match supervise.liveness.clone().spawn_worker(cx).await {
719 Ok(liveness_handle) => liveness_handle,
720 Err(error) => {
721 self.reject_and_stop(
722 cx,
723 &supervise,
724 format!("failed to spawn proc liveness actor: {error}"),
725 );
726 return Ok(());
727 }
728 };
729 let supervisor = supervise.supervisor.clone();
730 self.peer_guard = Some(peer_guard);
731 self.control = Some(joined.control);
732 self.session = Some(UnixProcSession {
733 session_id: supervise.session_id.clone(),
734 supervisor: supervise.supervisor,
735 options: supervise.options,
736 liveness_handle,
737 });
738 supervisor.post(
739 cx,
740 SupervisorEvent::Linked {
741 session_id: supervise.session_id,
742 worker: cx.port::<WorkerCommand>().bind(),
743 child: self.actor_spawner.actor_addr().clone(),
744 display_name: Some(self.uid.to_string()),
745 },
746 );
747 Ok(())
748 }
749
750 fn handle_liveness_event(
751 &mut self,
752 cx: &impl context::Actor,
753 event: &ActorSupervisionEvent,
754 ) -> anyhow::Result<bool> {
755 let Some(session) = &self.session else {
756 return Ok(true);
757 };
758 if session.liveness_handle.actor_id() != &event.actor_id {
759 return Ok(!event.is_error());
760 }
761
762 let session = self
763 .session
764 .take()
765 .expect("liveness event must have an active session");
766 let _ = session
767 .liveness_handle
768 .stop("proc supervision liveness failed");
769 session.supervisor.post(
770 cx,
771 SupervisorEvent::SupervisionEvent {
772 session_id: session.session_id.clone(),
773 event: ActorSupervisionEvent::new(
774 self.actor_spawner.actor_addr().clone(),
775 Some(self.uid.to_string()),
776 ActorStatus::generic_failure("proc supervision liveness failed"),
777 None,
778 ),
779 disposition: RemoteActorDisposition::Unreachable,
780 },
781 );
782 if session.options.orphan_policy == OrphanPolicy::Stop {
783 self.signal_child(Signal::SIGTERM, "proc supervision liveness failed");
784 }
785 Ok(true)
786 }
787
788 fn unlink_session(&mut self, reason: &str) -> Option<UnixProcSession> {
789 let session = self.session.take()?;
790 let _ = session.liveness_handle.stop(reason);
791 Some(session)
792 }
793
794 fn request_child_stop(&mut self, cx: &Context<Self>, mode: StopMode, reason: &str) {
801 self.stop_reason = Some(reason.to_string());
802 let Some(control) = &self.control else {
803 self.signal_child(Signal::SIGTERM, reason);
804 return;
805 };
806 control.post(
807 cx,
808 StopProc {
809 mode,
810 reason: reason.to_string(),
811 },
812 );
813 cx.post_after(
817 cx,
818 KillProc {
819 reason: reason.to_string(),
820 },
821 hyperactor_config::global::get(crate::config::STOP_GRACE_PERIOD),
822 );
823 }
824}
825
826#[derive(Debug)]
830struct KillProc {
831 reason: String,
832}
833
834#[async_trait]
835impl Actor for UnixProcWorker {
836 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
837 if let Err(error) = self.launch(this)
838 && let Some(supervise) = self.supervise.take()
839 {
840 reject_supervise(
841 this,
842 &supervise,
843 format!("failed to launch unix proc: {error}"),
844 );
845 this.exit("failed to launch unix proc")?;
846 }
847 Ok(())
848 }
849
850 async fn handle_supervision_event(
851 &mut self,
852 this: &Instance<Self>,
853 event: &ActorSupervisionEvent,
854 ) -> anyhow::Result<bool> {
855 self.handle_liveness_event(this, event)
856 }
857
858 async fn handle_stop(
859 &mut self,
860 this: &Instance<Self>,
861 mode: StopMode,
862 reason: &str,
863 ) -> anyhow::Result<()> {
864 self.stop_reason = Some(reason.to_string());
865 let _ = self.unlink_session(reason);
866 if let Some(control) = self.control.take() {
872 control.post(
873 this,
874 StopProc {
875 mode,
876 reason: reason.to_string(),
877 },
878 );
879 let grace = hyperactor_config::global::get(crate::config::STOP_GRACE_PERIOD);
880 if !self.await_child_exit(grace).await {
881 self.signal_child(Signal::SIGTERM, reason);
882 }
883 } else {
884 self.signal_child(Signal::SIGTERM, reason);
887 }
888 this.close();
889 match mode {
890 StopMode::Stop => this.exit(reason)?,
891 StopMode::DrainAndStop => this.exit_after_drain(reason)?,
892 }
893 Ok(())
894 }
895}
896
897#[async_trait]
898impl Handler<token::Joined<UnixProcJoin>> for UnixProcWorker {
899 async fn handle(
900 &mut self,
901 cx: &Context<Self>,
902 message: token::Joined<UnixProcJoin>,
903 ) -> anyhow::Result<()> {
904 self.link_proc(cx, message.peer).await
905 }
906}
907
908#[async_trait]
909impl Handler<WorkerCommand> for UnixProcWorker {
910 async fn handle(&mut self, cx: &Context<Self>, message: WorkerCommand) -> anyhow::Result<()> {
911 match message {
912 WorkerCommand::Stop {
913 session_id,
914 mode,
915 reason,
916 } => {
917 self.ensure_session(session_id)?;
918 self.request_child_stop(cx, mode, &reason);
923 }
924 WorkerCommand::Unlink { session_id, reason } => {
925 self.ensure_session(session_id.clone())?;
926 if let Some(session) = self.unlink_session(&reason) {
927 if session.options.orphan_policy == OrphanPolicy::Stop {
928 self.stop_reason = Some(reason.clone());
929 self.signal_child(Signal::SIGTERM, &reason);
930 }
931 let _ = session
932 .supervisor
933 .post(cx, SupervisorEvent::Unlinked { session_id, reason });
934 }
935 }
936 }
937 Ok(())
938 }
939}
940
941#[async_trait]
942impl Handler<UnixProcExited> for UnixProcWorker {
943 async fn handle(&mut self, cx: &Context<Self>, message: UnixProcExited) -> anyhow::Result<()> {
944 self.pid = None;
945 self.peer_guard.take();
946 self.control = None;
947 let actor_status = message.actor_status(self.stop_reason.as_deref());
948 if let Some(session) = self.unlink_session("unix proc exited") {
949 session.supervisor.post(
950 cx,
951 SupervisorEvent::SupervisionEvent {
952 session_id: session.session_id,
953 event: ActorSupervisionEvent::new(
954 self.actor_spawner.actor_addr().clone(),
955 Some(self.uid.to_string()),
956 actor_status,
957 None,
958 ),
959 disposition: RemoteActorDisposition::Terminal,
960 },
961 );
962 } else if let Some(supervise) = self.supervise.take() {
963 reject_supervise(
964 cx,
965 &supervise,
966 format!("unix proc exited before joining: {}", message.describe()),
967 );
968 }
969 cx.exit("unix proc exited")?;
970 Ok(())
971 }
972}
973
974#[async_trait]
975impl Handler<ActorSupervisionEvent> for UnixProcWorker {
976 async fn handle(
977 &mut self,
978 cx: &Context<Self>,
979 event: ActorSupervisionEvent,
980 ) -> anyhow::Result<()> {
981 if !event.is_error() {
986 return Ok(());
987 }
988 if let Some(session) = self.unlink_session("child proc reported a failure") {
989 session.supervisor.post(
990 cx,
991 SupervisorEvent::SupervisionEvent {
992 session_id: session.session_id,
993 event,
994 disposition: RemoteActorDisposition::Terminal,
995 },
996 );
997 }
998 self.request_child_stop(cx, StopMode::DrainAndStop, "child proc reported a failure");
999 Ok(())
1000 }
1001}
1002
1003#[async_trait]
1004impl Handler<KillProc> for UnixProcWorker {
1005 async fn handle(&mut self, _cx: &Context<Self>, message: KillProc) -> anyhow::Result<()> {
1006 self.signal_child(Signal::SIGKILL, &message.reason);
1009 Ok(())
1010 }
1011}
1012
1013fn reject_supervise(cx: &impl context::Actor, supervise: &Supervise, reason: impl Into<String>) {
1014 let _ = supervise.supervisor.post(
1015 cx,
1016 SupervisorEvent::SuperviseRejected {
1017 session_id: supervise.session_id.clone(),
1018 reason: reason.into(),
1019 },
1020 );
1021}
1022
1023fn parse_env<T>(key: &str) -> anyhow::Result<T>
1024where
1025 T: std::str::FromStr,
1026 T::Err: Into<anyhow::Error>,
1027{
1028 std::env::var(key)?
1029 .parse()
1030 .map_err(|error: T::Err| error.into())
1031}
1032
1033fn signal_proc_group(pid: u32, sig: Signal) -> nix::Result<()> {
1039 let pgid = Pid::from_raw(-(pid as i32));
1040 match signal::kill(pgid, sig) {
1041 Ok(()) | Err(Errno::ESRCH) => Ok(()),
1042 Err(err) => Err(err),
1043 }
1044}
1045
1046fn install_pdeathsig_kill() -> std::io::Result<()> {
1052 #[cfg(target_os = "linux")]
1053 {
1054 let ppid_before = unsafe { libc::getppid() };
1056 let rc = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_int) };
1058 if rc != 0 {
1059 return Err(std::io::Error::last_os_error());
1060 }
1061 if unsafe { libc::getppid() } != ppid_before {
1063 std::process::exit(0);
1064 }
1065 }
1066 Ok(())
1067}
1068
1069fn exit_signal(status: &ExitStatus) -> Option<i32> {
1070 #[cfg(unix)]
1071 {
1072 use std::os::unix::process::ExitStatusExt as _;
1073 status.signal()
1074 }
1075 #[cfg(not(unix))]
1076 {
1077 let _ = status;
1078 None
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use hyperactor::Label;
1085 use hyperactor::ProcId;
1086
1087 use super::*;
1088
1089 #[tokio::test]
1090 async fn serves_on_its_own_socket_not_the_spawner_address() {
1091 let spawner = ProcAddr::new(
1095 ProcId::new(Uid::instance(Label::strip("spawner")), None),
1096 Location::from(ChannelAddr::any(ChannelTransport::Unix)),
1097 );
1098 let proc_addr =
1099 spawned_proc_addr_for_spawner_addr(&spawner, &Uid::instance(Label::strip("child")));
1100
1101 let proc = Proc::isolated();
1102 let (mut serve, gateway_addr) = UnixProc::serve_own_socket(&proc).unwrap();
1103
1104 let served = gateway_addr
1105 .as_addr()
1106 .expect("child reports a terminal address, not a via route");
1107 assert!(
1108 matches!(served, ChannelAddr::Unix(_)),
1109 "child serves on its own unix socket, got {served}"
1110 );
1111 assert_ne!(
1112 served,
1113 proc_addr.addr(),
1114 "child must serve on its own socket, not the spawner-derived address"
1115 );
1116
1117 serve.stop("test complete");
1118 let _ = serve.join().await;
1119 }
1120
1121 #[test]
1122 fn requested_stop_reports_stopped_even_when_process_exits_by_signal() {
1123 let status = UnixProcExited::Signalled {
1124 signal: libc::SIGTERM,
1125 }
1126 .actor_status(Some("caller stopped proc"));
1127
1128 assert_eq!(
1129 status,
1130 ActorStatus::Stopped("caller stopped proc".to_string())
1131 );
1132 }
1133
1134 #[test]
1135 fn unexpected_signal_exit_reports_failure() {
1136 let status = UnixProcExited::Signalled {
1137 signal: libc::SIGTERM,
1138 }
1139 .actor_status(None);
1140
1141 assert!(status.is_failed());
1142 }
1143
1144 #[test]
1145 fn describe_names_the_terminating_signal() {
1146 let exited = UnixProcExited::Signalled {
1147 signal: libc::SIGTERM,
1148 };
1149 assert_eq!(
1151 exited.describe(),
1152 format!("terminated by signal {} (SIGTERM)", libc::SIGTERM)
1153 );
1154 }
1155}