1use 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
33pub(crate) const UNRESPONSIVE_GRACE: Duration = Duration::from_secs(5);
36
37const HANDSHAKE_WAIT: Duration = Duration::from_secs(60);
39
40static NEXT_ID: AtomicU64 = AtomicU64::new(1);
43
44fn next_id() -> u64 {
45 NEXT_ID.fetch_add(1, Ordering::Relaxed)
46}
47
48#[derive(Debug, Clone)]
50enum Start {
51 Starting,
52 Spawned,
54 Ready {
55 pid: u32,
56 },
57 Failed(String),
58}
59
60struct CallEntry {
62 description: String,
63 registered: Instant,
65 http: Arc<Mutex<NetworkPolicyHooks>>,
66 events: StreamEventsTarget,
67 input: crate::runtime::InputWiring,
68 process: tokio::sync::Mutex<RemoteProcessHost>,
69 hook: Option<Arc<HookCallHost>>,
71 capsule: Option<Arc<Mutex<CapsuleStoreState>>>,
74 bodies: Mutex<Vec<tokio::task::AbortHandle>>,
76}
77
78impl CallEntry {
79 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
124pub(crate) struct WorkerConn {
126 kind: WorkerKind,
127 start: Mutex<Start>,
128 started: Condvar,
129 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 loading: Mutex<Option<String>>,
136 lost: Mutex<Option<String>>,
138 unresponsive: Mutex<Option<String>>,
142 session_lost: Arc<Mutex<Option<String>>>,
144 host_runtime: tokio::runtime::Handle,
145 #[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 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 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 #[allow(unsafe_code)]
287 fn kill(&self) {
288 if let Some(pid) = self.pid().filter(|pid| *pid != std::process::id()) {
289 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 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 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 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 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
593fn 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
624pub(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
640type LoadedGuests = HashMap<(WorkerKind, String), Arc<tokio::sync::OnceCell<u64>>>;
642
643pub struct GuestWorkers {
645 launcher: Launcher,
646 sandboxes: Mutex<HashMap<WorkerKind, Arc<super::launch::WorkerSandbox>>>,
649 conns: Mutex<HashMap<WorkerKind, Arc<WorkerConn>>>,
650 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 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 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 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 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 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 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 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 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 #[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 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 #[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 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 *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 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 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 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 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 #[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 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 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 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 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 pub(crate) fn close_driver(&self, instance: u64) {
1235 self.close(WorkerKind::Driver, ToWorker::DriverClose { instance });
1236 }
1237
1238 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 #[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 #[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 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 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 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 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
1416async 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
1449fn 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
1465struct 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
1510fn 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 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#[derive(Debug, Clone, Copy)]
1536enum KnownPid {
1537 Child(Option<u32>),
1539 Relay(Option<u32>),
1542}
1543
1544fn 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
1604fn 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
1625pub(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};