1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4
5use muxio_core::rpc::rpc_internals::RpcStreamEvent;
6use muxio_rpc_service::prebuffered::RpcMethodPrebuffered;
7use muxio_rpc_service_caller::prebuffered::RpcCallPrebuffered;
8use muxio_rpc_service_endpoint::{RpcServiceEndpointInterface, StreamResponder};
9use muxio_tokio_rpc_ipc_server::{RpcIpcConnectionContextHandle, RpcIpcServer, RpcIpcServerEvent};
10use portable_pty::PtySize;
11use tokio::sync::{Mutex, Notify, RwLock, mpsc, oneshot};
12
13use term_session_muxio_service_definitions::{
14 Attach, ChannelInfo, ChannelName, ClientInfo, CloseSession, KillChannel, KillClient,
15 ListChannels, ListChannelsResponse, OnPtyResized, RPC_ERROR_LIVE_SESSIONS,
16 RPC_ERROR_SHUTTING_DOWN, RPC_ERROR_UNATTACHED, ResizePty, STREAM_INPUT_METHOD_ID,
17 SUBSCRIBE_OUTPUT_METHOD_ID, SessionInfo, ShutdownGateway, Spawn, SpawnRequest, SpawnResponse,
18 WriteInput,
19};
20use term_wm_pty_engine::PtyStatus;
21
22use crate::session::Session;
23
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
37const SIGKILL_GRACE: std::time::Duration = std::time::Duration::from_millis(500);
39
40#[cfg(unix)]
44const SIGTERM: i32 = libc::SIGTERM;
45#[cfg(not(unix))]
46const SIGTERM: i32 = 15;
47#[cfg(unix)]
49const SIGKILL: i32 = libc::SIGKILL;
50#[cfg(not(unix))]
51#[allow(dead_code)]
52const SIGKILL: i32 = 9;
53
54const SHUTDOWN_FLUSH_GRACE_MS: u64 = 50;
59
60#[derive(Clone)]
63enum ConnState {
64 Unattached,
65 Attached(ChannelName),
66}
67
68#[derive(Clone)]
69struct ConnEntry {
70 handle: RpcIpcConnectionContextHandle,
71 state: ConnState,
72 hostname: String,
73 connected_at_unix: u64,
74 pid: u64,
76 user: String,
78 version: String,
80 ssh_ip: Option<String>,
82}
83
84#[derive(Clone)]
85struct ClientEntry {
86 caller: Option<RpcIpcConnectionContextHandle>,
87 hostname: String,
88 connected_at_unix: u64,
89 pid: u64,
90 user: String,
91 version: String,
92 ssh_ip: Option<String>,
93 cols: u16,
94 rows: u16,
95}
96
97struct SubscriberEntry {
98 conn_id: usize,
99 respond: StreamResponder,
100}
101
102struct ChannelState {
105 session: Option<Session>,
106 clients: HashMap<usize, ClientEntry>,
107 subscribers: Vec<SubscriberEntry>,
108 notify: Arc<Notify>,
109 created_at_unix: u64,
111 created_seq: u64,
116 cmd: Vec<String>,
118 input_tx: mpsc::Sender<Vec<u8>>,
119 kill_pending: bool,
122 is_reaped: bool,
125}
126
127struct ServerState {
133 conns: RwLock<HashMap<usize, ConnEntry>>,
134 channels: RwLock<HashMap<ChannelName, Arc<Mutex<ChannelState>>>>,
135 is_shutting_down: AtomicBool,
136 next_channel_seq: AtomicU64,
138 input_forwarders: std::sync::Mutex<HashMap<usize, mpsc::Sender<Vec<u8>>>>,
144}
145
146type SharedState = Arc<ServerState>;
147
148fn rpc_err(message: &str) -> Box<dyn std::error::Error + Send + Sync> {
149 Box::new(std::io::Error::other(message.to_string()))
150}
151
152fn boxed_io(e: std::io::Error) -> Box<dyn std::error::Error + Send + Sync> {
154 Box::new(e)
155}
156
157fn now_unix() -> u64 {
159 std::time::SystemTime::now()
160 .duration_since(std::time::UNIX_EPOCH)
161 .map(|d| d.as_secs())
162 .unwrap_or(0)
163}
164
165impl ChannelState {
166 fn new(
167 cmd: Vec<String>,
168 input_tx: mpsc::Sender<Vec<u8>>,
169 notify: Arc<Notify>,
170 created_seq: u64,
171 ) -> Self {
172 Self {
173 session: None,
174 clients: HashMap::new(),
175 subscribers: Vec::new(),
176 notify,
177 created_at_unix: now_unix(),
178 created_seq,
179 cmd,
180 input_tx,
181 kill_pending: false,
182 is_reaped: false,
183 }
184 }
185
186 fn set_session(&mut self, mut session: Session) {
189 let n = self.notify.clone();
190 session.set_status_callback(Some(Box::new(move |status| {
191 if matches!(status, PtyStatus::Wakeup | PtyStatus::Exited) {
192 n.notify_one();
193 }
194 })));
195 self.session = Some(session);
196 self.notify.notify_one();
199 }
200
201 fn request_session_kill(&mut self, signal: i32) {
206 let _ = &signal;
207 if let Some(session) = self.session.as_mut() {
208 #[cfg(unix)]
209 let _ = session.pty.signal_process_group(signal);
210 #[cfg(not(unix))]
211 let _ = session.pty.kill_child();
212 }
213 self.kill_pending = true;
214 self.notify.notify_one();
215 }
216
217 fn finalize_subscribers(&mut self) {
220 if let Some(session) = self.session.as_mut() {
221 let raw = session.read_output();
222 if !raw.is_empty() {
223 for sub in &self.subscribers {
224 sub.respond.respond(raw.clone(), false);
225 }
226 }
227 }
228 for sub in &self.subscribers {
229 sub.respond.respond(Vec::new(), true);
230 }
231 self.subscribers.clear();
232 }
233
234 fn recalculate_pty_size(&mut self) {
242 let Some(session) = self.session.as_mut() else {
243 return;
244 };
245 let Some(min_cols) = self
246 .clients
247 .values()
248 .map(|c| c.cols)
249 .filter(|&c| c != u16::MAX)
250 .min()
251 else {
252 return;
253 };
254 let Some(min_rows) = self
255 .clients
256 .values()
257 .map(|c| c.rows)
258 .filter(|&r| r != u16::MAX)
259 .min()
260 else {
261 return;
262 };
263 let size = PtySize {
264 rows: min_rows,
265 cols: min_cols,
266 pixel_width: 0,
267 pixel_height: 0,
268 };
269 let _ = session.pty.resize(size);
270 session.cols = min_cols;
271 session.rows = min_rows;
272 }
273
274 fn notify_clients(&self, clients: &[ClientEntry], cols: u16, rows: u16) {
277 for client in clients {
278 let Some(caller) = client.caller.clone() else {
279 continue;
280 };
281 tokio::spawn(async move {
282 if let Err(e) = OnPtyResized::call(&caller, (cols, rows)).await {
283 tracing::debug!(error = ?e, "Failed to deliver OnPtyResized notification");
284 }
285 });
286 }
287 }
288
289 fn to_info(&self, name: &ChannelName) -> ChannelInfo {
290 let session = self.session.as_ref().map(|s| SessionInfo {
291 id: s.id,
292 cols: s.cols,
293 rows: s.rows,
294 exited: s.exited,
295 exit_code: s.exit_code,
296 title: s.title.clone().unwrap_or_default(),
297 });
298 let mut clients: Vec<ClientInfo> = self
301 .clients
302 .iter()
303 .map(|(conn_id, c)| ClientInfo {
304 conn_id: *conn_id,
305 pid: c.pid,
306 hostname: c.hostname.clone(),
307 connected_at_unix: c.connected_at_unix,
308 cols: c.cols,
309 rows: c.rows,
310 user: c.user.clone(),
311 version: c.version.clone(),
312 ssh_ip: c.ssh_ip.clone(),
313 })
314 .collect();
315 clients.sort_by_key(|c| c.conn_id);
316 ChannelInfo {
317 name: name.to_string(),
318 created_at_unix: self.created_at_unix,
319 session,
320 clients,
321 }
322 }
323}
324
325async fn bound_channel(state: &ServerState, conn_id: usize) -> Option<ChannelName> {
327 let conns = state.conns.read().await;
328 match conns.get(&conn_id)?.state {
329 ConnState::Attached(ref name) => Some(name.clone()),
330 ConnState::Unattached => None,
331 }
332}
333
334async fn resolve_channel(
337 state: &ServerState,
338 name: &ChannelName,
339) -> Option<Arc<Mutex<ChannelState>>> {
340 let channels = state.channels.read().await;
341 channels.get(name).cloned()
342}
343
344async fn drain_input_forwarder(
354 state: SharedState,
355 conn_id: usize,
356 mut rx: mpsc::Receiver<Vec<u8>>,
357) {
358 let mut cached_tx: Option<mpsc::Sender<Vec<u8>>> = None;
359
360 while let Some(mut bytes) = rx.recv().await {
361 while let Ok(mut next) = rx.try_recv() {
363 bytes.append(&mut next);
364 }
365
366 if cached_tx.as_ref().is_none_or(|tx| tx.is_closed()) {
368 cached_tx = None;
369 if let Some(channel) = bound_channel(state.as_ref(), conn_id).await
370 && let Some(ch) = resolve_channel(state.as_ref(), &channel).await
371 {
372 let guard = ch.lock().await;
373 if !guard.is_reaped {
374 cached_tx = Some(guard.input_tx.clone());
375 }
376 }
377 }
378
379 if let Some(ref tx) = cached_tx {
380 if tx.send(bytes).await.is_err() {
383 cached_tx = None;
384 }
385 }
386 }
387}
388
389async fn get_or_create_channel(
393 state: &SharedState,
394 name: &ChannelName,
395) -> Arc<Mutex<ChannelState>> {
396 {
397 let channels = state.channels.read().await;
398 if let Some(existing) = channels.get(name) {
399 let arc = existing.clone();
400 drop(channels);
401 let is_reaped = arc.lock().await.is_reaped;
402 if !is_reaped {
403 return arc;
404 }
405 }
406 }
407
408 let mut channels = state.channels.write().await;
409 if let Some(existing) = channels.get(name) {
413 let arc = existing.clone();
414 let is_reaped = arc.lock().await.is_reaped;
415 if !is_reaped {
416 return arc;
417 }
418 }
419 let (input_tx, input_rx) = mpsc::channel::<Vec<u8>>(INPUT_CHANNEL_CAPACITY);
420 let notify = Arc::new(Notify::new());
421 let created_seq = state.next_channel_seq.fetch_add(1, Ordering::Relaxed);
422 let channel = Arc::new(Mutex::new(ChannelState::new(
423 Vec::new(),
424 input_tx,
425 notify,
426 created_seq,
427 )));
428 let ch = Arc::clone(&channel);
429 tokio::spawn(async move {
430 let mut input_rx = input_rx;
431 while let Some(mut data) = input_rx.recv().await {
432 while let Ok(mut next) = input_rx.try_recv() {
436 data.append(&mut next);
437 }
438
439 let writer = {
440 let guard = ch.lock().await;
441 guard.session.as_ref().map(|s| s.pty.writer_handle())
442 };
443 if let Some(writer) = writer {
444 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
445 }
446 }
447 });
450
451 {
455 let st = Arc::clone(state);
456 let ch = Arc::clone(&channel);
457 let notify = {
458 let locked = ch.lock().await;
459 locked.notify.clone()
460 };
461 let name_for_task = name.clone();
462 tokio::spawn(async move {
463 loop {
464 tokio::select! {
465 _ = notify.notified() => {}
466 _ = tokio::time::sleep(SESSION_EXIT_POLL_INTERVAL) => {}
467 }
468 let mut guard = ch.lock().await;
469 if guard.is_reaped {
470 break;
471 }
472 if guard.subscribers.is_empty() {
473 if let Some(session) = guard.session.as_mut() {
474 session.sync_screen();
475 if session.check_exited() {
476 tracing::info!(channel = %name_for_task, "Session exited");
477 guard.session = None;
478 guard.kill_pending = false;
479 }
480 }
481 } else {
482 let (raw, exited, code) = {
483 let Some(session) = guard.session.as_mut() else {
484 for sub in &guard.subscribers {
486 sub.respond.respond(Vec::new(), true);
487 }
488 guard.subscribers.clear();
489 guard.notify.notify_one();
490 continue;
491 };
492 let raw = session.read_output();
493 let exited = session.check_exited();
494 let code = session.exit_code;
495 (raw, exited, code)
496 };
497 if !raw.is_empty() {
498 for sub in &guard.subscribers {
499 sub.respond.respond(raw.clone(), false);
500 }
501 }
502 if exited {
503 tracing::info!(channel = %name_for_task, "Session exited with code {:?}", code);
504 for sub in &guard.subscribers {
505 sub.respond.respond(Vec::new(), true);
506 }
507 guard.subscribers.clear();
508 guard.session = None;
509 guard.kill_pending = false;
510 guard.notify.notify_one();
511 }
512 }
513 let should_reap = guard.session.is_none() && guard.clients.is_empty();
514 drop(guard);
515
516 if should_reap {
517 let mut channels = st.channels.write().await;
520 if let Some(arc) = channels.get(&name_for_task) {
521 let mut locked = arc.lock().await;
522 if locked.session.is_none() && locked.clients.is_empty() {
523 locked.is_reaped = true;
524 drop(locked);
525 channels.remove(&name_for_task);
526 tracing::info!(channel = %name_for_task, "Reaped idle channel");
527 }
528 }
529 }
530 }
535 });
536 }
537
538 channels.insert(name.clone(), Arc::clone(&channel));
539 channel
540}
541
542fn purge_input_forwarder(state: &ServerState, conn_id: usize) {
546 if let Ok(mut fwd) = state.input_forwarders.lock() {
547 fwd.remove(&conn_id);
548 }
549}
550
551async fn evict_conn(state: &ServerState, conn_id: usize) {
554 purge_input_forwarder(state, conn_id);
555 let channel = {
556 let mut conns = state.conns.write().await;
557 let entry = conns.remove(&conn_id);
558 entry.and_then(|e| match e.state {
559 ConnState::Attached(name) => Some(name),
560 ConnState::Unattached => None,
561 })
562 };
563 let Some(channel) = channel else {
564 return;
565 };
566 let Some(ch) = resolve_channel(state, &channel).await else {
567 return;
568 };
569 let mut guard = ch.lock().await;
570 guard.clients.remove(&conn_id);
571 guard.subscribers.retain(|s| s.conn_id != conn_id);
572 guard.recalculate_pty_size();
573 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
576 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
577 drop(guard);
578 let Some((ncols, nrows)) = session_size else {
579 return;
580 };
581 if let Some(ch) = resolve_channel(state, &channel).await {
584 let guard = ch.lock().await;
585 guard.notify_clients(&targets, ncols, nrows);
586 }
587}
588
589async fn spawn_kill_escalation(
598 state: &SharedState,
599 name: &ChannelName,
600) -> tokio::task::JoinHandle<()> {
601 let state = Arc::clone(state);
602 let name = name.clone();
603 tokio::spawn(async move {
604 tokio::time::sleep(SIGKILL_GRACE).await;
605 let Some(ch) = resolve_channel(&state, &name).await else {
606 return;
607 };
608 let mut guard = ch.lock().await;
609 if !guard.kill_pending {
610 return;
611 }
612 let alive = guard
614 .session
615 .as_ref()
616 .is_some_and(|s| !s.exited && s.pty.reader_is_alive());
617 guard.kill_pending = false;
618 if !alive {
619 return;
620 }
621 if let Some(session) = guard.session.as_mut() {
622 #[cfg(unix)]
623 let _ = session.pty.signal_process_group(SIGKILL);
624 #[cfg(not(unix))]
625 let _ = session.pty.kill_child();
626 }
627 })
628}
629
630pub async fn run_gateway(
633 gateway: ChannelName,
634) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
635 let socket_name = gateway.to_string();
636 let state: SharedState = Arc::new(ServerState {
637 conns: RwLock::new(HashMap::new()),
638 channels: RwLock::new(HashMap::new()),
639 is_shutting_down: AtomicBool::new(false),
640 next_channel_seq: AtomicU64::new(0),
641 input_forwarders: std::sync::Mutex::new(HashMap::new()),
642 });
643
644 let (event_tx, mut event_rx) = mpsc::unbounded_channel();
645 let server = RpcIpcServer::new(Some(event_tx));
646 let endpoint = server.endpoint();
647
648 let st = Arc::clone(&state);
650 endpoint
651 .register_prebuffered(Attach::METHOD_ID, move |payload, ctx| {
652 let state = Arc::clone(&st);
653 async move {
654 if state.is_shutting_down.load(Ordering::SeqCst) {
655 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
656 }
657 let req = Attach::decode_request(&payload)?;
658 let name = ChannelName::parse(&req.channel).map_err(|e| rpc_err(&e))?;
659 let _channel = get_or_create_channel(&state, &name).await;
660 let conn_id = ctx.conn_id;
661 purge_input_forwarder(&state, conn_id);
665 let mut conns = state.conns.write().await;
666 let entry = conns.entry(conn_id).or_insert_with(|| ConnEntry {
670 handle: RpcIpcConnectionContextHandle(ctx.clone()),
671 state: ConnState::Unattached,
672 hostname: String::new(),
673 connected_at_unix: now_unix(),
674 pid: 0,
675 user: String::new(),
676 version: String::new(),
677 ssh_ip: None,
678 });
679 entry.state = ConnState::Attached(name);
680 entry.hostname = req.hostname;
681 entry.connected_at_unix = now_unix();
682 entry.pid = req.pid;
683 entry.user = req.user;
684 entry.version = req.version;
685 entry.ssh_ip = req.ssh_ip;
686 Attach::encode_response(conn_id).map_err(boxed_io)
687 }
688 })
689 .await
690 .map_err(|e| format!("register Attach: {e:?}"))?;
691
692 let st = Arc::clone(&state);
694 endpoint
695 .register_prebuffered(Spawn::METHOD_ID, move |payload, ctx| {
696 let state = Arc::clone(&st);
697 async move {
698 if state.is_shutting_down.load(Ordering::SeqCst) {
699 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
700 }
701 let SpawnRequest {
702 cmd,
703 cols,
704 rows,
705 cwd,
706 } = Spawn::decode_request(&payload)?;
707 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
708 let Some(channel) = channel else {
709 return Err(rpc_err(RPC_ERROR_UNATTACHED));
710 };
711 let ch = get_or_create_channel(&state, &channel).await;
712 let conn_meta = {
715 let conns = state.conns.read().await;
716 conns.get(&ctx.conn_id).cloned()
717 };
718 let mut guard = ch.lock().await;
719 let entry = guard
720 .clients
721 .entry(ctx.conn_id)
722 .or_insert_with(|| ClientEntry {
723 caller: conn_meta.as_ref().map(|c| c.handle.clone()),
724 hostname: conn_meta
725 .as_ref()
726 .map(|c| c.hostname.clone())
727 .unwrap_or_default(),
728 connected_at_unix: conn_meta
729 .as_ref()
730 .map(|c| c.connected_at_unix)
731 .unwrap_or(0),
732 pid: conn_meta.as_ref().map(|c| c.pid).unwrap_or(0),
733 user: conn_meta
734 .as_ref()
735 .map(|c| c.user.clone())
736 .unwrap_or_default(),
737 version: conn_meta
738 .as_ref()
739 .map(|c| c.version.clone())
740 .unwrap_or_default(),
741 ssh_ip: conn_meta.as_ref().and_then(|c| c.ssh_ip.clone()),
742 cols,
743 rows,
744 });
745 entry.cols = cols;
746 entry.rows = rows;
747
748 if guard.session.as_ref().is_some_and(|s| !s.exited) {
750 guard.recalculate_pty_size();
751 let session = guard.session.as_ref().unwrap();
752 let (ncols, nrows) = (session.cols, session.rows);
753 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
754 let id = session.id;
755 let cols = session.cols;
756 let rows = session.rows;
757 drop(guard);
758 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
759 let g = ch.lock().await;
760 g.notify_clients(&targets, ncols, nrows);
761 }
762 return Spawn::encode_response(SpawnResponse { id, cols, rows })
763 .map_err(boxed_io);
764 }
765
766 let effective_cmd = if let Some(c) = cmd
769 && !c.is_empty()
770 {
771 guard.cmd = c.clone();
772 Some(c)
773 } else if !guard.cmd.is_empty() {
774 Some(guard.cmd.clone())
775 } else {
776 None
777 };
778 let effective_cwd = cwd.filter(|c| !c.is_empty());
782 let id = SESSION_ID;
783 let session = Session::spawn(
784 id,
785 effective_cmd,
786 cols,
787 rows,
788 Some(&channel),
789 effective_cwd.as_ref(),
790 )?;
791 guard.set_session(session);
792 guard.recalculate_pty_size();
793 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
794 let session = guard.session.as_ref().unwrap();
795 let (sid, scol, srow) = (session.id, session.cols, session.rows);
796 let (ncols, nrows) = (scol, srow);
797 drop(guard);
798 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
799 let g = ch.lock().await;
800 g.notify_clients(&targets, ncols, nrows);
801 }
802 Spawn::encode_response(SpawnResponse {
803 id: sid,
804 cols: scol,
805 rows: srow,
806 })
807 .map_err(boxed_io)
808 }
809 })
810 .await
811 .map_err(|e| format!("register Spawn: {e:?}"))?;
812
813 let st = Arc::clone(&state);
815 endpoint
816 .register_prebuffered(ResizePty::METHOD_ID, move |payload, ctx| {
817 let state = Arc::clone(&st);
818 async move {
819 if state.is_shutting_down.load(Ordering::SeqCst) {
820 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
821 }
822 let (_id, cols, rows) = ResizePty::decode_request(&payload)?;
823 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
824 let Some(channel) = channel else {
825 return Err(rpc_err(RPC_ERROR_UNATTACHED));
826 };
827 let ch = resolve_channel(state.as_ref(), &channel)
828 .await
829 .ok_or_else(|| rpc_err("channel not found"))?;
830 let mut guard = ch.lock().await;
831 if let Some(client) = guard.clients.get_mut(&ctx.conn_id) {
832 client.cols = cols;
833 client.rows = rows;
834 }
835 guard.recalculate_pty_size();
836 let (ncols, nrows) = guard
837 .session
838 .as_ref()
839 .map(|s| (s.cols, s.rows))
840 .unwrap_or((cols, rows));
841 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
842 drop(guard);
843 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
844 let g = ch.lock().await;
845 g.notify_clients(&targets, ncols, nrows);
846 }
847 ResizePty::encode_response((ncols, nrows)).map_err(boxed_io)
848 }
849 })
850 .await
851 .map_err(|e| format!("register ResizePty: {e:?}"))?;
852
853 let st = Arc::clone(&state);
855 endpoint
856 .register_prebuffered(CloseSession::METHOD_ID, move |payload, ctx| {
857 let state = Arc::clone(&st);
858 async move {
859 if state.is_shutting_down.load(Ordering::SeqCst) {
860 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
861 }
862 let _id = CloseSession::decode_request(&payload)?;
863 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
864 let Some(channel) = channel else {
865 return Err(rpc_err(RPC_ERROR_UNATTACHED));
866 };
867 let ch = resolve_channel(state.as_ref(), &channel)
868 .await
869 .ok_or_else(|| rpc_err("channel not found"))?;
870 let mut guard = ch.lock().await;
871 guard.request_session_kill(SIGTERM);
872 guard.finalize_subscribers();
873 drop(guard);
874 spawn_kill_escalation(&state, &channel).await;
876 CloseSession::encode_response(()).map_err(boxed_io)
877 }
878 })
879 .await
880 .map_err(|e| format!("register CloseSession: {e:?}"))?;
881
882 let st = Arc::clone(&state);
884 endpoint
885 .register_prebuffered(WriteInput::METHOD_ID, move |payload, ctx| {
886 let state = Arc::clone(&st);
887 async move {
888 if state.is_shutting_down.load(Ordering::SeqCst) {
889 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
890 }
891 let (id, data) = WriteInput::decode_request(&payload)?;
892 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
893 let Some(channel) = channel else {
894 return Err(rpc_err(RPC_ERROR_UNATTACHED));
895 };
896 let ch = resolve_channel(state.as_ref(), &channel)
897 .await
898 .ok_or_else(|| rpc_err("channel not found"))?;
899 let writer = {
900 let guard = ch.lock().await;
901 guard
902 .session
903 .as_ref()
904 .filter(|s| s.id == id)
905 .map(|s| s.pty.writer_handle())
906 };
907 if let Some(writer) = writer {
911 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
912 }
913 WriteInput::encode_response(()).map_err(boxed_io)
914 }
915 })
916 .await
917 .map_err(|e| format!("register WriteInput: {e:?}"))?;
918
919 let st = Arc::clone(&state);
921 endpoint
922 .register_stream_handler(STREAM_INPUT_METHOD_ID, move |event, _responder, ctx| {
923 let state = Arc::clone(&st);
924 let conn_id = ctx.conn_id;
925 match event {
926 RpcStreamEvent::PayloadChunk { bytes, .. } => {
927 let forwarder = {
935 let mut fwd = state
936 .input_forwarders
937 .lock()
938 .unwrap_or_else(|e| e.into_inner());
939 if let Some(tx) = fwd.get(&conn_id) {
940 tx.clone()
941 } else {
942 let (tx, rx) = mpsc::channel(INPUT_CHANNEL_CAPACITY);
943 fwd.insert(conn_id, tx.clone());
944 let fwd_state = Arc::clone(&state);
945 tokio::spawn(async move {
946 drain_input_forwarder(fwd_state, conn_id, rx).await;
947 });
948 tx
949 }
950 };
951 if let Err(e) = forwarder.try_send(bytes) {
952 tracing::warn!(error = %e, "gateway input buffer full; dropping input chunk");
953 }
954 }
955 RpcStreamEvent::End { .. } | RpcStreamEvent::Error { .. } => {
956 if let Ok(mut fwd) = state.input_forwarders.lock() {
961 fwd.remove(&conn_id);
962 }
963 }
964 _ => {}
965 }
966 })
967 .await
968 .map_err(|e| format!("register stream handler STREAM_INPUT: {e:?}"))?;
969
970 let st = Arc::clone(&state);
972 endpoint
973 .register_stream_handler(SUBSCRIBE_OUTPUT_METHOD_ID, move |event, respond, ctx| {
974 let is_new = matches!(&event, RpcStreamEvent::Header { .. });
975 if is_new {
976 let st = Arc::clone(&st);
977 let conn_id = ctx.conn_id;
978 tokio::spawn(async move {
979 let channel = bound_channel(&st, conn_id).await;
980 let Some(channel) = channel else {
981 return;
982 };
983 let ch = resolve_channel(&st, &channel).await;
984 let Some(ch) = ch else {
985 return;
986 };
987 let mut guard = ch.lock().await;
988 let early = guard.session.as_mut().and_then(|s| {
991 let data = s.read_output();
992 if data.is_empty() { None } else { Some(data) }
993 });
994 let snapshot = guard.session.as_mut().map(|s| s.generate_snapshot());
995 guard.subscribers.push(SubscriberEntry {
996 conn_id,
997 respond: respond.clone(),
998 });
999 guard.notify.notify_one();
1000 let is_dead = guard.session.is_none();
1001 drop(guard);
1002 if let Some(data) = snapshot
1003 && !data.is_empty()
1004 {
1005 respond.respond(data, false);
1006 }
1007 if let Some(data) = early {
1008 respond.respond(data, false);
1009 }
1010 if is_dead {
1011 respond.respond(Vec::new(), true);
1012 }
1013 });
1014 }
1015 })
1016 .await
1017 .map_err(|e| format!("register SubscribeOutput: {e:?}"))?;
1018
1019 let st = Arc::clone(&state);
1021 let list_socket = socket_name.clone();
1022 endpoint
1023 .register_prebuffered(ListChannels::METHOD_ID, move |_payload, _ctx| {
1024 let state = Arc::clone(&st);
1025 let socket = list_socket.clone();
1026 async move {
1027 let channels = {
1028 let chans = state.channels.read().await;
1029 chans
1030 .iter()
1031 .map(|(k, v)| (k.clone(), v.clone()))
1032 .collect::<Vec<_>>()
1033 };
1034 let mut out: Vec<(u64, ChannelInfo)> = Vec::with_capacity(channels.len());
1038 for (name, ch) in channels {
1039 let guard = ch.lock().await;
1040 out.push((guard.created_seq, guard.to_info(&name)));
1041 }
1042 out.sort_by_key(|(seq, _)| *seq);
1043 let out: Vec<ChannelInfo> = out.into_iter().map(|(_, info)| info).collect();
1044 ListChannels::encode_response(ListChannelsResponse {
1045 gateway_pid: std::process::id() as u64,
1046 socket,
1047 channels: out,
1048 })
1049 .map_err(boxed_io)
1050 }
1051 })
1052 .await
1053 .map_err(|e| format!("register ListChannels: {e:?}"))?;
1054
1055 let st = Arc::clone(&state);
1057 endpoint
1058 .register_prebuffered(KillChannel::METHOD_ID, move |payload, _ctx| {
1059 let state = Arc::clone(&st);
1060 async move {
1061 if state.is_shutting_down.load(Ordering::SeqCst) {
1062 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
1063 }
1064 let channel_str = KillChannel::decode_request(&payload)?;
1065 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
1066 let target_conns: Vec<usize> = {
1068 let conns = state.conns.read().await;
1069 conns
1070 .iter()
1071 .filter(|(_, entry)| matches!(entry.state, ConnState::Attached(ref n) if n == &name))
1072 .map(|(conn_id, _)| *conn_id)
1073 .collect()
1074 };
1075 if let Some(ch) = resolve_channel(state.as_ref(), &name).await {
1077 let mut guard = ch.lock().await;
1078 guard.request_session_kill(SIGTERM);
1079 guard.finalize_subscribers();
1080 for conn_id in &target_conns {
1081 guard.clients.remove(conn_id);
1082 guard.subscribers.retain(|s| s.conn_id != *conn_id);
1083 }
1084 drop(guard);
1085 spawn_kill_escalation(&state, &name).await;
1086 }
1087 let mut conns = state.conns.write().await;
1089 for conn_id in &target_conns {
1090 conns.remove(conn_id);
1091 }
1092 KillChannel::encode_response(()).map_err(boxed_io)
1093 }
1094 })
1095 .await
1096 .map_err(|e| format!("register KillChannel: {e:?}"))?;
1097
1098 let st = Arc::clone(&state);
1100 endpoint
1101 .register_prebuffered(KillClient::METHOD_ID, move |payload, _ctx| {
1102 let state = Arc::clone(&st);
1103 async move {
1104 if state.is_shutting_down.load(Ordering::SeqCst) {
1105 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
1106 }
1107 let (channel_str, conn_id) = KillClient::decode_request(&payload)?;
1108 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
1109 let bound_channel = {
1112 let conns = state.conns.read().await;
1113 conns.get(&conn_id).and_then(|c| match &c.state {
1114 ConnState::Attached(n) if n == &name => Some(n.clone()),
1115 _ => None,
1116 })
1117 };
1118 let Some(bound) = bound_channel else {
1119 return Err(rpc_err(&format!(
1120 "client {conn_id} is not attached to channel '{name}'"
1121 )));
1122 };
1123 {
1125 let mut conns = state.conns.write().await;
1126 conns.remove(&conn_id);
1127 }
1128 if let Some(ch) = resolve_channel(state.as_ref(), &bound).await {
1129 let mut guard = ch.lock().await;
1130 let mut evicted: Vec<StreamResponder> = Vec::new();
1132 let mut keep = Vec::with_capacity(guard.subscribers.len());
1133 for sub in guard.subscribers.drain(..) {
1134 if sub.conn_id == conn_id {
1135 evicted.push(sub.respond);
1136 } else {
1137 keep.push(sub);
1138 }
1139 }
1140 guard.subscribers = keep;
1141 for respond in evicted {
1142 respond.respond(Vec::new(), true);
1143 }
1144 guard.clients.remove(&conn_id);
1145 guard.recalculate_pty_size();
1146 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
1147 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
1148 drop(guard);
1149 if let (Some((ncols, nrows)), Some(ch)) =
1150 (session_size, resolve_channel(state.as_ref(), &bound).await)
1151 {
1152 let g = ch.lock().await;
1153 g.notify_clients(&targets, ncols, nrows);
1154 }
1155 }
1156 KillClient::encode_response(()).map_err(boxed_io)
1157 }
1158 })
1159 .await
1160 .map_err(|e| format!("register KillClient: {e:?}"))?;
1161
1162 let st = Arc::clone(&state);
1164 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
1165 let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
1166 endpoint
1167 .register_prebuffered(ShutdownGateway::METHOD_ID, move |payload, _ctx| {
1168 let state = Arc::clone(&st);
1169 let shutdown_tx = Arc::clone(&shutdown_tx);
1170 async move {
1171 let force = ShutdownGateway::decode_request(&payload).map_err(boxed_io)?;
1176 if !force {
1177 let live = {
1178 let chans = state.channels.read().await;
1179 let mut n = 0usize;
1180 for ch in chans.values() {
1181 let guard = ch.lock().await;
1182 if guard.session.as_ref().is_some_and(|s| !s.exited) {
1183 n += 1;
1184 }
1185 }
1186 n
1187 };
1188 if live > 0 {
1189 return Err(rpc_err(&format!(
1190 "{RPC_ERROR_LIVE_SESSIONS} ({live} live session(s))"
1191 )));
1192 }
1193 }
1194 state.is_shutting_down.store(true, Ordering::SeqCst);
1196 let channels: Vec<(ChannelName, Arc<Mutex<ChannelState>>)> = {
1198 let chans = state.channels.read().await;
1199 chans.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1200 };
1201 let mut escalations = Vec::new();
1205 for (name, ch) in channels {
1206 let mut guard = ch.lock().await;
1207 tracing::info!(channel = %name, "Shutdown: signaling session tree");
1208 guard.request_session_kill(SIGTERM);
1209 guard.finalize_subscribers();
1210 drop(guard);
1211 escalations.push(spawn_kill_escalation(&state, &name).await);
1212 }
1213 tokio::spawn(async move {
1219 for handle in escalations {
1220 let _ = handle.await;
1221 }
1222 tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_FLUSH_GRACE_MS))
1223 .await;
1224 let mut tx_guard = shutdown_tx.lock().await;
1225 if let Some(tx) = tx_guard.take() {
1226 let _ = tx.send(());
1227 }
1228 });
1229 ShutdownGateway::encode_response(()).map_err(boxed_io)
1230 }
1231 })
1232 .await
1233 .map_err(|e| format!("register ShutdownGateway: {e:?}"))?;
1234
1235 let st = Arc::clone(&state);
1237 tokio::spawn(async move {
1238 while let Some(event) = event_rx.recv().await {
1239 match event {
1240 RpcIpcServerEvent::ClientConnected(handle) => {
1241 tracing::info!("Client {} connected", handle.0.conn_id);
1242 let mut conns = st.conns.write().await;
1243 conns.entry(handle.0.conn_id).or_insert_with(|| ConnEntry {
1249 handle: handle.clone(),
1250 state: ConnState::Unattached,
1251 hostname: String::new(),
1252 connected_at_unix: now_unix(),
1253 pid: 0,
1254 user: String::new(),
1255 version: String::new(),
1256 ssh_ip: None,
1257 });
1258 }
1259 RpcIpcServerEvent::ClientDisconnected(conn_id) => {
1260 tracing::info!("Client {conn_id} disconnected");
1261 evict_conn(st.as_ref(), conn_id).await;
1262 }
1263 }
1264 }
1265 });
1266
1267 tracing::info!("Gateway listening on channel {gateway}");
1268
1269 let exit_code = tokio::select! {
1271 result = async {
1272 server.serve(&socket_name).await.map_err(|e| format!("serve: {e:?}"))
1273 } => {
1274 result?;
1275 0
1276 }
1277 _ = &mut shutdown_rx => {
1278 tokio::time::sleep(SESSION_EXIT_FLUSH_GRACE).await;
1280 0
1281 }
1282 };
1283
1284 Ok(exit_code)
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289 use super::*;
1290 use muxio_core::rpc::RpcDispatcher;
1291 use muxio_tokio_rpc_ipc_server::RpcIpcConnectionContext;
1292
1293 fn state_with_input(input_tx: mpsc::Sender<Vec<u8>>) -> SharedState {
1297 let name = ChannelName::parse("test/coalesce").expect("parse channel");
1298 let channel = Arc::new(Mutex::new(ChannelState::new(
1299 Vec::new(),
1300 input_tx,
1301 Arc::new(Notify::new()),
1302 1,
1303 )));
1304 let mut channels = HashMap::new();
1305 channels.insert(name.clone(), channel);
1306 let (write_tx, _write_rx) = mpsc::unbounded_channel();
1307 let conn = ConnEntry {
1308 handle: RpcIpcConnectionContextHandle(Arc::new(RpcIpcConnectionContext {
1309 write_tx,
1310 conn_id: 1,
1311 is_connected: Arc::new(AtomicBool::new(true)),
1312 dispatcher: Arc::new(Mutex::new(RpcDispatcher::new())),
1313 })),
1314 state: ConnState::Attached(name),
1315 hostname: String::new(),
1316 connected_at_unix: 0,
1317 pid: 0,
1318 user: String::new(),
1319 version: String::new(),
1320 ssh_ip: None,
1321 };
1322 let mut conns = HashMap::new();
1323 conns.insert(1, conn);
1324 Arc::new(ServerState {
1325 conns: RwLock::new(conns),
1326 channels: RwLock::new(channels),
1327 is_shutting_down: AtomicBool::new(false),
1328 next_channel_seq: AtomicU64::new(0),
1329 input_forwarders: std::sync::Mutex::new(HashMap::new()),
1330 })
1331 }
1332
1333 #[tokio::test]
1334 async fn drain_input_forwarder_coalesces_queued_chunks() {
1335 let (input_tx, mut input_rx) = mpsc::channel(128);
1336 let state = state_with_input(input_tx);
1337
1338 let (fwd_tx, fwd_rx) = mpsc::channel(128);
1340 fwd_tx.send(b"chunk1".to_vec()).await.unwrap();
1341 fwd_tx.send(b"chunk2".to_vec()).await.unwrap();
1342 fwd_tx.send(b"chunk3".to_vec()).await.unwrap();
1343 drop(fwd_tx); tokio::spawn(drain_input_forwarder(state, 1, fwd_rx));
1346
1347 let received = input_rx.recv().await.expect("coalesced input");
1349 assert_eq!(received, b"chunk1chunk2chunk3");
1350 }
1351
1352 #[tokio::test]
1353 async fn drain_input_forwarder_forwards_isolated_chunk_unchanged() {
1354 let (input_tx, mut input_rx) = mpsc::channel(128);
1355 let state = state_with_input(input_tx);
1356
1357 let (fwd_tx, fwd_rx) = mpsc::channel(128);
1358 fwd_tx.send(b"only".to_vec()).await.unwrap();
1359 drop(fwd_tx);
1360
1361 tokio::spawn(drain_input_forwarder(state, 1, fwd_rx));
1362
1363 let received = input_rx.recv().await.expect("forwarded input");
1364 assert_eq!(received, b"only");
1365 }
1366
1367 #[tokio::test]
1368 async fn evict_conn_purges_input_forwarder() {
1369 let (input_tx, _input_rx) = mpsc::channel(128);
1370 let state = state_with_input(input_tx);
1371 let (fwd_tx, _fwd_rx) = mpsc::channel(128);
1372 state.input_forwarders.lock().unwrap().insert(1, fwd_tx);
1373
1374 evict_conn(&state, 1).await;
1375
1376 assert!(!state.input_forwarders.lock().unwrap().contains_key(&1));
1377 }
1378
1379 #[test]
1380 fn reattach_purges_existing_forwarder() {
1381 let (input_tx, _input_rx) = mpsc::channel(128);
1387 let state = state_with_input(input_tx);
1388 let (fwd_tx, _fwd_rx) = mpsc::channel(128);
1389 state.input_forwarders.lock().unwrap().insert(1, fwd_tx);
1390
1391 purge_input_forwarder(&state, 1);
1392
1393 assert!(!state.input_forwarders.lock().unwrap().contains_key(&1));
1394 }
1395}