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