1use std::sync::{
2 Arc, Mutex,
3 atomic::{AtomicU8, AtomicU64, Ordering},
4};
5
6use sim_citizen_derive::non_citizen;
7use sim_kernel::{ClassRef, Cx, Expr, Object, Result, Symbol, Value};
8
9use crate::{
10 DeterministicWallClock, EvalSite, FrameRouter, IsolationPolicy, ServerAddress, ServerFrame,
11 ServerRuntime, TriggerHandle, WallClock, symbol_list_value,
12};
13
14static NEXT_SERVER_ID: AtomicU64 = AtomicU64::new(1);
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum ThreadMode {
19 Main,
21 Coop,
23 Spawn,
25 Pool,
27 Coroutine(Box<ThreadMode>),
29}
30
31impl ThreadMode {
32 pub fn from_expr(expr: &Expr) -> Result<Self> {
35 match expr {
36 Expr::Symbol(symbol) => match symbol.name.as_ref() {
37 "main" => Ok(Self::Main),
38 "coop" => Ok(Self::Coop),
39 "spawn" => Ok(Self::Spawn),
40 "pool" => Ok(Self::Pool),
41 other => Err(sim_kernel::Error::Eval(format!(
42 "unsupported thread mode {other}"
43 ))),
44 },
45 Expr::List(items) | Expr::Vector(items) => {
46 let Some(Expr::Symbol(head)) = items.first() else {
47 return Err(sim_kernel::Error::TypeMismatch {
48 expected: "thread mode list starting with a symbol",
49 found: "non-symbol",
50 });
51 };
52 if head.name.as_ref() != "coroutine" {
53 return Err(sim_kernel::Error::Eval(format!(
54 "unsupported thread mode {}",
55 head
56 )));
57 }
58 let base = match items.get(1) {
59 Some(expr) => Self::from_expr(expr)?,
60 None => Self::Coop,
61 };
62 Ok(Self::Coroutine(Box::new(base)))
63 }
64 _ => Err(sim_kernel::Error::TypeMismatch {
65 expected: "thread mode expression",
66 found: "non-thread-mode",
67 }),
68 }
69 }
70
71 pub fn as_expr(&self) -> Expr {
73 match self {
74 Self::Main => Expr::Symbol(Symbol::new("main")),
75 Self::Coop => Expr::Symbol(Symbol::new("coop")),
76 Self::Spawn => Expr::Symbol(Symbol::new("spawn")),
77 Self::Pool => Expr::Symbol(Symbol::new("pool")),
78 Self::Coroutine(base) => {
79 Expr::List(vec![Expr::Symbol(Symbol::new("coroutine")), base.as_expr()])
80 }
81 }
82 }
83
84 pub fn is_available_now(&self) -> bool {
86 match self {
87 Self::Main | Self::Coop | Self::Spawn | Self::Pool => true,
88 Self::Coroutine(base) => matches!(base.as_ref(), Self::Main | Self::Coop),
89 }
90 }
91
92 pub fn is_coroutine(&self) -> bool {
94 matches!(self, Self::Coroutine(_))
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum ServerStatus {
101 Running,
103 Suspended,
105 Stopped,
107}
108
109impl ServerStatus {
110 fn as_u8(self) -> u8 {
111 match self {
112 Self::Running => 0,
113 Self::Suspended => 1,
114 Self::Stopped => 2,
115 }
116 }
117
118 fn from_u8(value: u8) -> Self {
119 match value {
120 0 => Self::Running,
121 1 => Self::Suspended,
122 2 => Self::Stopped,
123 _ => Self::Stopped,
124 }
125 }
126
127 fn as_symbol(self) -> Symbol {
128 Symbol::new(match self {
129 Self::Running => "running",
130 Self::Suspended => "suspended",
131 Self::Stopped => "stopped",
132 })
133 }
134}
135
136#[non_citizen(
137 reason = "live server handle; reconstruct configuration via server/Address descriptor and start ops",
138 kind = "handle",
139 descriptor = "server/Address"
140)]
141pub struct Server {
144 id: u64,
145 name: Option<Symbol>,
146 address: ServerAddress,
147 default_codec: Symbol,
148 supported_codecs: Vec<Symbol>,
149 thread: ThreadMode,
150 isolation: IsolationPolicy,
151 status: AtomicU8,
152 site: Arc<dyn EvalSite>,
153 spec: Vec<(Symbol, Expr)>,
154 router: Arc<FrameRouter>,
155 triggers: Arc<Mutex<Vec<Arc<TriggerHandle>>>>,
156 runtime: Option<Arc<ServerRuntime>>,
157 wall_clock: Arc<dyn WallClock>,
158 started_at_ms: u64,
159}
160
161impl Server {
162 #[allow(clippy::too_many_arguments)]
164 pub fn new(
165 address: ServerAddress,
166 default_codec: Symbol,
167 supported_codecs: Vec<Symbol>,
168 thread: ThreadMode,
169 isolation: IsolationPolicy,
170 name: Option<Symbol>,
171 site: Arc<dyn EvalSite>,
172 spec: Vec<(Symbol, Expr)>,
173 ) -> Result<Self> {
174 Self::with_runtime(
175 address,
176 default_codec,
177 supported_codecs,
178 thread,
179 isolation,
180 name,
181 site,
182 spec,
183 None,
184 )
185 }
186
187 #[allow(clippy::too_many_arguments)]
190 pub fn with_runtime(
191 address: ServerAddress,
192 default_codec: Symbol,
193 supported_codecs: Vec<Symbol>,
194 thread: ThreadMode,
195 isolation: IsolationPolicy,
196 name: Option<Symbol>,
197 site: Arc<dyn EvalSite>,
198 spec: Vec<(Symbol, Expr)>,
199 runtime: Option<Arc<ServerRuntime>>,
200 ) -> Result<Self> {
201 address.ensure_transport_available()?;
202 let wall_clock: Arc<dyn WallClock> = Arc::new(DeterministicWallClock::new(0, 1));
203 let started_at_ms = wall_clock.now_ms()?;
204 Ok(Self {
205 id: NEXT_SERVER_ID.fetch_add(1, Ordering::Relaxed),
206 name,
207 address,
208 default_codec,
209 supported_codecs,
210 thread,
211 isolation,
212 status: AtomicU8::new(ServerStatus::Running.as_u8()),
213 site,
214 spec,
215 router: Arc::new(FrameRouter::default()),
216 triggers: Arc::new(Mutex::new(Vec::new())),
217 runtime,
218 wall_clock,
219 started_at_ms,
220 })
221 }
222
223 pub fn with_wall_clock(mut self, wall_clock: Arc<dyn WallClock>) -> Self {
227 self.started_at_ms = wall_clock.now_ms().unwrap_or(0);
228 self.wall_clock = wall_clock;
229 self
230 }
231
232 pub fn id(&self) -> u64 {
234 self.id
235 }
236
237 pub fn name(&self) -> Option<&Symbol> {
239 self.name.as_ref()
240 }
241
242 pub fn address(&self) -> &ServerAddress {
244 &self.address
245 }
246
247 pub fn default_codec(&self) -> &Symbol {
249 &self.default_codec
250 }
251
252 pub fn supported_codecs(&self) -> &[Symbol] {
254 &self.supported_codecs
255 }
256
257 pub fn thread(&self) -> &ThreadMode {
259 &self.thread
260 }
261
262 pub fn site(&self) -> &Arc<dyn EvalSite> {
264 &self.site
265 }
266
267 pub fn isolation(&self) -> &IsolationPolicy {
269 &self.isolation
270 }
271
272 pub fn spec(&self) -> &[(Symbol, Expr)] {
274 &self.spec
275 }
276
277 pub fn runtime(&self) -> Option<&Arc<ServerRuntime>> {
279 self.runtime.as_ref()
280 }
281
282 pub fn wall_clock(&self) -> &Arc<dyn WallClock> {
284 &self.wall_clock
285 }
286
287 pub fn status(&self) -> ServerStatus {
289 ServerStatus::from_u8(self.status.load(Ordering::Relaxed))
290 }
291
292 pub fn set_status(&self, status: ServerStatus) {
294 self.status.store(status.as_u8(), Ordering::Relaxed);
295 }
296
297 pub fn uptime_millis(&self) -> u64 {
299 self.wall_clock
300 .now_ms()
301 .unwrap_or(self.started_at_ms)
302 .saturating_sub(self.started_at_ms)
303 }
304
305 pub fn register_trigger(&self, trigger: Arc<TriggerHandle>) -> Result<()> {
307 self.triggers
308 .lock()
309 .map_err(|_| sim_kernel::Error::PoisonedLock("server triggers"))?
310 .push(trigger);
311 Ok(())
312 }
313
314 pub fn stop_triggers(&self) -> Result<()> {
316 for trigger in self.trigger_snapshots()? {
317 trigger.stop()?;
318 }
319 Ok(())
320 }
321
322 pub fn deliver_trigger_frame(&self, cx: &mut Cx, frame: ServerFrame) -> Result<()> {
324 self.router.push_inbound(frame.clone())?;
325 let _ = self.site.answer(cx, frame)?;
326 Ok(())
327 }
328
329 pub fn trigger_snapshots(&self) -> Result<Vec<Arc<TriggerHandle>>> {
331 Ok(self
332 .triggers
333 .lock()
334 .map_err(|_| sim_kernel::Error::PoisonedLock("server triggers"))?
335 .clone())
336 }
337
338 pub fn reflect_value(&self, cx: &mut Cx) -> Result<Value> {
340 let mut entries = table_entries(self, cx)?;
341 entries.extend(live_state_entries(self, cx)?);
342 cx.factory().table(entries)
343 }
344
345 pub fn health_value(&self, cx: &mut Cx) -> Result<Value> {
347 let (sessions, connections, messages_sent, messages_received) = self
348 .runtime
349 .as_ref()
350 .map(|runtime| {
351 (
352 runtime.session_count(),
353 runtime.connection_count(),
354 runtime.messages_sent(),
355 runtime.messages_received(),
356 )
357 })
358 .unwrap_or((0, 0, 0, 0));
359 cx.factory().table(vec![
360 (
361 Symbol::new("status"),
362 cx.factory().symbol(self.status().as_symbol())?,
363 ),
364 (
365 Symbol::new("uptime"),
366 cx.factory().string(self.uptime_millis().to_string())?,
367 ),
368 (
369 Symbol::new("sessions"),
370 cx.factory().string(sessions.to_string())?,
371 ),
372 (
373 Symbol::new("connections"),
374 cx.factory().string(connections.to_string())?,
375 ),
376 (
377 Symbol::new("messages-sent"),
378 cx.factory().string(messages_sent.to_string())?,
379 ),
380 (
381 Symbol::new("messages-received"),
382 cx.factory().string(messages_received.to_string())?,
383 ),
384 ])
385 }
386
387 pub fn sessions_value(&self, cx: &mut Cx) -> Result<Value> {
389 let Some(runtime) = &self.runtime else {
390 return cx.factory().list(Vec::new());
391 };
392 let sessions = runtime
393 .sessions()?
394 .into_iter()
395 .map(|session| session.as_value(cx))
396 .collect::<Result<Vec<_>>>()?;
397 cx.factory().list(sessions)
398 }
399}
400
401impl Object for Server {
402 fn display(&self, _cx: &mut Cx) -> Result<String> {
403 Ok("#<server>".to_owned())
404 }
405
406 fn as_any(&self) -> &dyn std::any::Any {
407 self
408 }
409}
410
411impl sim_kernel::ObjectCompat for Server {
412 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
413 cx.factory().class_stub(
414 sim_kernel::ClassId(0),
415 Symbol::qualified("server", "Server"),
416 )
417 }
418 fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
419 self.as_table(cx)?.object().as_expr(cx)
420 }
421 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
422 let mut entries = table_entries(self, cx)?;
423 entries.extend(live_state_entries(self, cx)?);
424 cx.factory().table(entries)
425 }
426}
427
428impl Clone for Server {
429 fn clone(&self) -> Self {
430 Self {
431 id: self.id,
432 name: self.name.clone(),
433 address: self.address.clone(),
434 default_codec: self.default_codec.clone(),
435 supported_codecs: self.supported_codecs.clone(),
436 thread: self.thread.clone(),
437 isolation: self.isolation.clone(),
438 status: AtomicU8::new(self.status().as_u8()),
439 site: self.site.clone(),
440 spec: self.spec.clone(),
441 router: self.router.clone(),
442 triggers: self.triggers.clone(),
443 runtime: self.runtime.clone(),
444 wall_clock: self.wall_clock.clone(),
445 started_at_ms: self.started_at_ms,
446 }
447 }
448}
449
450fn table_entries(server: &Server, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
451 let name = match server.name() {
452 Some(name) => cx.factory().symbol(name.clone())?,
453 None => cx.factory().nil()?,
454 };
455 let spec_entries = server
456 .spec()
457 .iter()
458 .map(|(key, value)| {
459 cx.factory()
460 .expr(Expr::List(vec![Expr::Symbol(key.clone()), value.clone()]))
461 })
462 .collect::<Result<Vec<_>>>()?;
463 let spec = cx.factory().list(spec_entries)?;
464 let address = server.address.as_value(cx)?;
465 let default_codec = cx.factory().symbol(server.default_codec.clone())?;
466 let supported_codecs = symbol_list_value(cx, &server.supported_codecs)?;
467 let thread = cx.factory().expr(server.thread.as_expr())?;
468 let site_kind = cx.factory().string(server.site.site_kind().to_owned())?;
469 let site_address = server.site.address().as_value(cx)?;
470 let site_codecs = symbol_list_value(cx, server.site.codecs())?;
471 let isolation = server.isolation.as_value(cx)?;
472 let listening = cx.factory().bool(server.runtime.is_some())?;
473 let next_msg_id = cx
474 .factory()
475 .string(server.router.peek_next_msg_id().to_string())?;
476 Ok(vec![
477 (
478 Symbol::new("kind"),
479 cx.factory().symbol(Symbol::new("server"))?,
480 ),
481 (
482 Symbol::new("id"),
483 cx.factory().string(server.id.to_string())?,
484 ),
485 (Symbol::new("name"), name),
486 (Symbol::new("address"), address),
487 (Symbol::new("default-codec"), default_codec),
488 (Symbol::new("supported-codecs"), supported_codecs),
489 (Symbol::new("thread"), thread),
490 (Symbol::new("site-kind"), site_kind),
491 (Symbol::new("site-address"), site_address),
492 (Symbol::new("site-codecs"), site_codecs),
493 (Symbol::new("isolation"), isolation),
494 (Symbol::new("listening"), listening),
495 (Symbol::new("spec"), spec),
496 (Symbol::new("next-msg-id"), next_msg_id),
497 ])
498}
499
500fn live_state_entries(server: &Server, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
501 let trigger_values = server
502 .trigger_snapshots()?
503 .into_iter()
504 .map(|trigger| trigger.reflect_value(cx))
505 .collect::<Result<Vec<_>>>()?;
506 let triggers = cx.factory().list(trigger_values)?;
507 let (sessions, connections, messages_sent, messages_received) = server
508 .runtime
509 .as_ref()
510 .map(|runtime| {
511 (
512 runtime.session_count(),
513 runtime.connection_count(),
514 runtime.messages_sent(),
515 runtime.messages_received(),
516 )
517 })
518 .unwrap_or((0, 0, 0, 0));
519 Ok(vec![
520 (
521 Symbol::new("status"),
522 cx.factory().symbol(server.status().as_symbol())?,
523 ),
524 (
525 Symbol::new("uptime"),
526 cx.factory().string(server.uptime_millis().to_string())?,
527 ),
528 (
529 Symbol::new("sessions"),
530 cx.factory().string(sessions.to_string())?,
531 ),
532 (
533 Symbol::new("connections"),
534 cx.factory().string(connections.to_string())?,
535 ),
536 (
537 Symbol::new("messages-sent"),
538 cx.factory().string(messages_sent.to_string())?,
539 ),
540 (
541 Symbol::new("messages-received"),
542 cx.factory().string(messages_received.to_string())?,
543 ),
544 (Symbol::new("triggers"), triggers),
545 (Symbol::new("line-driver"), cx.factory().nil()?),
546 ])
547}