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