Skip to main content

capsule_runtime/guest_worker/
supervisor.rs

1//! The supervisor side: starts each worker at most once, sends it guests and calls, and answers
2//! its host calls with the objects it registered for the call in flight.
3
4use std::collections::HashMap;
5use std::os::unix::net::UnixStream;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Condvar, Mutex};
8use std::time::{Duration, Instant};
9
10use http_body_util::BodyExt;
11use tokio::sync::oneshot;
12use wasmtime::component::Component;
13use wasmtime_wasi_http::WasiHttpHooks;
14
15use super::http::{head_of, request_from_head, to_wire};
16use super::wire::{
17    read_frame, write_frame, FromWorker, HostReply, HostRequest, Reply, StreamEvent, ToWorker,
18};
19use super::{Launcher, WorkerKind, PROTOCOL_VERSION};
20use crate::bindings::hook::exports::murmur::hook::lifecycle::{HookOutput, StageEvent};
21use crate::bindings::host::murmur::tool::run::{ToolInput, ToolResult};
22use crate::errors::RuntimeError;
23use crate::hooks::{HookArg, HookCallHost, HookSpec};
24use crate::inference_import::HookInferencePurpose;
25use crate::limits::{CallDeadlines, ExecutionLimits};
26use crate::process_driver::{DriverAnswer, DriverCall};
27use crate::runtime::{
28    CapsuleSpec, CapsuleStoreState, NetworkPolicyHooks, ToolCallHost, ToolCallSpec,
29};
30use crate::session_process::RemoteProcessHost;
31use crate::stream_events::StreamEventsTarget;
32
33/// How long past a call's own bound the supervisor waits for a worker to answer before killing it
34/// and treating it as lost.
35pub(crate) const UNRESPONSIVE_GRACE: Duration = Duration::from_secs(5);
36
37/// How long a worker has to say hello once started.
38const HANDSHAKE_WAIT: Duration = Duration::from_secs(60);
39
40/// Request, call and guest ids: issued from one counter, so an id from one connection is never in
41/// flight on another.
42static NEXT_ID: AtomicU64 = AtomicU64::new(1);
43
44fn next_id() -> u64 {
45    NEXT_ID.fetch_add(1, Ordering::Relaxed)
46}
47
48/// Where a worker's start stands.
49#[derive(Debug, Clone)]
50enum Start {
51    Starting,
52    /// Forked and exec'd, or running on a thread; not yet past the handshake.
53    Spawned,
54    Ready {
55        pid: u32,
56    },
57    Failed(String),
58}
59
60/// What the supervisor holds for one guest call in flight: the objects its host imports reach.
61struct CallEntry {
62    description: String,
63    /// When the call was registered: the oldest call in flight is the one a loss names.
64    registered: Instant,
65    http: Arc<Mutex<NetworkPolicyHooks>>,
66    events: StreamEventsTarget,
67    input: crate::runtime::InputWiring,
68    process: tokio::sync::Mutex<RemoteProcessHost>,
69    /// A hook's `inference`, `task-io` and `conversation` objects, for a hook call.
70    hook: Option<Arc<HookCallHost>>,
71    /// The script capsule's session state, for its root's run: its registry, artifact manager,
72    /// message send and `wasi:http` policy.
73    capsule: Option<Arc<Mutex<CapsuleStoreState>>>,
74    /// The response bodies this call is streaming, stopped when the call ends.
75    bodies: Mutex<Vec<tokio::task::AbortHandle>>,
76}
77
78impl CallEntry {
79    /// A call whose guest reaches nothing past WASI: every other host import is refused or
80    /// unavailable, as it is for a store that links none.
81    fn bare(description: String) -> Self {
82        Self {
83            description,
84            registered: Instant::now(),
85            http: Arc::new(Mutex::new(NetworkPolicyHooks {
86                network_allow_rules: Vec::new(),
87                gateway: None,
88                formation: None,
89                task_provenance: None,
90                member_requests: None,
91            })),
92            events: StreamEventsTarget {
93                sse: None,
94                task: None,
95                chunks_emitted: Arc::new(std::sync::atomic::AtomicBool::new(false)),
96                tool_calls: None,
97            },
98            input: crate::runtime::InputWiring::unavailable(),
99            process: tokio::sync::Mutex::new(RemoteProcessHost::new(
100                crate::session_process::ProcessAccess::Unavailable(
101                    crate::session_process::unavailable::NOT_AGENT_TOOL_CALL,
102                ),
103            )),
104            hook: None,
105            capsule: None,
106            bodies: Mutex::new(Vec::new()),
107        }
108    }
109}
110
111impl Drop for CallEntry {
112    fn drop(&mut self) {
113        for body in self
114            .bodies
115            .get_mut()
116            .unwrap_or_else(std::sync::PoisonError::into_inner)
117            .drain(..)
118        {
119            body.abort();
120        }
121    }
122}
123
124/// One worker's connection, seen from the supervisor.
125pub(crate) struct WorkerConn {
126    kind: WorkerKind,
127    start: Mutex<Start>,
128    started: Condvar,
129    /// Woken with [`Self::started`], for a waiter on an async runtime.
130    started_async: tokio::sync::Notify,
131    writer: Mutex<Option<UnixStream>>,
132    pending: Mutex<HashMap<u64, oneshot::Sender<Result<Reply, String>>>>,
133    calls: Mutex<HashMap<u64, Arc<CallEntry>>>,
134    /// The guest being handed over to the worker, named as the loss message names a call.
135    loading: Mutex<Option<String>>,
136    /// Set once the connection is lost: every later request fails with it.
137    lost: Mutex<Option<String>>,
138    /// Set when the supervisor killed the worker for not answering, to the call it stopped
139    /// answering in: the loss then says so, instead of reporting the supervisor's own `SIGKILL`
140    /// as if something else had sent it.
141    unresponsive: Mutex<Option<String>>,
142    /// The session-wide loss latch this connection reports into.
143    session_lost: Arc<Mutex<Option<String>>>,
144    host_runtime: tokio::runtime::Handle,
145    /// A frame tap for tests: every supervisor-to-worker frame, encoded.
146    #[cfg(test)]
147    tap: Option<Arc<Mutex<Vec<Vec<u8>>>>>,
148}
149
150impl WorkerConn {
151    fn send(&self, message: &ToWorker) -> Result<(), String> {
152        #[cfg(test)]
153        if let Some(tap) = &self.tap {
154            if let Ok(frame) = super::wire::encode(message) {
155                tap.lock()
156                    .unwrap_or_else(std::sync::PoisonError::into_inner)
157                    .push(frame);
158            }
159        }
160        let mut writer = self
161            .writer
162            .lock()
163            .unwrap_or_else(std::sync::PoisonError::into_inner);
164        match writer.as_mut() {
165            Some(writer) => write_frame(writer, message),
166            None => Err("the guest worker is not connected".to_string()),
167        }
168    }
169
170    /// The worker's pid once started, waiting for the start to finish. A thread worker has the
171    /// supervisor's own pid.
172    fn wait_started(&self, wait: Duration) -> Result<u32, String> {
173        let deadline = Instant::now() + wait;
174        let mut start = self
175            .start
176            .lock()
177            .unwrap_or_else(std::sync::PoisonError::into_inner);
178        loop {
179            match &*start {
180                Start::Ready { pid } => return Ok(*pid),
181                Start::Failed(reason) => return Err(reason.clone()),
182                Start::Starting | Start::Spawned => {}
183            }
184            let now = Instant::now();
185            if now >= deadline {
186                return Err(format!("it did not say hello within {}s", wait.as_secs()));
187            }
188            start = self
189                .started
190                .wait_timeout(start, deadline - now)
191                .unwrap_or_else(std::sync::PoisonError::into_inner)
192                .0;
193        }
194    }
195
196    fn notify_started(&self) {
197        self.started.notify_all();
198        self.started_async.notify_waiters();
199    }
200
201    fn start_outcome(&self) -> Option<Result<u32, String>> {
202        match &*self
203            .start
204            .lock()
205            .unwrap_or_else(std::sync::PoisonError::into_inner)
206        {
207            Start::Ready { pid } => Some(Ok(*pid)),
208            Start::Failed(reason) => Some(Err(reason.clone())),
209            Start::Starting | Start::Spawned => None,
210        }
211    }
212
213    async fn wait_started_async(self: &Arc<Self>) -> Result<u32, String> {
214        if tokio::runtime::Handle::try_current().is_err() {
215            return self.wait_started(HANDSHAKE_WAIT);
216        }
217        let waited = tokio::time::timeout(HANDSHAKE_WAIT, async {
218            loop {
219                let notified = self.started_async.notified();
220                tokio::pin!(notified);
221                // Registered before the state is read, so a start that lands in between wakes it.
222                notified.as_mut().enable();
223                if let Some(outcome) = self.start_outcome() {
224                    return outcome;
225                }
226                notified.await;
227            }
228        })
229        .await;
230        waited.unwrap_or_else(|_| {
231            Err(format!(
232                "it did not say hello within {}s",
233                HANDSHAKE_WAIT.as_secs()
234            ))
235        })
236    }
237
238    fn pid(&self) -> Option<u32> {
239        match &*self
240            .start
241            .lock()
242            .unwrap_or_else(std::sync::PoisonError::into_inner)
243        {
244            Start::Ready { pid } => Some(*pid),
245            _ => None,
246        }
247    }
248
249    async fn request(&self, req: u64, message: ToWorker) -> Result<Reply, String> {
250        let (tx, rx) = oneshot::channel();
251        {
252            if let Some(lost) = self
253                .lost
254                .lock()
255                .unwrap_or_else(std::sync::PoisonError::into_inner)
256                .clone()
257            {
258                return Err(lost);
259            }
260            self.pending
261                .lock()
262                .unwrap_or_else(std::sync::PoisonError::into_inner)
263                .insert(req, tx);
264        }
265        if let Err(error) = self.send(&message) {
266            self.pending
267                .lock()
268                .unwrap_or_else(std::sync::PoisonError::into_inner)
269                .remove(&req);
270            return Err(self.lost_or(error));
271        }
272        rx.await
273            .unwrap_or_else(|_| Err(self.lost_or("the guest worker went away".to_string())))
274    }
275
276    fn lost_or(&self, fallback: String) -> String {
277        self.lost
278            .lock()
279            .unwrap_or_else(std::sync::PoisonError::into_inner)
280            .clone()
281            .unwrap_or(fallback)
282    }
283
284    /// Kills the worker process, when it is one. The reader then sees end of file and fails every
285    /// pending request.
286    #[allow(unsafe_code)]
287    fn kill(&self) {
288        if let Some(pid) = self.pid().filter(|pid| *pid != std::process::id()) {
289            // SAFETY: plain syscall with integer arguments on a child this connection started.
290            unsafe {
291                libc::kill(pid as libc::pid_t, libc::SIGKILL);
292            }
293        }
294        if let Some(writer) = self
295            .writer
296            .lock()
297            .unwrap_or_else(std::sync::PoisonError::into_inner)
298            .as_ref()
299        {
300            let _ = writer.shutdown(std::net::Shutdown::Both);
301        }
302    }
303
304    /// The call a loss names: the oldest in flight, or the guest being handed over.
305    fn call_description(&self) -> Option<String> {
306        let call = self
307            .calls
308            .lock()
309            .unwrap_or_else(std::sync::PoisonError::into_inner)
310            .values()
311            .min_by_key(|entry| entry.registered)
312            .map(|entry| entry.description.clone());
313        call.or_else(|| {
314            self.loading
315                .lock()
316                .unwrap_or_else(std::sync::PoisonError::into_inner)
317                .clone()
318        })
319    }
320
321    /// Marks the connection lost with `message`, fails every pending request with it and reports it
322    /// into the session latch.
323    fn mark_lost(&self, message: String) {
324        *self
325            .lost
326            .lock()
327            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(message.clone());
328        {
329            let mut session = self
330                .session_lost
331                .lock()
332                .unwrap_or_else(std::sync::PoisonError::into_inner);
333            if session.is_none() {
334                *session = Some(message.clone());
335            }
336        }
337        let pending: Vec<_> = self
338            .pending
339            .lock()
340            .unwrap_or_else(std::sync::PoisonError::into_inner)
341            .drain()
342            .collect();
343        for (_, waiter) in pending {
344            let _ = waiter.send(Err(message.clone()));
345        }
346        self.calls
347            .lock()
348            .unwrap_or_else(std::sync::PoisonError::into_inner)
349            .clear();
350    }
351
352    fn reply_host(&self, host: u64, reply: HostReply) {
353        let _ = self.send(&ToWorker::HostReply { host, reply });
354    }
355
356    /// Handles one frame from the worker. Runs on the connection's reader thread, so events are
357    /// applied in arrival order, before any reply that arrived after them.
358    fn dispatch(self: &Arc<Self>, message: FromWorker) {
359        match message {
360            FromWorker::Hello { .. } => {}
361            FromWorker::Reply { req, reply } => {
362                let waiter = self
363                    .pending
364                    .lock()
365                    .unwrap_or_else(std::sync::PoisonError::into_inner)
366                    .remove(&req);
367                if let Some(waiter) = waiter {
368                    let _ = waiter.send(Ok(reply));
369                }
370            }
371            FromWorker::Event { call, event } => {
372                let entry = self.call(call);
373                if let Some(entry) = entry {
374                    apply_event(&entry.events, event);
375                }
376            }
377            FromWorker::RunStarted { call, deadline } => {
378                if let Some(entry) = self.call(call) {
379                    let started = match entry.process.try_lock() {
380                        Ok(mut process) => {
381                            process.begin_call(deadline);
382                            true
383                        }
384                        Err(_) => false,
385                    };
386                    if !started {
387                        self.host_runtime.spawn(async move {
388                            entry.process.lock().await.begin_call(deadline);
389                        });
390                    }
391                }
392            }
393            FromWorker::HostCall {
394                host,
395                call,
396                request,
397            } => {
398                let Some(entry) = self.call(call) else {
399                    self.reply_host(
400                        host,
401                        HostReply::Refused(format!(
402                            "call {call} is not in flight on this guest worker's connection"
403                        )),
404                    );
405                    return;
406                };
407                let conn = Arc::clone(self);
408                self.host_runtime.spawn(async move {
409                    conn.answer(host, call, entry, request).await;
410                });
411            }
412        }
413    }
414
415    fn call(&self, call: u64) -> Option<Arc<CallEntry>> {
416        self.calls
417            .lock()
418            .unwrap_or_else(std::sync::PoisonError::into_inner)
419            .get(&call)
420            .cloned()
421    }
422
423    async fn answer(
424        self: Arc<Self>,
425        host: u64,
426        call: u64,
427        entry: Arc<CallEntry>,
428        request: HostRequest,
429    ) {
430        match request {
431            HostRequest::RequestInput { prompt } => {
432                let answer = entry
433                    .input
434                    .request(prompt)
435                    .await
436                    .map_err(|error| error.to_string());
437                self.reply_host(host, HostReply::Input(answer));
438            }
439            HostRequest::Process(call) => {
440                let reply = entry.process.lock().await.answer(call).await;
441                self.reply_host(host, HostReply::Process(reply));
442            }
443            HostRequest::Inference { purpose, request } => {
444                let answer = match entry.hook.as_deref() {
445                    Some(HookCallHost {
446                        inference: Ok(ctx),
447                        origin,
448                        ..
449                    }) => ctx.run(origin, purpose, request).await,
450                    Some(HookCallHost {
451                        inference: Err(reason),
452                        ..
453                    }) => Err(reason.message().to_string()),
454                    None => Err(crate::inference_import::InferenceUnavailable::NotConfigured
455                        .message()
456                        .to_string()),
457                };
458                self.reply_host(host, HostReply::Inference(answer));
459            }
460            HostRequest::TaskIo(call) => {
461                let state = entry.hook.as_ref().and_then(|hook| hook.task_io.as_deref());
462                let reply = crate::task_io_import::answer_task_io(state, call);
463                self.reply_host(host, HostReply::TaskIo(reply));
464            }
465            HostRequest::Conversation { cursor, limit } => {
466                let page = match entry
467                    .hook
468                    .as_ref()
469                    .and_then(|hook| hook.conversation.as_deref())
470                {
471                    Some(state) => state.read_messages(cursor, limit),
472                    None => Err("not-granted".to_string()),
473                };
474                self.reply_host(host, HostReply::Conversation(page));
475            }
476            HostRequest::Capsule(capsule_call) => {
477                let reply = match &entry.capsule {
478                    Some(state) => {
479                        let mut state = state
480                            .lock()
481                            .unwrap_or_else(std::sync::PoisonError::into_inner);
482                        crate::runtime::answer_capsule_call(&mut state, *capsule_call)
483                    }
484                    None => crate::runtime::CapsuleReply::Unanswered(
485                        "this call is not a script capsule's run".to_string(),
486                    ),
487                };
488                self.reply_host(host, HostReply::Capsule(Box::new(reply)));
489                // A worker lost while the root waited on this call ends the root's run: the
490                // registry call it made may have been the one the loss failed.
491                let lost = self
492                    .session_lost
493                    .lock()
494                    .unwrap_or_else(std::sync::PoisonError::into_inner)
495                    .clone();
496                if let Some(lost) = lost {
497                    let waiter = self
498                        .pending
499                        .lock()
500                        .unwrap_or_else(std::sync::PoisonError::into_inner)
501                        .remove(&call);
502                    if let Some(waiter) = waiter {
503                        let _ = waiter.send(Err(lost));
504                    }
505                    let _ = self.send(&ToWorker::Cancel { req: call });
506                }
507            }
508            HostRequest::Http(head) => {
509                let sent = match request_from_head(head) {
510                    Err(error) => Err(error),
511                    Ok((request, options)) => {
512                        let future = match &entry.capsule {
513                            Some(state) => state
514                                .lock()
515                                .unwrap_or_else(std::sync::PoisonError::into_inner)
516                                .http_hooks
517                                .send_request(request, options, Box::new(std::future::pending())),
518                            None => entry
519                                .http
520                                .lock()
521                                .unwrap_or_else(std::sync::PoisonError::into_inner)
522                                .send_request(request, options, Box::new(std::future::pending())),
523                        };
524                        Box::into_pin(future).await
525                    }
526                };
527                match sent {
528                    Err(error) => self.reply_host(host, HostReply::Http(Err(to_wire(&error)))),
529                    Ok((response, io)) => {
530                        tokio::spawn(Box::into_pin(io));
531                        self.reply_host(host, HostReply::Http(Ok(head_of(&response))));
532                        let conn = Arc::clone(&self);
533                        let body = tokio::spawn(async move {
534                            let mut body = response.into_body();
535                            loop {
536                                match body.frame().await {
537                                    None => {
538                                        let _ = conn.send(&ToWorker::BodyEnd {
539                                            stream: host,
540                                            error: None,
541                                        });
542                                        break;
543                                    }
544                                    Some(Err(error)) => {
545                                        let _ = conn.send(&ToWorker::BodyEnd {
546                                            stream: host,
547                                            error: Some(to_wire(&error)),
548                                        });
549                                        break;
550                                    }
551                                    Some(Ok(frame)) => {
552                                        if let Ok(data) = frame.into_data() {
553                                            if conn
554                                                .send(&ToWorker::BodyChunk {
555                                                    stream: host,
556                                                    bytes: data.to_vec(),
557                                                })
558                                                .is_err()
559                                            {
560                                                break;
561                                            }
562                                        }
563                                    }
564                                }
565                            }
566                        });
567                        entry
568                            .bodies
569                            .lock()
570                            .unwrap_or_else(std::sync::PoisonError::into_inner)
571                            .push(body.abort_handle());
572                    }
573                }
574            }
575        }
576    }
577}
578
579fn apply_event(target: &StreamEventsTarget, event: StreamEvent) {
580    use crate::stream_events;
581    match event {
582        StreamEvent::Chunk(chunk) => stream_events::emit_chunk(target, &chunk),
583        StreamEvent::ThinkingChunk(chunk) => stream_events::emit_thinking_chunk(target, &chunk),
584        StreamEvent::ToolCallStarted { id, name } => {
585            stream_events::tool_call_started(target, &id, &name)
586        }
587        StreamEvent::ToolCallInputBytes { id, bytes } => {
588            stream_events::tool_call_input_bytes(target, &id, bytes, Instant::now())
589        }
590    }
591}
592
593/// How a worker process ended, as the lost-worker message words it.
594fn describe_exit(status: Option<std::process::ExitStatus>) -> String {
595    use std::os::unix::process::ExitStatusExt;
596    match status {
597        None => "closed its connection".to_string(),
598        Some(status) => match (status.code(), status.signal()) {
599            (Some(code), _) => format!("exited with status {code}"),
600            (None, Some(signal)) => {
601                format!("was killed by signal {signal} ({})", signal_name(signal))
602            }
603            _ => "ended".to_string(),
604        },
605    }
606}
607
608fn signal_name(signal: i32) -> &'static str {
609    match signal {
610        libc::SIGKILL => "SIGKILL",
611        libc::SIGTERM => "SIGTERM",
612        libc::SIGSEGV => "SIGSEGV",
613        libc::SIGABRT => "SIGABRT",
614        libc::SIGBUS => "SIGBUS",
615        libc::SIGINT => "SIGINT",
616        libc::SIGHUP => "SIGHUP",
617        libc::SIGILL => "SIGILL",
618        libc::SIGFPE => "SIGFPE",
619        libc::SIGPIPE => "SIGPIPE",
620        _ => "signal",
621    }
622}
623
624/// The `E-RUN-065` text for a lost worker.
625pub(crate) fn lost_message(
626    kind: WorkerKind,
627    pid: Option<u32>,
628    call: Option<&str>,
629    exit: &str,
630) -> String {
631    let pid = pid.map_or_else(|| "unknown".to_string(), |pid| pid.to_string());
632    let call = call.unwrap_or("no call");
633    format!(
634        "the {kind} guest worker (pid {pid}) was lost while running {call}: it {exit}; the task \
635         failed and the session ended",
636        kind = kind.as_str()
637    )
638}
639
640/// Each worker's guests by tool name: the id a guest has there once its hand-over finished.
641type LoadedGuests = HashMap<(WorkerKind, String), Arc<tokio::sync::OnceCell<u64>>>;
642
643/// The guest workers of one session: at most one per kind, each started on first need.
644pub struct GuestWorkers {
645    launcher: Launcher,
646    /// What each kind's worker process is confined by, set by staging before the kind starts.
647    /// A kind with none starts unconfined (a library embedder, an unconfined session's tests).
648    sandboxes: Mutex<HashMap<WorkerKind, Arc<super::launch::WorkerSandbox>>>,
649    conns: Mutex<HashMap<WorkerKind, Arc<WorkerConn>>>,
650    /// The guests each worker holds, by tool name, with their guest ids.
651    loaded: Mutex<LoadedGuests>,
652    lost: Arc<Mutex<Option<String>>>,
653    host_runtime: Mutex<Option<tokio::runtime::Runtime>>,
654    host_handle: tokio::runtime::Handle,
655    children: Mutex<Vec<std::process::Child>>,
656    #[cfg(test)]
657    tap: Option<Arc<Mutex<Vec<Vec<u8>>>>>,
658}
659
660impl std::fmt::Debug for GuestWorkers {
661    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662        f.debug_struct("GuestWorkers")
663            .field("kinds", &self.started_kinds())
664            .finish()
665    }
666}
667
668impl GuestWorkers {
669    pub(crate) fn new(launcher: Launcher) -> Arc<Self> {
670        let runtime = tokio::runtime::Builder::new_multi_thread()
671            .worker_threads(2)
672            .thread_name("guest-host-calls")
673            .enable_all()
674            .build()
675            .expect("the guest workers' host-call runtime builds");
676        let handle = runtime.handle().clone();
677        Arc::new(Self {
678            launcher,
679            sandboxes: Mutex::new(HashMap::new()),
680            conns: Mutex::new(HashMap::new()),
681            loaded: Mutex::new(HashMap::new()),
682            lost: Arc::new(Mutex::new(None)),
683            host_runtime: Mutex::new(Some(runtime)),
684            host_handle: handle,
685            children: Mutex::new(Vec::new()),
686            #[cfg(test)]
687            tap: None,
688        })
689    }
690
691    #[cfg(test)]
692    pub(crate) fn new_tapped(launcher: Launcher, tap: Arc<Mutex<Vec<Vec<u8>>>>) -> Arc<Self> {
693        let workers = Self::new(launcher);
694        let mut workers = Arc::try_unwrap(workers).expect("fresh");
695        workers.tap = Some(tap);
696        Arc::new(workers)
697    }
698
699    /// The kinds started so far.
700    pub fn started_kinds(&self) -> Vec<WorkerKind> {
701        let mut kinds: Vec<_> = self
702            .conns
703            .lock()
704            .unwrap_or_else(std::sync::PoisonError::into_inner)
705            .keys()
706            .copied()
707            .collect();
708        kinds.sort();
709        kinds
710    }
711
712    /// The session's loss latch: the `E-RUN-065` message of the first worker lost, if any.
713    pub fn lost(&self) -> Option<String> {
714        self.lost
715            .lock()
716            .unwrap_or_else(std::sync::PoisonError::into_inner)
717            .clone()
718    }
719
720    /// Confines every later start of `kind`'s worker process to `sandbox`. Called by staging for
721    /// every kind before the first start, so a worker started later in the session (the tools
722    /// worker at a pulled tool's first call) is confined the same way.
723    pub(crate) fn confine(&self, kind: WorkerKind, sandbox: super::launch::WorkerSandbox) {
724        self.sandboxes
725            .lock()
726            .unwrap_or_else(std::sync::PoisonError::into_inner)
727            .insert(kind, Arc::new(sandbox));
728    }
729
730    /// Starts the worker of `kind` unless it is already started. Does not wait for it.
731    pub(crate) fn start(self: &Arc<Self>, kind: WorkerKind) -> Arc<WorkerConn> {
732        let mut conns = self
733            .conns
734            .lock()
735            .unwrap_or_else(std::sync::PoisonError::into_inner);
736        if let Some(conn) = conns.get(&kind) {
737            return Arc::clone(conn);
738        }
739        let conn = Arc::new(WorkerConn {
740            kind,
741            start: Mutex::new(Start::Starting),
742            started: Condvar::new(),
743            started_async: tokio::sync::Notify::new(),
744            writer: Mutex::new(None),
745            pending: Mutex::new(HashMap::new()),
746            calls: Mutex::new(HashMap::new()),
747            loading: Mutex::new(None),
748            lost: Mutex::new(None),
749            unresponsive: Mutex::new(None),
750            session_lost: Arc::clone(&self.lost),
751            host_runtime: self.host_handle.clone(),
752            #[cfg(test)]
753            tap: self.tap.clone(),
754        });
755        conns.insert(kind, Arc::clone(&conn));
756        drop(conns);
757        self.launch(&conn);
758        conn
759    }
760
761    #[allow(unsafe_code)]
762    fn launch(self: &Arc<Self>, conn: &Arc<WorkerConn>) {
763        let launcher = self.launcher.clone();
764        let sandbox = self
765            .sandboxes
766            .lock()
767            .unwrap_or_else(std::sync::PoisonError::into_inner)
768            .get(&conn.kind)
769            .cloned();
770        let conn_thread = Arc::clone(conn);
771        let workers = Arc::downgrade(self);
772        let spawned = std::thread::Builder::new()
773            .name(format!("guest-worker-{}-conn", conn.kind.as_str()))
774            .spawn(move || {
775                let kind = conn_thread.kind;
776                // The worker is spawned from this thread, which lives as long as the connection, so
777                // its parent-death signal is tied to the connection's life.
778                let started = super::launch::start_worker(&launcher, kind, sandbox.as_deref());
779                let (stream, child) = match started {
780                    Ok(started) => started,
781                    Err(reason) => {
782                        *conn_thread
783                            .start
784                            .lock()
785                            .unwrap_or_else(std::sync::PoisonError::into_inner) =
786                            Start::Failed(reason);
787                        conn_thread.notify_started();
788                        return;
789                    }
790                };
791                let child_pid = child.as_ref().map(std::process::Child::id);
792                *conn_thread
793                    .start
794                    .lock()
795                    .unwrap_or_else(std::sync::PoisonError::into_inner) = Start::Spawned;
796                conn_thread.notify_started();
797                if let (Some(child), Some(workers)) = (child, workers.upgrade()) {
798                    workers
799                        .children
800                        .lock()
801                        .unwrap_or_else(std::sync::PoisonError::into_inner)
802                        .push(child);
803                }
804                // A worker forked into the session's PID namespace knows itself by a
805                // namespace-local pid; the process `spawn` returned is its relay, whose death
806                // takes the worker with it, so the relay is the pid this supervisor signals.
807                let known_pid = if sandbox
808                    .as_deref()
809                    .is_some_and(super::launch::WorkerSandbox::enters_pid_namespace)
810                {
811                    KnownPid::Relay(child_pid)
812                } else {
813                    KnownPid::Child(child_pid)
814                };
815                let handshake = handshake(kind, &stream, known_pid);
816                let mut reader = stream;
817                match handshake {
818                    Err(reason) => {
819                        if let Some(pid) = child_pid {
820                            // SAFETY: plain syscall on the child this thread started.
821                            unsafe {
822                                libc::kill(pid as libc::pid_t, libc::SIGKILL);
823                            }
824                        }
825                        *conn_thread
826                            .start
827                            .lock()
828                            .unwrap_or_else(std::sync::PoisonError::into_inner) =
829                            Start::Failed(reason);
830                        conn_thread.notify_started();
831                        return;
832                    }
833                    Ok((pid, writer)) => {
834                        *conn_thread
835                            .writer
836                            .lock()
837                            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(writer);
838                        *conn_thread
839                            .start
840                            .lock()
841                            .unwrap_or_else(std::sync::PoisonError::into_inner) =
842                            Start::Ready { pid };
843                        conn_thread.notify_started();
844                    }
845                }
846                while let Ok(message) = read_frame::<FromWorker>(&mut reader) {
847                    conn_thread.dispatch(message);
848                }
849                let status = child_pid.and_then(|pid| reap(pid, workers.upgrade()));
850                let unresponsive = conn_thread
851                    .unresponsive
852                    .lock()
853                    .unwrap_or_else(std::sync::PoisonError::into_inner)
854                    .take();
855                let (call, exit) = match unresponsive {
856                    Some(call) => (Some(call), "stopped answering and was killed".to_string()),
857                    None => (conn_thread.call_description(), describe_exit(status)),
858                };
859                let message = lost_message(kind, conn_thread.pid(), call.as_deref(), &exit);
860                conn_thread.mark_lost(message);
861            });
862        if let Err(error) = spawned {
863            *conn
864                .start
865                .lock()
866                .unwrap_or_else(std::sync::PoisonError::into_inner) =
867                Start::Failed(format!("its connection thread did not start: {error}"));
868            conn.notify_started();
869        }
870    }
871
872    /// Waits for every started worker to finish its handshake, or the `E-RUN-066` refusal of
873    /// the first that did not.
874    pub(crate) fn ready(&self) -> Result<(), RuntimeError> {
875        let mut conns: Vec<_> = self
876            .conns
877            .lock()
878            .unwrap_or_else(std::sync::PoisonError::into_inner)
879            .values()
880            .cloned()
881            .collect();
882        conns.sort_by_key(|conn| conn.kind);
883        for conn in conns {
884            conn.wait_started(HANDSHAKE_WAIT)
885                .map_err(|reason| start_error(conn.kind, &reason))?;
886        }
887        Ok(())
888    }
889
890    /// Runs tool `name`'s `component` in the worker of `kind`: loads it there on first use,
891    /// registers `host` as what the call's host imports reach, and records every wall-clock bound
892    /// that fired in it into `call_deadlines` before returning.
893    #[allow(clippy::too_many_arguments)]
894    pub(crate) async fn run_tool(
895        self: &Arc<Self>,
896        kind: WorkerKind,
897        component: &Component,
898        spec: ToolCallSpec,
899        host: ToolCallHost,
900        input: ToolInput,
901        call_deadlines: &CallDeadlines,
902    ) -> Result<ToolResult, String> {
903        let name = spec.name.clone();
904        let mut entry = CallEntry::bare(format!("{} '{name}'", spec.role.wire_name()));
905        entry.http = Arc::new(Mutex::new(host.http_hooks));
906        entry.events = host.events;
907        entry.input = host.input;
908        entry.process = tokio::sync::Mutex::new(RemoteProcessHost::new(host.process));
909        // Instantiation and `run` are each bounded by the worker; past both bounds and the grace,
910        // the worker is no longer answering.
911        let bound = spec.limits.wall_bound() * 2 + UNRESPONSIVE_GRACE;
912        let reply = self
913            .call(
914                kind,
915                &name,
916                component,
917                entry,
918                bound,
919                call_deadlines,
920                |req, guest| ToWorker::RunTool {
921                    req,
922                    guest,
923                    spec: Box::new(spec),
924                    input,
925                },
926            )
927            .await?;
928        match reply {
929            Reply::Tool { result, .. } => result,
930            other => Err(format!("the guest worker answered {other:?}")),
931        }
932    }
933
934    /// Sends one guest call to the worker of `kind`: starts the worker and hands it `component`
935    /// as guest `name` on first use, registers `entry` as what the call's host calls reach while
936    /// it is in flight, and waits up to `bound` for the answer before killing the worker as
937    /// unresponsive. Every wall-clock bound that fired in the call is recorded in
938    /// `call_deadlines` before the reply is returned.
939    #[allow(clippy::too_many_arguments)]
940    async fn call(
941        self: &Arc<Self>,
942        kind: WorkerKind,
943        name: &str,
944        component: &Component,
945        entry: CallEntry,
946        bound: Duration,
947        call_deadlines: &CallDeadlines,
948        message: impl FnOnce(u64, u64) -> ToWorker,
949    ) -> Result<Reply, String> {
950        let conn = self.start(kind);
951        if let Err(reason) = conn.wait_started_async().await {
952            return Err(start_error(kind, &reason).to_string());
953        }
954        let guest = self.ensure_loaded(&conn, name, component).await?;
955        self.send_call(&conn, entry, bound, call_deadlines, |req| {
956            message(req, guest)
957        })
958        .await
959    }
960
961    /// [`Self::call`] for a guest the worker already holds an instance of.
962    async fn send_call(
963        &self,
964        conn: &Arc<WorkerConn>,
965        entry: CallEntry,
966        bound: Duration,
967        call_deadlines: &CallDeadlines,
968        message: impl FnOnce(u64) -> ToWorker,
969    ) -> Result<Reply, String> {
970        let req = next_id();
971        conn.calls
972            .lock()
973            .unwrap_or_else(std::sync::PoisonError::into_inner)
974            .insert(req, Arc::new(entry));
975        let mut in_flight = InFlight {
976            conn: Arc::clone(conn),
977            req,
978            done: false,
979        };
980        let replied = tokio::time::timeout(bound, conn.request(req, message(req))).await;
981        in_flight.done = true;
982        let entry = conn
983            .calls
984            .lock()
985            .unwrap_or_else(std::sync::PoisonError::into_inner)
986            .remove(&req);
987        let reply = match replied {
988            Ok(reply) => reply,
989            Err(_) => {
990                // Recorded before the kill, so the reader that sees the worker go names this call.
991                *conn
992                    .unresponsive
993                    .lock()
994                    .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(
995                    entry.map_or_else(|| "no call".to_string(), |entry| entry.description.clone()),
996                );
997                conn.kill();
998                // The reader fails the request once it sees the worker gone.
999                return Err(wait_lost(conn).await);
1000            }
1001        };
1002        let mut reply = reply?;
1003        for cut in reply.take_deadlines() {
1004            call_deadlines.record(cut);
1005        }
1006        Ok(reply)
1007    }
1008
1009    /// The connection of the started worker of `kind`, once it is past its handshake.
1010    async fn started(self: &Arc<Self>, kind: WorkerKind) -> Result<Arc<WorkerConn>, String> {
1011        let conn = self.start(kind);
1012        conn.wait_started_async()
1013            .await
1014            .map_err(|reason| start_error(kind, &reason).to_string())?;
1015        Ok(conn)
1016    }
1017
1018    /// `error`, a guest call's failure, as the launch reports it: the session's `E-RUN-065` once
1019    /// a worker was lost, a worker that could not start as `E-RUN-066`, anything else as itself.
1020    pub(crate) fn runtime_error(&self, error: String) -> RuntimeError {
1021        if let Some(lost) = self.lost() {
1022            return RuntimeError::GuestWorkerLost(lost);
1023        }
1024        if error.contains(" guest worker could not be started: ") {
1025            return RuntimeError::GuestWorkerStart(error);
1026        }
1027        RuntimeError::Runtime(error)
1028    }
1029
1030    /// Instantiates hook `name`'s `component` in the hooks worker as a session-long instance.
1031    /// Returns its instance id and the lifecycle functions it exports.
1032    pub(crate) async fn open_hook(
1033        self: &Arc<Self>,
1034        name: &str,
1035        component: &Component,
1036        spec: HookSpec,
1037        host: Arc<HookCallHost>,
1038        call_deadlines: &CallDeadlines,
1039    ) -> Result<(u64, Vec<String>), String> {
1040        let instance = next_id();
1041        let mut entry = CallEntry::bare(format!("hook '{name}' instantiate"));
1042        entry.http = Arc::clone(&host.http);
1043        entry.hook = Some(host);
1044        let bound = spec.limits.wall_bound() + UNRESPONSIVE_GRACE;
1045        let reply = self
1046            .call(
1047                WorkerKind::Hooks,
1048                name,
1049                component,
1050                entry,
1051                bound,
1052                call_deadlines,
1053                |req, guest| ToWorker::HookOpen {
1054                    req,
1055                    guest,
1056                    instance,
1057                    spec: Box::new(spec),
1058                },
1059            )
1060            .await?;
1061        match reply {
1062            Reply::HookOpened { result, .. } => result.map(|fns| (instance, fns)),
1063            other => Err(format!("the guest worker answered {other:?}")),
1064        }
1065    }
1066
1067    /// Calls lifecycle function `fn_name` with `arg` on hook instance `instance`.
1068    #[allow(clippy::too_many_arguments)]
1069    pub(crate) async fn call_hook(
1070        self: &Arc<Self>,
1071        instance: u64,
1072        name: &str,
1073        fn_name: &str,
1074        arg: HookArg,
1075        purpose: HookInferencePurpose,
1076        host: &Arc<HookCallHost>,
1077        limits: ExecutionLimits,
1078        call_deadlines: &CallDeadlines,
1079    ) -> Result<Option<HookOutput>, String> {
1080        let conn = self.started(WorkerKind::Hooks).await?;
1081        let mut entry = CallEntry::bare(format!("hook '{name}' {fn_name}"));
1082        entry.http = Arc::clone(&host.http);
1083        entry.hook = Some(Arc::clone(host));
1084        let bound = limits.wall_bound() + UNRESPONSIVE_GRACE;
1085        let reply = self
1086            .send_call(&conn, entry, bound, call_deadlines, |req| {
1087                ToWorker::HookCall {
1088                    req,
1089                    instance,
1090                    fn_name: fn_name.to_string(),
1091                    arg: Box::new(arg),
1092                    purpose,
1093                }
1094            })
1095            .await?;
1096        match reply {
1097            Reply::Hook { result, .. } => result,
1098            other => Err(format!("the guest worker answered {other:?}")),
1099        }
1100    }
1101
1102    /// Calls `on-stage` with `event` on a throwaway instance of hook `name`'s `component`.
1103    pub(crate) async fn stage_hook(
1104        self: &Arc<Self>,
1105        name: &str,
1106        component: &Component,
1107        spec: HookSpec,
1108        host: Arc<HookCallHost>,
1109        call_deadlines: &CallDeadlines,
1110        event: StageEvent,
1111    ) -> Result<HookOutput, String> {
1112        let mut entry = CallEntry::bare(format!("hook '{name}' on-stage"));
1113        entry.http = Arc::clone(&host.http);
1114        entry.hook = Some(host);
1115        let bound = spec.limits.wall_bound() * 2 + UNRESPONSIVE_GRACE;
1116        let reply = self
1117            .call(
1118                WorkerKind::Hooks,
1119                name,
1120                component,
1121                entry,
1122                bound,
1123                call_deadlines,
1124                |req, guest| ToWorker::HookStage {
1125                    req,
1126                    guest,
1127                    spec: Box::new(spec),
1128                    event,
1129                },
1130            )
1131            .await?;
1132        match reply {
1133            Reply::Hook {
1134                result: Ok(Some(output)),
1135                ..
1136            } => Ok(output),
1137            Reply::Hook {
1138                result: Ok(None), ..
1139            } => Err(format!("hook {name} returned no on-stage output")),
1140            Reply::Hook {
1141                result: Err(error), ..
1142            } => Err(error),
1143            other => Err(format!("the guest worker answered {other:?}")),
1144        }
1145    }
1146
1147    /// Drops hook instance `instance` in the hooks worker.
1148    pub(crate) fn close_hook(&self, instance: u64) {
1149        self.close(WorkerKind::Hooks, ToWorker::HookClose { instance });
1150    }
1151
1152    fn close(&self, kind: WorkerKind, message: ToWorker) {
1153        let conn = self
1154            .conns
1155            .lock()
1156            .unwrap_or_else(std::sync::PoisonError::into_inner)
1157            .get(&kind)
1158            .cloned();
1159        if let Some(conn) = conn {
1160            let _ = conn.send(&message);
1161        }
1162    }
1163
1164    /// Instantiates process driver `name`'s `component` in the driver worker. Returns the
1165    /// instance id.
1166    pub(crate) async fn open_driver(
1167        self: &Arc<Self>,
1168        name: &str,
1169        version: &str,
1170        component: &Component,
1171        call_deadlines: &CallDeadlines,
1172    ) -> Result<
1173        (
1174            u64,
1175            Result<DriverAnswer, crate::process_driver::DriverError>,
1176        ),
1177        String,
1178    > {
1179        let instance = next_id();
1180        let entry = CallEntry::bare(format!("process driver '{name}' instantiate"));
1181        let bound = driver_bound();
1182        let reply = self
1183            .call(
1184                WorkerKind::Driver,
1185                &format!("process-driver:{name}"),
1186                component,
1187                entry,
1188                bound,
1189                call_deadlines,
1190                |req, guest| ToWorker::DriverOpen {
1191                    req,
1192                    guest,
1193                    instance,
1194                    name: name.to_string(),
1195                    version: version.to_string(),
1196                },
1197            )
1198            .await?;
1199        match reply {
1200            Reply::Driver { result, .. } => Ok((instance, result)),
1201            other => Err(format!("the guest worker answered {other:?}")),
1202        }
1203    }
1204
1205    /// Runs `call` on process driver instance `instance`.
1206    pub(crate) async fn call_driver(
1207        self: &Arc<Self>,
1208        instance: u64,
1209        name: &str,
1210        call: DriverCall,
1211        call_deadlines: &CallDeadlines,
1212    ) -> Result<Result<DriverAnswer, crate::process_driver::DriverError>, String> {
1213        let conn = self.started(WorkerKind::Driver).await?;
1214        let entry = CallEntry::bare(format!(
1215            "process driver '{name}' {}",
1216            driver_call_name(&call)
1217        ));
1218        let reply = self
1219            .send_call(&conn, entry, driver_bound(), call_deadlines, |req| {
1220                ToWorker::DriverCall {
1221                    req,
1222                    instance,
1223                    call: Box::new(call),
1224                }
1225            })
1226            .await?;
1227        match reply {
1228            Reply::Driver { result, .. } => Ok(result),
1229            other => Err(format!("the guest worker answered {other:?}")),
1230        }
1231    }
1232
1233    /// Drops process driver instance `instance` in the driver worker.
1234    pub(crate) fn close_driver(&self, instance: u64) {
1235        self.close(WorkerKind::Driver, ToWorker::DriverClose { instance });
1236    }
1237
1238    /// Runs the script capsule `name`'s root `component` in the driver worker, its host imports
1239    /// answered from `state`.
1240    pub(crate) async fn run_capsule(
1241        self: &Arc<Self>,
1242        name: &str,
1243        component: &Component,
1244        spec: CapsuleSpec,
1245        state: Arc<Mutex<CapsuleStoreState>>,
1246        call_deadlines: &CallDeadlines,
1247    ) -> Result<Result<(), crate::runtime::CapsuleRunError>, String> {
1248        let mut entry = CallEntry::bare(format!("capsule '{name}'"));
1249        entry.capsule = Some(state);
1250        let bound = spec.limits.wall_bound() * 2 + UNRESPONSIVE_GRACE;
1251        let reply = self
1252            .call(
1253                WorkerKind::Driver,
1254                &format!("capsule:{name}"),
1255                component,
1256                entry,
1257                bound,
1258                call_deadlines,
1259                |req, guest| ToWorker::RunCapsule {
1260                    req,
1261                    guest,
1262                    spec: Box::new(spec),
1263                },
1264            )
1265            .await?;
1266        match reply {
1267            Reply::Capsule { result, .. } => Ok(result),
1268            other => Err(format!("the guest worker answered {other:?}")),
1269        }
1270    }
1271
1272    /// Registers a call in flight on the `kind` worker's connection, holding host objects that
1273    /// serve nothing, and returns its id.
1274    #[cfg(test)]
1275    pub(crate) fn register_test_call(&self, kind: WorkerKind) -> (u64, StreamEventsTarget) {
1276        let conn = self
1277            .conns
1278            .lock()
1279            .unwrap_or_else(std::sync::PoisonError::into_inner)
1280            .get(&kind)
1281            .cloned()
1282            .expect("the worker is started");
1283        let events = StreamEventsTarget {
1284            sse: None,
1285            task: None,
1286            chunks_emitted: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1287            tool_calls: None,
1288        };
1289        let mut entry = CallEntry::bare("tool 'probe'".to_string());
1290        entry.events = events.clone();
1291        let entry = Arc::new(entry);
1292        let id = next_id();
1293        conn.calls
1294            .lock()
1295            .unwrap_or_else(std::sync::PoisonError::into_inner)
1296            .insert(id, entry);
1297        (id, events)
1298    }
1299
1300    /// Sends `message` as request `req` on the `kind` worker's connection and awaits its reply.
1301    #[cfg(test)]
1302    pub(crate) async fn request_for_test(
1303        &self,
1304        kind: WorkerKind,
1305        req: u64,
1306        message: ToWorker,
1307    ) -> Result<Reply, String> {
1308        let conn = self
1309            .conns
1310            .lock()
1311            .unwrap_or_else(std::sync::PoisonError::into_inner)
1312            .get(&kind)
1313            .cloned()
1314            .expect("the worker is started");
1315        conn.request(req, message).await
1316    }
1317
1318    /// Guest `name`'s id in `conn`'s worker, handing it `component` first unless it holds it.
1319    /// Concurrent callers for one guest share one hand-over.
1320    pub(crate) async fn ensure_loaded(
1321        &self,
1322        conn: &Arc<WorkerConn>,
1323        name: &str,
1324        component: &Component,
1325    ) -> Result<u64, String> {
1326        let cell = Arc::clone(
1327            self.loaded
1328                .lock()
1329                .unwrap_or_else(std::sync::PoisonError::into_inner)
1330                .entry((conn.kind, name.to_string()))
1331                .or_default(),
1332        );
1333        cell.get_or_try_init(|| load(conn, name, component))
1334            .await
1335            .copied()
1336    }
1337
1338    /// Starts handing guest `name` to the `kind` worker in the background, so its first call does
1339    /// not wait for it.
1340    pub(crate) fn preload(self: &Arc<Self>, kind: WorkerKind, name: &str, component: &Component) {
1341        let workers = Arc::clone(self);
1342        let name = name.to_string();
1343        let component = component.clone();
1344        self.host_handle.spawn(async move {
1345            let conn = workers.start(kind);
1346            if conn.wait_started_async().await.is_ok() {
1347                let _ = workers.ensure_loaded(&conn, &name, &component).await;
1348            }
1349        });
1350    }
1351
1352    /// Forgets tool `name` in every worker, so its next call loads the component it is given then.
1353    pub(crate) fn forget(&self, name: &str) {
1354        self.loaded
1355            .lock()
1356            .unwrap_or_else(std::sync::PoisonError::into_inner)
1357            .retain(|(_, loaded), _| loaded != name);
1358    }
1359
1360    /// Ends every worker: `Shutdown`, up to 1 s to exit, then `SIGKILL`, and reaps each.
1361    pub fn shutdown(&self) {
1362        let conns: Vec<_> = self
1363            .conns
1364            .lock()
1365            .unwrap_or_else(std::sync::PoisonError::into_inner)
1366            .values()
1367            .cloned()
1368            .collect();
1369        for conn in &conns {
1370            let _ = conn.send(&ToWorker::Shutdown);
1371        }
1372        let mut children: Vec<_> = self
1373            .children
1374            .lock()
1375            .unwrap_or_else(std::sync::PoisonError::into_inner)
1376            .drain(..)
1377            .collect();
1378        let deadline = Instant::now() + Duration::from_secs(1);
1379        while !children.is_empty() && Instant::now() < deadline {
1380            children.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
1381            if !children.is_empty() {
1382                std::thread::sleep(Duration::from_millis(5));
1383            }
1384        }
1385        for mut child in children {
1386            let _ = child.kill();
1387            let _ = child.wait();
1388        }
1389        for conn in &conns {
1390            if let Some(writer) = conn
1391                .writer
1392                .lock()
1393                .unwrap_or_else(std::sync::PoisonError::into_inner)
1394                .as_ref()
1395            {
1396                let _ = writer.shutdown(std::net::Shutdown::Both);
1397            }
1398        }
1399    }
1400}
1401
1402impl Drop for GuestWorkers {
1403    fn drop(&mut self) {
1404        self.shutdown();
1405        if let Some(runtime) = self
1406            .host_runtime
1407            .get_mut()
1408            .unwrap_or_else(std::sync::PoisonError::into_inner)
1409            .take()
1410        {
1411            runtime.shutdown_background();
1412        }
1413    }
1414}
1415
1416/// Hands `component` to `conn`'s worker as a new guest named `name`.
1417async fn load(conn: &Arc<WorkerConn>, name: &str, component: &Component) -> Result<u64, String> {
1418    let form = component
1419        .serialize()
1420        .map_err(|error| format!("failed to hand '{name}' to the guest worker: {error}"))?;
1421    let guest = next_id();
1422    let req = next_id();
1423    *conn
1424        .loading
1425        .lock()
1426        .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(format!("the load of '{name}'"));
1427    let reply = conn
1428        .request(
1429            req,
1430            ToWorker::Load {
1431                req,
1432                guest,
1433                name: name.to_string(),
1434                form,
1435            },
1436        )
1437        .await;
1438    *conn
1439        .loading
1440        .lock()
1441        .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1442    match reply? {
1443        Reply::Loaded(Ok(())) => Ok(guest),
1444        Reply::Loaded(Err(error)) => Err(error),
1445        other => Err(format!("the guest worker answered {other:?}")),
1446    }
1447}
1448
1449/// How long the supervisor waits for one process driver call: the worker bounds it by the hook
1450/// call limits, and past that and the grace it is no longer answering.
1451fn driver_bound() -> Duration {
1452    ExecutionLimits::default().for_hook_calls(None).wall_bound() + UNRESPONSIVE_GRACE
1453}
1454
1455fn driver_call_name(call: &DriverCall) -> &'static str {
1456    match call {
1457        DriverCall::Describe => "describe",
1458        DriverCall::Launch(_) => "launch",
1459        DriverCall::Parse(_) => "parse",
1460        DriverCall::ClassifyExit(_) => "classify-exit",
1461        DriverCall::ReadLogin(_) => "read-login",
1462    }
1463}
1464
1465/// A guest call in flight; dropped before its reply, it tells the worker to drop the call.
1466struct InFlight {
1467    conn: Arc<WorkerConn>,
1468    req: u64,
1469    done: bool,
1470}
1471
1472impl Drop for InFlight {
1473    fn drop(&mut self) {
1474        if !self.done {
1475            self.conn
1476                .pending
1477                .lock()
1478                .unwrap_or_else(std::sync::PoisonError::into_inner)
1479                .remove(&self.req);
1480            self.conn
1481                .calls
1482                .lock()
1483                .unwrap_or_else(std::sync::PoisonError::into_inner)
1484                .remove(&self.req);
1485            let _ = self.conn.send(&ToWorker::Cancel { req: self.req });
1486        }
1487    }
1488}
1489
1490async fn wait_lost(conn: &Arc<WorkerConn>) -> String {
1491    for _ in 0..500 {
1492        if let Some(lost) = conn
1493            .lost
1494            .lock()
1495            .unwrap_or_else(std::sync::PoisonError::into_inner)
1496            .clone()
1497        {
1498            return lost;
1499        }
1500        tokio::time::sleep(Duration::from_millis(10)).await;
1501    }
1502    lost_message(
1503        conn.kind,
1504        conn.pid(),
1505        None,
1506        "stopped answering and was killed",
1507    )
1508}
1509
1510/// Reaps worker `pid`, returning how it ended.
1511fn reap(pid: u32, workers: Option<Arc<GuestWorkers>>) -> Option<std::process::ExitStatus> {
1512    let workers = workers?;
1513    let mut children = workers
1514        .children
1515        .lock()
1516        .unwrap_or_else(std::sync::PoisonError::into_inner);
1517    let index = children.iter().position(|child| child.id() == pid)?;
1518    let mut child = children.remove(index);
1519    drop(children);
1520    // The connection closed; give the process a moment to finish exiting.
1521    let deadline = Instant::now() + Duration::from_secs(2);
1522    loop {
1523        match child.try_wait() {
1524            Ok(Some(status)) => return Some(status),
1525            Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(5)),
1526            _ => {
1527                let _ = child.kill();
1528                return child.wait().ok();
1529            }
1530        }
1531    }
1532}
1533
1534/// How the supervisor knows a just-started worker's pid.
1535#[derive(Debug, Clone, Copy)]
1536enum KnownPid {
1537    /// The pid `spawn` returned is the worker itself (`None` for a thread worker).
1538    Child(Option<u32>),
1539    /// The pid `spawn` returned is the relay of a worker forked into the session's PID namespace,
1540    /// whose hello names its namespace-local pid.
1541    Relay(Option<u32>),
1542}
1543
1544/// Exchanges `Hello` and `Init` with a just-started worker. Returns its pid and the writer half.
1545/// A worker process spawned directly is known by the pid it was spawned as: a hello naming any
1546/// other pid is refused, so the supervisor never signals a process it did not start. A worker
1547/// behind a relay names its namespace-local pid, which is not compared: its host pid is read as
1548/// the relay's one child ([`relayed_worker_pid`]), and is the relay's own when that read fails. A
1549/// thread worker, which has no child, is taken at its word.
1550fn handshake(
1551    kind: WorkerKind,
1552    stream: &UnixStream,
1553    known: KnownPid,
1554) -> Result<(u32, UnixStream), String> {
1555    let (child, relay) = match known {
1556        KnownPid::Child(child) => (child, None),
1557        KnownPid::Relay(relay) => (None, relay),
1558    };
1559    let mut reader = stream
1560        .try_clone()
1561        .map_err(|error| format!("its connection could not be shared: {error}"))?;
1562    reader
1563        .set_read_timeout(Some(HANDSHAKE_WAIT))
1564        .map_err(|error| error.to_string())?;
1565    let hello = read_frame::<FromWorker>(&mut reader)
1566        .map_err(|error| format!("it did not say hello: {error}"))?;
1567    let pid = match hello {
1568        FromWorker::Hello {
1569            protocol,
1570            version,
1571            pid,
1572        } => {
1573            if protocol != PROTOCOL_VERSION {
1574                return Err(format!(
1575                    "it speaks guest-worker protocol {protocol} (mur {version}), not {PROTOCOL_VERSION}"
1576                ));
1577            }
1578            if let Some(relay) = relay {
1579                relayed_worker_pid(relay).unwrap_or(relay)
1580            } else {
1581                match child {
1582                    Some(child) if child != pid => {
1583                        return Err(format!(
1584                            "it said hello as pid {pid}, but it was started as pid {child}"
1585                        ));
1586                    }
1587                    Some(child) => child,
1588                    None => pid,
1589                }
1590            }
1591        }
1592        other => return Err(format!("its first frame was not a hello: {other:?}")),
1593    };
1594    reader
1595        .set_read_timeout(None)
1596        .map_err(|error| error.to_string())?;
1597    let mut writer = stream
1598        .try_clone()
1599        .map_err(|error| format!("its connection could not be shared: {error}"))?;
1600    write_frame(&mut writer, &ToWorker::Init { kind })?;
1601    Ok((pid, writer))
1602}
1603
1604/// The host pid of the worker a relay forked into the session's PID namespace: the relay's one
1605/// child, read once the worker has said hello. `None` off Linux or when it cannot be read, and
1606/// the relay's own pid is used instead; signalling either ends the worker.
1607fn relayed_worker_pid(relay: u32) -> Option<u32> {
1608    let children = std::fs::read_to_string(format!("/proc/{relay}/task/{relay}/children")).ok()?;
1609    let mut pids = children.split_whitespace().map(str::parse::<u32>);
1610    match (pids.next(), pids.next()) {
1611        (Some(Ok(worker)), None) => Some(worker),
1612        _ => None,
1613    }
1614}
1615
1616#[cfg(test)]
1617pub(crate) fn handshake_for_test(
1618    kind: WorkerKind,
1619    stream: &UnixStream,
1620    child: Option<u32>,
1621) -> Result<u32, String> {
1622    handshake(kind, stream, KnownPid::Child(child)).map(|(pid, _)| pid)
1623}
1624
1625/// The `E-RUN-066` refusal for a worker of `kind` that did not start, for `reason`.
1626pub(crate) fn start_error(kind: WorkerKind, reason: &str) -> RuntimeError {
1627    RuntimeError::GuestWorkerStart(format!(
1628        "the {} guest worker could not be started: {reason}",
1629        kind.as_str()
1630    ))
1631}
1632
1633const _: fn() = || {
1634    fn is<T: Send + Sync>() {}
1635    is::<GuestWorkers>();
1636};