1use std::collections::HashMap;
2use std::sync::Arc;
3
4use muxio_core::rpc::rpc_internals::RpcStreamEvent;
5use muxio_rpc_service::prebuffered::RpcMethodPrebuffered;
6use muxio_rpc_service_caller::prebuffered::RpcCallPrebuffered;
7use muxio_rpc_service_endpoint::{RpcServiceEndpointInterface, StreamResponder};
8use muxio_tokio_rpc_ipc_server::{RpcIpcConnectionContextHandle, RpcIpcServer, RpcIpcServerEvent};
9use portable_pty::PtySize;
10use tokio::sync::{Mutex, Notify, mpsc, oneshot};
11
12use term_session_muxio_service_definitions::{
13 ChannelName, CloseSession, ListSessions, OnPtyResized, ResizePty, STREAM_INPUT_METHOD_ID,
14 SUBSCRIBE_OUTPUT_METHOD_ID, Spawn, WriteInput,
15};
16use term_wm_pty_engine::PtyStatus;
17
18use crate::session::Session;
19
20const FALLBACK_COLS: u16 = 80;
22const FALLBACK_ROWS: u16 = 24;
24const SESSION_ID: u64 = 1;
26const INPUT_CHANNEL_CAPACITY: usize = 128;
28
29const SESSION_EXIT_FLUSH_GRACE: std::time::Duration = std::time::Duration::from_millis(100);
32
33const SESSION_EXIT_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
36
37pub struct SessionServerConfig {
38 pub channel: ChannelName,
39 pub cmd: Vec<String>,
40 pub cols: u16,
41 pub rows: u16,
42}
43
44#[derive(Clone)]
45struct ClientEntry {
46 caller: Option<RpcIpcConnectionContextHandle>,
47 cols: u16,
48 rows: u16,
49}
50
51struct SubscriberEntry {
52 conn_id: usize,
53 respond: StreamResponder,
54}
55
56struct ServerState {
57 session: Option<Session>,
58 clients: HashMap<usize, ClientEntry>,
59 subscribers: Vec<SubscriberEntry>,
60 notify: Arc<Notify>,
61}
62
63impl ServerState {
64 fn new(notify: Arc<Notify>) -> Self {
65 Self {
66 session: None,
67 clients: HashMap::new(),
68 subscribers: Vec::new(),
69 notify,
70 }
71 }
72
73 fn set_session(&mut self, mut session: Session) {
76 let n = self.notify.clone();
77 session.set_status_callback(Some(Box::new(move |status| {
78 if matches!(status, PtyStatus::Wakeup | PtyStatus::Exited) {
79 n.notify_one();
80 }
81 })));
82 self.session = Some(session);
83 self.notify.notify_one();
86 }
87
88 fn clear_session(&mut self) {
91 if let Some(mut session) = self.session.take() {
92 let _ = session.pty.kill_child();
93 let raw = session.read_output();
94 if !raw.is_empty() {
95 for sub in &self.subscribers {
96 sub.respond.respond(raw.clone(), false);
97 }
98 }
99 }
100 for sub in &self.subscribers {
101 sub.respond.respond(Vec::new(), true);
102 }
103 self.subscribers.clear();
104 self.notify.notify_one();
105 }
106
107 fn recalculate_pty_size(&mut self) {
110 let Some(session) = self.session.as_mut() else {
111 return;
112 };
113 if self.clients.is_empty() {
114 return;
115 }
116 let min_cols = self
117 .clients
118 .values()
119 .map(|c| c.cols)
120 .filter(|&c| c != u16::MAX)
121 .min()
122 .unwrap_or(FALLBACK_COLS);
123 let min_rows = self
124 .clients
125 .values()
126 .map(|c| c.rows)
127 .filter(|&r| r != u16::MAX)
128 .min()
129 .unwrap_or(FALLBACK_ROWS);
130 let size = PtySize {
131 rows: min_rows,
132 cols: min_cols,
133 pixel_width: 0,
134 pixel_height: 0,
135 };
136 let _ = session.pty.resize(size);
137 session.cols = min_cols;
138 session.rows = min_rows;
139 }
140
141 fn notify_clients(clients: &[ClientEntry], cols: u16, rows: u16) {
144 for client in clients {
145 let Some(caller) = client.caller.clone() else {
146 continue;
147 };
148 tokio::spawn(async move {
149 if let Err(e) = OnPtyResized::call(&caller, (cols, rows)).await {
150 tracing::debug!(error = ?e, "Failed to deliver OnPtyResized notification");
151 }
152 });
153 }
154 }
155}
156
157type SharedState = Arc<Mutex<ServerState>>;
158
159pub async fn run_server(
161 config: SessionServerConfig,
162) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
163 let socket_name = config.channel.to_string();
164 let notify = Arc::new(Notify::new());
165 let state: SharedState = Arc::new(Mutex::new(ServerState::new(notify.clone())));
166
167 {
168 let mut st = state.lock().await;
169 let cmd = if config.cmd.is_empty() {
170 None
171 } else {
172 Some(config.cmd.clone())
173 };
174 let session = Session::spawn(
175 SESSION_ID,
176 cmd,
177 config.cols,
178 config.rows,
179 Some(&config.channel),
180 )?;
181 st.set_session(session);
182 }
183
184 let channel_id = config.channel.clone();
185
186 let (event_tx, mut event_rx) = mpsc::unbounded_channel();
187 let server = RpcIpcServer::new(Some(event_tx));
188 let endpoint = server.endpoint();
189
190 let st = Arc::clone(&state);
192 let ch = channel_id.clone();
193 endpoint
194 .register_prebuffered(Spawn::METHOD_ID, move |payload, ctx| {
195 let state = Arc::clone(&st);
196 let ch = ch.clone();
197 async move {
198 let mut guard = state.lock().await;
199 let (cmd, cols, rows) = Spawn::decode_request(&payload)
200 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
201 let entry = guard
202 .clients
203 .entry(ctx.conn_id)
204 .or_insert_with(|| ClientEntry {
205 caller: None,
206 cols,
207 rows,
208 });
209 entry.cols = cols;
210 entry.rows = rows;
211
212 if guard.session.as_ref().is_some_and(|s| !s.exited) {
214 guard.recalculate_pty_size();
215 let (ncols, nrows) = guard
216 .session
217 .as_ref()
218 .map(|s| (s.cols, s.rows))
219 .unwrap_or((FALLBACK_COLS, FALLBACK_ROWS));
220 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
221 let id = guard.session.as_ref().map(|s| s.id).unwrap_or(SESSION_ID);
222 let cols = guard.session.as_ref().map(|s| s.cols).unwrap_or(cols);
223 let rows = guard.session.as_ref().map(|s| s.rows).unwrap_or(rows);
224 drop(guard);
225 ServerState::notify_clients(&targets, ncols, nrows);
226 return Spawn::encode_response((id, cols, rows))
227 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
228 }
229 let id = SESSION_ID;
230 let session = Session::spawn(id, cmd, cols, rows, Some(&ch))?;
231 guard.set_session(session);
232 guard.recalculate_pty_size();
234 let (ncols, nrows) = guard
235 .session
236 .as_ref()
237 .map(|s| (s.cols, s.rows))
238 .unwrap_or((FALLBACK_COLS, FALLBACK_ROWS));
239 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
240 let session = guard.session.as_ref().unwrap();
241 let (sid, scol, srow) = (session.id, session.cols, session.rows);
242 drop(guard);
243 ServerState::notify_clients(&targets, ncols, nrows);
244 Spawn::encode_response((sid, scol, srow))
245 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
246 }
247 })
248 .await
249 .map_err(|e| format!("register Spawn: {e:?}"))?;
250
251 let st = Arc::clone(&state);
253 endpoint
254 .register_prebuffered(ResizePty::METHOD_ID, move |payload, ctx| {
255 let state = Arc::clone(&st);
256 async move {
257 let (_id, cols, rows) = ResizePty::decode_request(&payload)
258 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
259 let mut guard = state.lock().await;
260 if let Some(client) = guard.clients.get_mut(&ctx.conn_id) {
261 client.cols = cols;
262 client.rows = rows;
263 }
264 guard.recalculate_pty_size();
265 let (ncols, nrows) = guard
266 .session
267 .as_ref()
268 .map(|s| (s.cols, s.rows))
269 .unwrap_or((cols, rows));
270 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
271 drop(guard);
272 ServerState::notify_clients(&targets, ncols, nrows);
273 ResizePty::encode_response((ncols, nrows))
274 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
275 }
276 })
277 .await
278 .map_err(|e| format!("register ResizePty: {e:?}"))?;
279
280 let st = Arc::clone(&state);
282 endpoint
283 .register_prebuffered(CloseSession::METHOD_ID, move |payload, _ctx| {
284 let state = Arc::clone(&st);
285 async move {
286 let _id = CloseSession::decode_request(&payload)
287 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
288 let mut guard = state.lock().await;
289 guard.clear_session();
290 CloseSession::encode_response(())
291 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
292 }
293 })
294 .await
295 .map_err(|e| format!("register CloseSession: {e:?}"))?;
296
297 let st = Arc::clone(&state);
299 endpoint
300 .register_prebuffered(ListSessions::METHOD_ID, move |_payload, _ctx| {
301 let state = Arc::clone(&st);
302 async move {
303 let guard = state.lock().await;
304 let sessions = match &guard.session {
305 Some(s) => vec![(s.id, String::new(), s.exited)],
306 None => vec![],
307 };
308 ListSessions::encode_response(sessions)
309 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
310 }
311 })
312 .await
313 .map_err(|e| format!("register ListSessions: {e:?}"))?;
314
315 let st = Arc::clone(&state);
317 endpoint
318 .register_prebuffered(WriteInput::METHOD_ID, move |payload, _ctx| {
319 let state = Arc::clone(&st);
320 async move {
321 let (id, data) = WriteInput::decode_request(&payload)
322 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
323 let writer = {
324 let guard = state.lock().await;
325 guard
326 .session
327 .as_ref()
328 .filter(|s| s.id == id)
329 .map(|s| s.pty.writer_handle())
330 };
331 if let Some(writer) = writer {
335 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
336 }
337 WriteInput::encode_response(())
338 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
339 }
340 })
341 .await
342 .map_err(|e| format!("register WriteInput: {e:?}"))?;
343
344 let (input_tx, mut input_rx) = mpsc::channel::<Vec<u8>>(INPUT_CHANNEL_CAPACITY);
354 endpoint
355 .register_stream_handler(STREAM_INPUT_METHOD_ID, move |event, _responder, _ctx| {
356 if let RpcStreamEvent::PayloadChunk { bytes, .. } = event
357 && let Err(e) = input_tx.try_send(bytes)
358 {
359 tracing::warn!(error = %e, "server input buffer full; dropping input chunk");
360 }
361 })
363 .await
364 .map_err(|e| format!("register stream handler STREAM_INPUT: {e:?}"))?;
365
366 let input_st = Arc::clone(&state);
368 tokio::spawn(async move {
369 while let Some(data) = input_rx.recv().await {
370 let writer = {
371 let guard = input_st.lock().await;
372 guard.session.as_ref().map(|s| s.pty.writer_handle())
373 };
374 if let Some(writer) = writer {
378 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
379 }
380 }
381 });
382
383 let st = Arc::clone(&state);
385 endpoint
386 .register_stream_handler(SUBSCRIBE_OUTPUT_METHOD_ID, move |event, respond, ctx| {
387 let is_new = matches!(&event, RpcStreamEvent::Header { .. });
388 if is_new {
389 let st = Arc::clone(&st);
390 tokio::spawn(async move {
391 let mut guard = st.lock().await;
392
393 let early = guard.session.as_mut().and_then(|s| {
396 let data = s.read_output();
397 if data.is_empty() { None } else { Some(data) }
398 });
399 let snapshot = guard.session.as_mut().map(|s| s.generate_snapshot());
400 guard.subscribers.push(SubscriberEntry {
401 conn_id: ctx.conn_id,
402 respond: respond.clone(),
403 });
404
405 guard.notify.notify_one();
408 let is_dead = guard.session.is_none();
409 drop(guard);
410 if let Some(data) = snapshot
411 && !data.is_empty()
412 {
413 respond.respond(data, false);
414 }
415 if let Some(data) = early {
416 respond.respond(data, false);
417 }
418 if is_dead {
419 respond.respond(Vec::new(), true);
420 }
421 });
422 }
423 })
424 .await
425 .map_err(|e| format!("register SubscribeOutput: {e:?}"))?;
426
427 let st = Arc::clone(&state);
429 tokio::spawn(async move {
430 while let Some(event) = event_rx.recv().await {
431 match event {
432 RpcIpcServerEvent::ClientConnected(handle) => {
433 tracing::info!("Client {} connected", handle.0.conn_id);
434 let mut guard = st.lock().await;
435 let handle_clone = handle.clone();
436 guard.clients.insert(
437 handle.0.conn_id,
438 ClientEntry {
439 caller: Some(handle_clone),
440 cols: u16::MAX,
441 rows: u16::MAX,
442 },
443 );
444 }
445 RpcIpcServerEvent::ClientDisconnected(conn_id) => {
446 tracing::info!("Client {conn_id} disconnected");
447 let mut guard = st.lock().await;
448 guard.clients.remove(&conn_id);
449 guard.subscribers.retain(|s| s.conn_id != conn_id);
450 guard.recalculate_pty_size();
451 let (ncols, nrows) = guard
452 .session
453 .as_ref()
454 .map(|s| (s.cols, s.rows))
455 .unwrap_or((FALLBACK_COLS, FALLBACK_ROWS));
456 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
457 drop(guard);
458 ServerState::notify_clients(&targets, ncols, nrows);
459 }
460 }
461 }
462 });
463
464 let (exit_tx, mut exit_rx) = oneshot::channel::<i32>();
471 let st = Arc::clone(&state);
472 tokio::spawn(async move {
473 loop {
474 tokio::select! {
475 _ = notify.notified() => {}
476 _ = tokio::time::sleep(SESSION_EXIT_POLL_INTERVAL) => {}
477 }
478 let mut guard = st.lock().await;
479 if guard.subscribers.is_empty() {
480 let mut exited = false;
481 if let Some(session) = guard.session.as_mut() {
482 session.sync_screen();
483 exited = session.check_exited();
484 }
485 if exited {
486 tracing::info!("Session exited, tearing down");
487 guard.session = None;
488 }
489 continue;
490 }
491 let (raw, exited, code) = {
492 let Some(session) = guard.session.as_mut() else {
493 let _ = exit_tx.send(0);
494 break;
495 };
496 let raw = session.read_output();
497 let exited = session.check_exited();
498 let code = session.exit_code;
499 (raw, exited, code)
500 };
501 if raw.is_empty() && !guard.subscribers.is_empty() {
502 tracing::debug!(
503 "PTY output empty with {} subscribers",
504 guard.subscribers.len()
505 );
506 }
507
508 if !raw.is_empty() {
510 for sub in &guard.subscribers {
511 sub.respond.respond(raw.clone(), false);
512 }
513 }
514
515 if exited {
517 for sub in &guard.subscribers {
518 sub.respond.respond(Vec::new(), true);
519 }
520 guard.subscribers.clear();
521 let _ = exit_tx.send(code.unwrap_or(0));
522 tracing::info!("Session exited with code {:?}", code);
523 break;
524 }
525 }
526 });
527
528 tracing::info!("Session server listening on channel {}", config.channel);
529
530 let exit_code = tokio::select! {
532 result = async {
533 server
534 .serve(&socket_name)
535 .await
536 .map_err(|e| format!("serve: {e:?}"))
537 } => {
538 result?;
539 0
540 }
541 code = &mut exit_rx => {
542 tokio::time::sleep(SESSION_EXIT_FLUSH_GRACE).await;
548 code.unwrap_or(0)
549 }
550 };
551
552 Ok(exit_code)
553}