Skip to main content

sim_lib_server/
transport.rs

1use std::{sync::Arc, thread, time::Duration};
2
3use sim_kernel::{CapabilityName, Cx, Error, Result, Symbol};
4
5use crate::{
6    EvalSite, Server, ServerAddress, ServerRuntime, ThreadMode, pool::default_worker_pool,
7};
8
9mod backends;
10mod framing;
11#[cfg(feature = "server-net-http")]
12mod http_transport;
13mod site;
14mod socket;
15#[cfg(feature = "server-net-http")]
16mod sse_transport;
17#[cfg(test)]
18mod tests;
19#[cfg(feature = "server-net-http")]
20mod ws_transport;
21
22pub use backends::{
23    LocalTransport, LoopbackTransportEndpoint, RegistryTransport, WasmConnectionTransport,
24};
25pub use framing::{decode_transport_frame, encode_transport_frame};
26#[cfg(feature = "server-net-http")]
27pub use http_transport::{HttpConnectionTransport, HttpServerTransport};
28pub use site::TransportEvalSite;
29pub use socket::{TcpConnectionTransport, TcpServerTransport};
30#[cfg(unix)]
31#[allow(unused_imports)]
32pub use socket::{UnixConnectionTransport, UnixServerTransport};
33#[cfg(feature = "server-net-http")]
34pub use sse_transport::{SseConnectionTransport, SseServerTransport};
35#[cfg(feature = "server-net-http")]
36pub use ws_transport::{WsConnectionTransport, WsServerTransport};
37
38pub(crate) use backends::TransportEndpoint;
39use backends::{has_registered_endpoint, register_endpoint, unregister_endpoint};
40use framing::{
41    answer_or_negotiate, error_frame_from_error, io_to_host, is_timeout, read_frame_from,
42    route_frame_bytes, update_negotiated_codec_from_reply, write_frame_to,
43};
44
45pub(crate) const MAX_TRANSPORT_FRAME_BYTES: usize = 8 * 1024 * 1024;
46pub(crate) const SERVER_CONNECTION_IO_TIMEOUT_MS: u64 = 250;
47pub(crate) const DEFAULT_MAX_INFLIGHT_FRAMES: usize = 8;
48pub(crate) const WEBHOOK_SERVE_CAPABILITY: &str = "webhook-serve";
49#[cfg(feature = "server-net-http")]
50pub(crate) const HTTP_TRANSPORT_PATH: &str = "/sim/frame";
51#[cfg(feature = "server-net-http")]
52pub(crate) const SSE_TRANSPORT_PATH: &str = "/sim/stream";
53#[cfg(feature = "server-net-http")]
54pub(crate) const WS_TRANSPORT_PATH: &str = "/sim/ws";
55
56/// Listening side of a transport: binds an address and accepts connections.
57pub trait ServerTransport: Send + Sync {
58    /// Returns the address this transport is bound to.
59    fn address(&self) -> &ServerAddress;
60    /// Blocks until a connection arrives and returns it.
61    fn accept(&self, cx: &mut Cx) -> Result<Box<dyn ConnectionTransport>>;
62    /// Shuts down the listener and releases its resources.
63    fn shutdown(&self, cx: &mut Cx) -> Result<()>;
64
65    /// Accepts a connection, returning `None` if `timeout` elapses first.
66    fn accept_timeout(
67        &self,
68        cx: &mut Cx,
69        timeout: Duration,
70    ) -> Result<Option<Box<dyn ConnectionTransport>>>;
71}
72
73/// One open connection over which server frames are sent and received.
74pub trait ConnectionTransport: Send + Sync {
75    /// Sends one frame over the connection.
76    fn send_frame(&mut self, cx: &mut Cx, frame: crate::ServerFrame) -> Result<()>;
77    /// Receives one frame, returning `None` on timeout or end of stream.
78    fn recv_frame(
79        &mut self,
80        cx: &mut Cx,
81        timeout: Option<Duration>,
82    ) -> Result<Option<crate::ServerFrame>>;
83    /// Closes the connection.
84    fn close(&mut self, cx: &mut Cx) -> Result<()>;
85    /// Returns this connection as `Any` for downcasting.
86    fn as_any(&self) -> &dyn std::any::Any;
87
88    /// Serves the connection server-side against `site`.
89    ///
90    /// The default implementation errors; transports that support server-side
91    /// serving override it.
92    fn serve_connection(
93        &mut self,
94        _runtime: &Arc<ServerRuntime>,
95        _site: &Arc<dyn EvalSite>,
96    ) -> Result<()> {
97        Err(Error::Eval(
98            "transport does not support server-side serving".to_owned(),
99        ))
100    }
101}
102
103pub fn start_server_transport(server: &Server) -> Result<()> {
104    if !server.address().transport_available() {
105        return Err(Error::Eval(format!(
106            "no transport for address kind {}",
107            server.address().kind_symbol()
108        )));
109    }
110    match server.address() {
111        ServerAddress::Local | ServerAddress::Any => Ok(()),
112        ServerAddress::Tcp { .. }
113        | ServerAddress::Unix { .. }
114        | ServerAddress::Http { .. }
115        | ServerAddress::Sse { .. }
116        | ServerAddress::Ws { .. } => {
117            let Some(runtime) = server.runtime().cloned() else {
118                return Ok(());
119            };
120            register_endpoint(TransportEndpoint {
121                address: server.address().clone(),
122                site: server.site().clone(),
123            })?;
124            let site = server.site().clone();
125            match server.thread() {
126                ThreadMode::Main => {
127                    run_accept_loop(runtime, site);
128                    Ok(())
129                }
130                ThreadMode::Coroutine(_) => Ok(()),
131                ThreadMode::Coop | ThreadMode::Spawn | ThreadMode::Pool => {
132                    let accept_runtime = runtime.clone();
133                    let handle = thread::spawn(move || run_accept_loop(accept_runtime, site));
134                    runtime.set_accept_thread(handle)
135                }
136            }
137        }
138        ServerAddress::Wasm { region } => {
139            let _ = crate::wasm::lookup_wasm_region(region)?;
140            Ok(())
141        }
142        _ => register_endpoint(TransportEndpoint {
143            address: server.address().clone(),
144            site: server.site().clone(),
145        }),
146    }
147}
148
149pub fn shutdown_server_transport(server: &Server) -> Result<()> {
150    match server.address() {
151        ServerAddress::Local | ServerAddress::Any => Ok(()),
152        ServerAddress::Tcp { .. }
153        | ServerAddress::Unix { .. }
154        | ServerAddress::Http { .. }
155        | ServerAddress::Sse { .. }
156        | ServerAddress::Ws { .. } => {
157            if let Some(runtime) = server.runtime() {
158                runtime.begin_stop();
159                runtime.join_accept_thread()?;
160                runtime.join_worker_threads()?;
161                runtime.with_cx(|cx| runtime.transport().shutdown(cx))?;
162                runtime.clear_sessions()?;
163            }
164            unregister_endpoint(server.address())?;
165            Ok(())
166        }
167        ServerAddress::Wasm { .. } => Ok(()),
168        _ => unregister_endpoint(server.address()),
169    }
170}
171
172pub fn require_start_capabilities(cx: &Cx, address: &ServerAddress) -> Result<()> {
173    match address {
174        ServerAddress::Tcp { .. } | ServerAddress::Unix { .. } => require_network_capability(cx),
175        ServerAddress::Http { .. } | ServerAddress::Sse { .. } | ServerAddress::Ws { .. } => {
176            require_network_capability(cx)?;
177            cx.require(&CapabilityName::new(WEBHOOK_SERVE_CAPABILITY))
178        }
179        _ => Ok(()),
180    }
181}
182
183pub fn require_connect_capabilities(cx: &Cx, address: &ServerAddress) -> Result<()> {
184    match address {
185        ServerAddress::Tcp { .. }
186        | ServerAddress::Unix { .. }
187        | ServerAddress::Http { .. }
188        | ServerAddress::Sse { .. }
189        | ServerAddress::Ws { .. } => require_network_capability(cx),
190        _ => Ok(()),
191    }
192}
193
194fn require_network_capability(cx: &Cx) -> Result<()> {
195    require_with_aliases(cx, net_http_capability(), net_http_aliases())
196}
197
198fn net_http_capability() -> CapabilityName {
199    CapabilityName::new("net/http")
200}
201
202fn net_http_aliases() -> &'static [&'static str] {
203    &["net.http", "net-connect", "network"]
204}
205
206fn require_with_aliases(
207    cx: &Cx,
208    canonical: CapabilityName,
209    aliases: &'static [&'static str],
210) -> Result<()> {
211    if cx.capabilities().contains(&canonical)
212        || aliases
213            .iter()
214            .any(|alias| cx.capabilities().contains(&CapabilityName::new(*alias)))
215    {
216        Ok(())
217    } else {
218        Err(Error::CapabilityDenied {
219            capability: canonical,
220        })
221    }
222}
223
224/// Connects to `address` and returns the eval site plus negotiated codec.
225///
226/// Loopback fallback is disabled; see
227/// [`connect_transport_site_with_loopback`].
228pub fn connect_transport_site(
229    cx: &mut Cx,
230    address: ServerAddress,
231    offered_codecs: Vec<Symbol>,
232) -> Result<(Arc<dyn EvalSite>, Symbol)> {
233    connect_transport_site_with_loopback(cx, address, offered_codecs, false)
234}
235
236/// Connects to `address`, returning the eval site plus negotiated codec.
237///
238/// When `allow_loopback` is set, a connection that fails may fall back to a
239/// registered in-process endpoint for the same address.
240pub fn connect_transport_site_with_loopback(
241    cx: &mut Cx,
242    address: ServerAddress,
243    offered_codecs: Vec<Symbol>,
244    allow_loopback: bool,
245) -> Result<(Arc<dyn EvalSite>, Symbol)> {
246    require_connect_capabilities(cx, &address)?;
247    TransportEvalSite::connect_with_loopback(cx, address, offered_codecs, allow_loopback)
248}
249
250/// Registers `site` as the loopback endpoint for `address`.
251///
252/// The returned [`LoopbackTransportEndpoint`] unregisters the endpoint when
253/// dropped.
254pub fn register_loopback_transport_endpoint(
255    address: ServerAddress,
256    site: Arc<dyn EvalSite>,
257) -> Result<LoopbackTransportEndpoint> {
258    backends::register_loopback_endpoint(address, site)
259}
260
261#[cfg(unix)]
262fn open_unix_connection_transport(
263    address: &ServerAddress,
264    allow_loopback: bool,
265) -> Result<Box<dyn ConnectionTransport>> {
266    match socket::UnixConnectionTransport::connect(address) {
267        Ok(transport) => Ok(Box::new(transport)),
268        Err(_error) if allow_loopback && has_registered_endpoint(address)? => {
269            Ok(Box::new(RegistryTransport::new(address.clone())))
270        }
271        Err(error) => Err(error),
272    }
273}
274
275#[cfg(not(unix))]
276fn open_unix_connection_transport(
277    _address: &ServerAddress,
278    _allow_loopback: bool,
279) -> Result<Box<dyn ConnectionTransport>> {
280    Err(Error::Eval(
281        "unix sockets are not available on this target".to_owned(),
282    ))
283}
284
285#[cfg(feature = "server-net-http")]
286fn open_http_connection_transport(
287    address: &ServerAddress,
288    allow_loopback: bool,
289) -> Result<Box<dyn ConnectionTransport>> {
290    match HttpConnectionTransport::connect(address) {
291        Ok(transport) => Ok(Box::new(transport)),
292        Err(_error) if allow_loopback && has_registered_endpoint(address)? => {
293            Ok(Box::new(RegistryTransport::new(address.clone())))
294        }
295        Err(error) => Err(error),
296    }
297}
298
299#[cfg(not(feature = "server-net-http"))]
300fn open_http_connection_transport(
301    _address: &ServerAddress,
302    _allow_loopback: bool,
303) -> Result<Box<dyn ConnectionTransport>> {
304    Err(http_transport_disabled_error())
305}
306
307#[cfg(feature = "server-net-http")]
308fn open_sse_connection_transport(
309    address: &ServerAddress,
310    allow_loopback: bool,
311) -> Result<Box<dyn ConnectionTransport>> {
312    match SseConnectionTransport::connect(address) {
313        Ok(transport) => Ok(Box::new(transport)),
314        Err(_error) if allow_loopback && has_registered_endpoint(address)? => {
315            Ok(Box::new(RegistryTransport::new(address.clone())))
316        }
317        Err(error) => Err(error),
318    }
319}
320
321#[cfg(not(feature = "server-net-http"))]
322fn open_sse_connection_transport(
323    _address: &ServerAddress,
324    _allow_loopback: bool,
325) -> Result<Box<dyn ConnectionTransport>> {
326    Err(http_transport_disabled_error())
327}
328
329#[cfg(feature = "server-net-http")]
330fn open_ws_connection_transport(
331    address: &ServerAddress,
332    allow_loopback: bool,
333) -> Result<Box<dyn ConnectionTransport>> {
334    match WsConnectionTransport::connect(address) {
335        Ok(transport) => Ok(Box::new(transport)),
336        Err(_error) if allow_loopback && has_registered_endpoint(address)? => {
337            Ok(Box::new(RegistryTransport::new(address.clone())))
338        }
339        Err(error) => Err(error),
340    }
341}
342
343#[cfg(not(feature = "server-net-http"))]
344fn open_ws_connection_transport(
345    _address: &ServerAddress,
346    _allow_loopback: bool,
347) -> Result<Box<dyn ConnectionTransport>> {
348    Err(http_transport_disabled_error())
349}
350
351fn open_connection_transport(
352    address: &ServerAddress,
353    allow_loopback: bool,
354) -> Result<Box<dyn ConnectionTransport>> {
355    match address {
356        ServerAddress::Local | ServerAddress::Any => Err(Error::Eval(
357            "local addresses require a direct site or server value".to_owned(),
358        )),
359        ServerAddress::InProcess { .. } | ServerAddress::Coroutine { .. } => {
360            Ok(Box::new(RegistryTransport::new(address.clone())))
361        }
362        ServerAddress::Wasm { .. } => Ok(Box::new(WasmConnectionTransport::connect(address)?)),
363        ServerAddress::Http { .. } => open_http_connection_transport(address, allow_loopback),
364        ServerAddress::Sse { .. } => open_sse_connection_transport(address, allow_loopback),
365        ServerAddress::Ws { .. } => open_ws_connection_transport(address, allow_loopback),
366        ServerAddress::Tcp { .. } => match TcpConnectionTransport::connect(address) {
367            Ok(transport) => Ok(Box::new(transport)),
368            Err(_error) if allow_loopback && has_registered_endpoint(address)? => {
369                Ok(Box::new(RegistryTransport::new(address.clone())))
370            }
371            Err(error) => Err(error),
372        },
373        ServerAddress::Unix { .. } => open_unix_connection_transport(address, allow_loopback),
374        _ => Err(Error::Eval(format!(
375            "no connection transport for address kind {}",
376            address.kind_symbol()
377        ))),
378    }
379}
380
381#[cfg(unix)]
382fn open_unix_server_transport(address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
383    Ok(Some(Arc::new(socket::UnixServerTransport::bind(address)?)))
384}
385
386#[cfg(not(unix))]
387fn open_unix_server_transport(_address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
388    Err(Error::Eval(
389        "unix sockets are not available on this target".to_owned(),
390    ))
391}
392
393#[cfg(feature = "server-net-http")]
394fn open_http_server_transport(address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
395    Ok(Some(Arc::new(HttpServerTransport::bind(address)?)))
396}
397
398#[cfg(not(feature = "server-net-http"))]
399fn open_http_server_transport(_address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
400    Err(http_transport_disabled_error())
401}
402
403#[cfg(feature = "server-net-http")]
404fn open_sse_server_transport(address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
405    Ok(Some(Arc::new(SseServerTransport::bind(address)?)))
406}
407
408#[cfg(not(feature = "server-net-http"))]
409fn open_sse_server_transport(_address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
410    Err(http_transport_disabled_error())
411}
412
413#[cfg(feature = "server-net-http")]
414fn open_ws_server_transport(address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
415    Ok(Some(Arc::new(WsServerTransport::bind(address)?)))
416}
417
418#[cfg(not(feature = "server-net-http"))]
419fn open_ws_server_transport(_address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
420    Err(http_transport_disabled_error())
421}
422
423pub fn open_server_transport(address: ServerAddress) -> Result<Option<Arc<dyn ServerTransport>>> {
424    match &address {
425        ServerAddress::Local | ServerAddress::Any | ServerAddress::Coroutine { .. } => Ok(None),
426        ServerAddress::Tcp { .. } => Ok(Some(Arc::new(TcpServerTransport::bind(address)?))),
427        ServerAddress::Unix { .. } => open_unix_server_transport(address),
428        ServerAddress::InProcess { .. } => Ok(Some(
429            Arc::new(RegistryTransport::new(address)) as Arc<dyn ServerTransport>
430        )),
431        ServerAddress::Wasm { region } => {
432            let _ = crate::wasm::lookup_wasm_region(region)?;
433            Ok(None)
434        }
435        ServerAddress::Http { .. } => open_http_server_transport(address),
436        ServerAddress::Sse { .. } => open_sse_server_transport(address),
437        ServerAddress::Ws { .. } => open_ws_server_transport(address),
438        _ => Ok(None),
439    }
440}
441
442pub(crate) fn transport_kind(address: &ServerAddress) -> &'static str {
443    match address {
444        ServerAddress::InProcess { .. } => "in-proc",
445        ServerAddress::Coroutine { .. } => "coroutine",
446        ServerAddress::Tcp { .. } => "tcp",
447        ServerAddress::Unix { .. } => "unix",
448        ServerAddress::Wasm { .. } => "wasm-shmem",
449        ServerAddress::Http { .. } => "http",
450        ServerAddress::Sse { .. } => "sse",
451        ServerAddress::Ws { .. } => "ws",
452        _ => "transport",
453    }
454}
455
456#[cfg(not(feature = "server-net-http"))]
457fn http_transport_disabled_error() -> Error {
458    Error::Eval("http transport requires the server-net-http feature".to_owned())
459}
460
461fn run_accept_loop(runtime: Arc<ServerRuntime>, site: Arc<dyn EvalSite>) {
462    while !runtime.is_stopping() {
463        let accepted = match runtime.accept_timeout(Duration::from_millis(25)) {
464            Ok(connection) => connection,
465            Err(_) => break,
466        };
467        let Some(mut connection) = accepted else {
468            thread::sleep(Duration::from_millis(25));
469            continue;
470        };
471        match runtime.thread_mode() {
472            ThreadMode::Main | ThreadMode::Coop => {
473                let _ = connection.serve_connection(&runtime, &site);
474            }
475            ThreadMode::Spawn => {
476                let runtime_for_worker = runtime.clone();
477                let site_for_worker = site.clone();
478                let handle = thread::spawn(move || {
479                    let _ = connection.serve_connection(&runtime_for_worker, &site_for_worker);
480                });
481                if runtime.register_worker_thread(handle).is_err() {
482                    runtime.begin_stop();
483                    break;
484                }
485            }
486            ThreadMode::Pool => {
487                let runtime_for_worker = runtime.clone();
488                let site_for_worker = site.clone();
489                default_worker_pool().execute(move || {
490                    let _ = connection.serve_connection(&runtime_for_worker, &site_for_worker);
491                });
492            }
493            ThreadMode::Coroutine(_) => {}
494        }
495    }
496}