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, WriteInput,
18};
19use term_wm_pty_engine::PtyStatus;
20
21use crate::session::Session;
22
23const SESSION_ID: u64 = 1;
25const INPUT_CHANNEL_CAPACITY: usize = 128;
27
28const SESSION_EXIT_FLUSH_GRACE: std::time::Duration = std::time::Duration::from_millis(100);
31
32const SESSION_EXIT_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
35
36const SIGKILL_GRACE: std::time::Duration = std::time::Duration::from_millis(500);
38
39#[cfg(unix)]
43const SIGTERM: i32 = libc::SIGTERM;
44#[cfg(not(unix))]
45const SIGTERM: i32 = 15;
46#[cfg(unix)]
48const SIGKILL: i32 = libc::SIGKILL;
49#[cfg(not(unix))]
50#[allow(dead_code)]
51const SIGKILL: i32 = 9;
52
53const SHUTDOWN_FLUSH_GRACE_MS: u64 = 50;
58
59#[derive(Clone)]
62enum ConnState {
63 Unattached,
64 Attached(ChannelName),
65}
66
67#[derive(Clone)]
68struct ConnEntry {
69 handle: RpcIpcConnectionContextHandle,
70 state: ConnState,
71 hostname: String,
72 connected_at_unix: u64,
73 pid: u64,
75 user: String,
77 version: String,
79 ssh_ip: Option<String>,
81}
82
83#[derive(Clone)]
84struct ClientEntry {
85 caller: Option<RpcIpcConnectionContextHandle>,
86 hostname: String,
87 connected_at_unix: u64,
88 pid: u64,
89 user: String,
90 version: String,
91 ssh_ip: Option<String>,
92 cols: u16,
93 rows: u16,
94}
95
96struct SubscriberEntry {
97 conn_id: usize,
98 respond: StreamResponder,
99}
100
101struct ChannelState {
104 session: Option<Session>,
105 clients: HashMap<usize, ClientEntry>,
106 subscribers: Vec<SubscriberEntry>,
107 notify: Arc<Notify>,
108 created_at_unix: u64,
110 created_seq: u64,
115 cmd: Vec<String>,
117 input_tx: mpsc::Sender<Vec<u8>>,
118 kill_pending: bool,
121 is_reaped: bool,
124}
125
126struct ServerState {
132 conns: RwLock<HashMap<usize, ConnEntry>>,
133 channels: RwLock<HashMap<ChannelName, Arc<Mutex<ChannelState>>>>,
134 is_shutting_down: AtomicBool,
135 next_channel_seq: AtomicU64,
137}
138
139type SharedState = Arc<ServerState>;
140
141fn rpc_err(message: &str) -> Box<dyn std::error::Error + Send + Sync> {
142 Box::new(std::io::Error::other(message.to_string()))
143}
144
145fn boxed_io(e: std::io::Error) -> Box<dyn std::error::Error + Send + Sync> {
147 Box::new(e)
148}
149
150fn now_unix() -> u64 {
152 std::time::SystemTime::now()
153 .duration_since(std::time::UNIX_EPOCH)
154 .map(|d| d.as_secs())
155 .unwrap_or(0)
156}
157
158impl ChannelState {
159 fn new(
160 cmd: Vec<String>,
161 input_tx: mpsc::Sender<Vec<u8>>,
162 notify: Arc<Notify>,
163 created_seq: u64,
164 ) -> Self {
165 Self {
166 session: None,
167 clients: HashMap::new(),
168 subscribers: Vec::new(),
169 notify,
170 created_at_unix: now_unix(),
171 created_seq,
172 cmd,
173 input_tx,
174 kill_pending: false,
175 is_reaped: false,
176 }
177 }
178
179 fn set_session(&mut self, mut session: Session) {
182 let n = self.notify.clone();
183 session.set_status_callback(Some(Box::new(move |status| {
184 if matches!(status, PtyStatus::Wakeup | PtyStatus::Exited) {
185 n.notify_one();
186 }
187 })));
188 self.session = Some(session);
189 self.notify.notify_one();
192 }
193
194 fn request_session_kill(&mut self, signal: i32) {
199 let _ = &signal;
200 if let Some(session) = self.session.as_mut() {
201 #[cfg(unix)]
202 let _ = session.pty.signal_process_group(signal);
203 #[cfg(not(unix))]
204 let _ = session.pty.kill_child();
205 }
206 self.kill_pending = true;
207 self.notify.notify_one();
208 }
209
210 fn finalize_subscribers(&mut self) {
213 if let Some(session) = self.session.as_mut() {
214 let raw = session.read_output();
215 if !raw.is_empty() {
216 for sub in &self.subscribers {
217 sub.respond.respond(raw.clone(), false);
218 }
219 }
220 }
221 for sub in &self.subscribers {
222 sub.respond.respond(Vec::new(), true);
223 }
224 self.subscribers.clear();
225 }
226
227 fn recalculate_pty_size(&mut self) {
235 let Some(session) = self.session.as_mut() else {
236 return;
237 };
238 let Some(min_cols) = self
239 .clients
240 .values()
241 .map(|c| c.cols)
242 .filter(|&c| c != u16::MAX)
243 .min()
244 else {
245 return;
246 };
247 let Some(min_rows) = self
248 .clients
249 .values()
250 .map(|c| c.rows)
251 .filter(|&r| r != u16::MAX)
252 .min()
253 else {
254 return;
255 };
256 let size = PtySize {
257 rows: min_rows,
258 cols: min_cols,
259 pixel_width: 0,
260 pixel_height: 0,
261 };
262 let _ = session.pty.resize(size);
263 session.cols = min_cols;
264 session.rows = min_rows;
265 }
266
267 fn notify_clients(&self, clients: &[ClientEntry], cols: u16, rows: u16) {
270 for client in clients {
271 let Some(caller) = client.caller.clone() else {
272 continue;
273 };
274 tokio::spawn(async move {
275 if let Err(e) = OnPtyResized::call(&caller, (cols, rows)).await {
276 tracing::debug!(error = ?e, "Failed to deliver OnPtyResized notification");
277 }
278 });
279 }
280 }
281
282 fn to_info(&self, name: &ChannelName) -> ChannelInfo {
283 let session = self.session.as_ref().map(|s| SessionInfo {
284 id: s.id,
285 cols: s.cols,
286 rows: s.rows,
287 exited: s.exited,
288 exit_code: s.exit_code,
289 title: s.title.clone().unwrap_or_default(),
290 });
291 let mut clients: Vec<ClientInfo> = self
294 .clients
295 .iter()
296 .map(|(conn_id, c)| ClientInfo {
297 conn_id: *conn_id,
298 pid: c.pid,
299 hostname: c.hostname.clone(),
300 connected_at_unix: c.connected_at_unix,
301 cols: c.cols,
302 rows: c.rows,
303 user: c.user.clone(),
304 version: c.version.clone(),
305 ssh_ip: c.ssh_ip.clone(),
306 })
307 .collect();
308 clients.sort_by_key(|c| c.conn_id);
309 ChannelInfo {
310 name: name.to_string(),
311 created_at_unix: self.created_at_unix,
312 session,
313 clients,
314 }
315 }
316}
317
318async fn bound_channel(state: &ServerState, conn_id: usize) -> Option<ChannelName> {
320 let conns = state.conns.read().await;
321 match conns.get(&conn_id)?.state {
322 ConnState::Attached(ref name) => Some(name.clone()),
323 ConnState::Unattached => None,
324 }
325}
326
327async fn resolve_channel(
330 state: &ServerState,
331 name: &ChannelName,
332) -> Option<Arc<Mutex<ChannelState>>> {
333 let channels = state.channels.read().await;
334 channels.get(name).cloned()
335}
336
337async fn get_or_create_channel(
341 state: &SharedState,
342 name: &ChannelName,
343) -> Arc<Mutex<ChannelState>> {
344 {
345 let channels = state.channels.read().await;
346 if let Some(existing) = channels.get(name) {
347 let arc = existing.clone();
348 drop(channels);
349 let is_reaped = arc.lock().await.is_reaped;
350 if !is_reaped {
351 return arc;
352 }
353 }
354 }
355
356 let mut channels = state.channels.write().await;
357 if let Some(existing) = channels.get(name) {
361 let arc = existing.clone();
362 let is_reaped = arc.lock().await.is_reaped;
363 if !is_reaped {
364 return arc;
365 }
366 }
367 let (input_tx, input_rx) = mpsc::channel::<Vec<u8>>(INPUT_CHANNEL_CAPACITY);
368 let notify = Arc::new(Notify::new());
369 let created_seq = state.next_channel_seq.fetch_add(1, Ordering::Relaxed);
370 let channel = Arc::new(Mutex::new(ChannelState::new(
371 Vec::new(),
372 input_tx,
373 notify,
374 created_seq,
375 )));
376 let ch = Arc::clone(&channel);
377 tokio::spawn(async move {
378 let mut input_rx = input_rx;
379 while let Some(data) = input_rx.recv().await {
380 let writer = {
381 let guard = ch.lock().await;
382 guard.session.as_ref().map(|s| s.pty.writer_handle())
383 };
384 if let Some(writer) = writer {
385 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
386 }
387 }
388 });
391
392 {
396 let st = Arc::clone(state);
397 let ch = Arc::clone(&channel);
398 let notify = {
399 let locked = ch.lock().await;
400 locked.notify.clone()
401 };
402 let name_for_task = name.clone();
403 tokio::spawn(async move {
404 loop {
405 tokio::select! {
406 _ = notify.notified() => {}
407 _ = tokio::time::sleep(SESSION_EXIT_POLL_INTERVAL) => {}
408 }
409 let mut guard = ch.lock().await;
410 if guard.is_reaped {
411 break;
412 }
413 if guard.subscribers.is_empty() {
414 if let Some(session) = guard.session.as_mut() {
415 session.sync_screen();
416 if session.check_exited() {
417 tracing::info!(channel = %name_for_task, "Session exited");
418 guard.session = None;
419 guard.kill_pending = false;
420 }
421 }
422 } else {
423 let (raw, exited, code) = {
424 let Some(session) = guard.session.as_mut() else {
425 for sub in &guard.subscribers {
427 sub.respond.respond(Vec::new(), true);
428 }
429 guard.subscribers.clear();
430 guard.notify.notify_one();
431 continue;
432 };
433 let raw = session.read_output();
434 let exited = session.check_exited();
435 let code = session.exit_code;
436 (raw, exited, code)
437 };
438 if !raw.is_empty() {
439 for sub in &guard.subscribers {
440 sub.respond.respond(raw.clone(), false);
441 }
442 }
443 if exited {
444 tracing::info!(channel = %name_for_task, "Session exited with code {:?}", code);
445 for sub in &guard.subscribers {
446 sub.respond.respond(Vec::new(), true);
447 }
448 guard.subscribers.clear();
449 guard.session = None;
450 guard.kill_pending = false;
451 guard.notify.notify_one();
452 }
453 }
454 let should_reap = guard.session.is_none() && guard.clients.is_empty();
455 drop(guard);
456
457 if should_reap {
458 let mut channels = st.channels.write().await;
461 if let Some(arc) = channels.get(&name_for_task) {
462 let mut locked = arc.lock().await;
463 if locked.session.is_none() && locked.clients.is_empty() {
464 locked.is_reaped = true;
465 drop(locked);
466 channels.remove(&name_for_task);
467 tracing::info!(channel = %name_for_task, "Reaped idle channel");
468 }
469 }
470 }
471 }
476 });
477 }
478
479 channels.insert(name.clone(), Arc::clone(&channel));
480 channel
481}
482
483async fn evict_conn(state: &ServerState, conn_id: usize) {
486 let channel = {
487 let mut conns = state.conns.write().await;
488 let entry = conns.remove(&conn_id);
489 entry.and_then(|e| match e.state {
490 ConnState::Attached(name) => Some(name),
491 ConnState::Unattached => None,
492 })
493 };
494 let Some(channel) = channel else {
495 return;
496 };
497 let Some(ch) = resolve_channel(state, &channel).await else {
498 return;
499 };
500 let mut guard = ch.lock().await;
501 guard.clients.remove(&conn_id);
502 guard.subscribers.retain(|s| s.conn_id != conn_id);
503 guard.recalculate_pty_size();
504 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
507 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
508 drop(guard);
509 let Some((ncols, nrows)) = session_size else {
510 return;
511 };
512 if let Some(ch) = resolve_channel(state, &channel).await {
515 let guard = ch.lock().await;
516 guard.notify_clients(&targets, ncols, nrows);
517 }
518}
519
520async fn spawn_kill_escalation(
529 state: &SharedState,
530 name: &ChannelName,
531) -> tokio::task::JoinHandle<()> {
532 let state = Arc::clone(state);
533 let name = name.clone();
534 tokio::spawn(async move {
535 tokio::time::sleep(SIGKILL_GRACE).await;
536 let Some(ch) = resolve_channel(&state, &name).await else {
537 return;
538 };
539 let mut guard = ch.lock().await;
540 if !guard.kill_pending {
541 return;
542 }
543 let alive = guard
545 .session
546 .as_ref()
547 .is_some_and(|s| !s.exited && s.pty.reader_is_alive());
548 guard.kill_pending = false;
549 if !alive {
550 return;
551 }
552 if let Some(session) = guard.session.as_mut() {
553 #[cfg(unix)]
554 let _ = session.pty.signal_process_group(SIGKILL);
555 #[cfg(not(unix))]
556 let _ = session.pty.kill_child();
557 }
558 })
559}
560
561pub async fn run_gateway(
564 gateway: ChannelName,
565) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
566 let socket_name = gateway.to_string();
567 let state: SharedState = Arc::new(ServerState {
568 conns: RwLock::new(HashMap::new()),
569 channels: RwLock::new(HashMap::new()),
570 is_shutting_down: AtomicBool::new(false),
571 next_channel_seq: AtomicU64::new(0),
572 });
573
574 let (event_tx, mut event_rx) = mpsc::unbounded_channel();
575 let server = RpcIpcServer::new(Some(event_tx));
576 let endpoint = server.endpoint();
577
578 let st = Arc::clone(&state);
580 endpoint
581 .register_prebuffered(Attach::METHOD_ID, move |payload, ctx| {
582 let state = Arc::clone(&st);
583 async move {
584 if state.is_shutting_down.load(Ordering::SeqCst) {
585 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
586 }
587 let req = Attach::decode_request(&payload)?;
588 let name = ChannelName::parse(&req.channel).map_err(|e| rpc_err(&e))?;
589 let _channel = get_or_create_channel(&state, &name).await;
590 let conn_id = ctx.conn_id;
591 let mut conns = state.conns.write().await;
592 let entry = conns.entry(conn_id).or_insert_with(|| ConnEntry {
596 handle: RpcIpcConnectionContextHandle(ctx.clone()),
597 state: ConnState::Unattached,
598 hostname: String::new(),
599 connected_at_unix: now_unix(),
600 pid: 0,
601 user: String::new(),
602 version: String::new(),
603 ssh_ip: None,
604 });
605 entry.state = ConnState::Attached(name);
606 entry.hostname = req.hostname;
607 entry.connected_at_unix = now_unix();
608 entry.pid = req.pid;
609 entry.user = req.user;
610 entry.version = req.version;
611 entry.ssh_ip = req.ssh_ip;
612 Attach::encode_response(conn_id).map_err(boxed_io)
613 }
614 })
615 .await
616 .map_err(|e| format!("register Attach: {e:?}"))?;
617
618 let st = Arc::clone(&state);
620 endpoint
621 .register_prebuffered(Spawn::METHOD_ID, move |payload, ctx| {
622 let state = Arc::clone(&st);
623 async move {
624 if state.is_shutting_down.load(Ordering::SeqCst) {
625 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
626 }
627 let (cmd, cols, rows) = Spawn::decode_request(&payload)?;
628 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
629 let Some(channel) = channel else {
630 return Err(rpc_err(RPC_ERROR_UNATTACHED));
631 };
632 let ch = get_or_create_channel(&state, &channel).await;
633 let conn_meta = {
636 let conns = state.conns.read().await;
637 conns.get(&ctx.conn_id).cloned()
638 };
639 let mut guard = ch.lock().await;
640 let entry = guard
641 .clients
642 .entry(ctx.conn_id)
643 .or_insert_with(|| ClientEntry {
644 caller: conn_meta.as_ref().map(|c| c.handle.clone()),
645 hostname: conn_meta
646 .as_ref()
647 .map(|c| c.hostname.clone())
648 .unwrap_or_default(),
649 connected_at_unix: conn_meta
650 .as_ref()
651 .map(|c| c.connected_at_unix)
652 .unwrap_or(0),
653 pid: conn_meta.as_ref().map(|c| c.pid).unwrap_or(0),
654 user: conn_meta
655 .as_ref()
656 .map(|c| c.user.clone())
657 .unwrap_or_default(),
658 version: conn_meta
659 .as_ref()
660 .map(|c| c.version.clone())
661 .unwrap_or_default(),
662 ssh_ip: conn_meta.as_ref().and_then(|c| c.ssh_ip.clone()),
663 cols,
664 rows,
665 });
666 entry.cols = cols;
667 entry.rows = rows;
668
669 if guard.session.as_ref().is_some_and(|s| !s.exited) {
671 guard.recalculate_pty_size();
672 let session = guard.session.as_ref().unwrap();
673 let (ncols, nrows) = (session.cols, session.rows);
674 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
675 let id = session.id;
676 let cols = session.cols;
677 let rows = session.rows;
678 drop(guard);
679 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
680 let g = ch.lock().await;
681 g.notify_clients(&targets, ncols, nrows);
682 }
683 return Spawn::encode_response((id, cols, rows)).map_err(boxed_io);
684 }
685
686 let effective_cmd = if let Some(c) = cmd
689 && !c.is_empty()
690 {
691 guard.cmd = c.clone();
692 Some(c)
693 } else if !guard.cmd.is_empty() {
694 Some(guard.cmd.clone())
695 } else {
696 None
697 };
698 let id = SESSION_ID;
699 let session = Session::spawn(id, effective_cmd, cols, rows, Some(&channel))?;
700 guard.set_session(session);
701 guard.recalculate_pty_size();
702 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
703 let session = guard.session.as_ref().unwrap();
704 let (sid, scol, srow) = (session.id, session.cols, session.rows);
705 let (ncols, nrows) = (scol, srow);
706 drop(guard);
707 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
708 let g = ch.lock().await;
709 g.notify_clients(&targets, ncols, nrows);
710 }
711 Spawn::encode_response((sid, scol, srow)).map_err(boxed_io)
712 }
713 })
714 .await
715 .map_err(|e| format!("register Spawn: {e:?}"))?;
716
717 let st = Arc::clone(&state);
719 endpoint
720 .register_prebuffered(ResizePty::METHOD_ID, move |payload, ctx| {
721 let state = Arc::clone(&st);
722 async move {
723 if state.is_shutting_down.load(Ordering::SeqCst) {
724 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
725 }
726 let (_id, cols, rows) = ResizePty::decode_request(&payload)?;
727 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
728 let Some(channel) = channel else {
729 return Err(rpc_err(RPC_ERROR_UNATTACHED));
730 };
731 let ch = resolve_channel(state.as_ref(), &channel)
732 .await
733 .ok_or_else(|| rpc_err("channel not found"))?;
734 let mut guard = ch.lock().await;
735 if let Some(client) = guard.clients.get_mut(&ctx.conn_id) {
736 client.cols = cols;
737 client.rows = rows;
738 }
739 guard.recalculate_pty_size();
740 let (ncols, nrows) = guard
741 .session
742 .as_ref()
743 .map(|s| (s.cols, s.rows))
744 .unwrap_or((cols, rows));
745 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
746 drop(guard);
747 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
748 let g = ch.lock().await;
749 g.notify_clients(&targets, ncols, nrows);
750 }
751 ResizePty::encode_response((ncols, nrows)).map_err(boxed_io)
752 }
753 })
754 .await
755 .map_err(|e| format!("register ResizePty: {e:?}"))?;
756
757 let st = Arc::clone(&state);
759 endpoint
760 .register_prebuffered(CloseSession::METHOD_ID, move |payload, ctx| {
761 let state = Arc::clone(&st);
762 async move {
763 if state.is_shutting_down.load(Ordering::SeqCst) {
764 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
765 }
766 let _id = CloseSession::decode_request(&payload)?;
767 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
768 let Some(channel) = channel else {
769 return Err(rpc_err(RPC_ERROR_UNATTACHED));
770 };
771 let ch = resolve_channel(state.as_ref(), &channel)
772 .await
773 .ok_or_else(|| rpc_err("channel not found"))?;
774 let mut guard = ch.lock().await;
775 guard.request_session_kill(SIGTERM);
776 guard.finalize_subscribers();
777 drop(guard);
778 spawn_kill_escalation(&state, &channel).await;
780 CloseSession::encode_response(()).map_err(boxed_io)
781 }
782 })
783 .await
784 .map_err(|e| format!("register CloseSession: {e:?}"))?;
785
786 let st = Arc::clone(&state);
788 endpoint
789 .register_prebuffered(WriteInput::METHOD_ID, move |payload, ctx| {
790 let state = Arc::clone(&st);
791 async move {
792 if state.is_shutting_down.load(Ordering::SeqCst) {
793 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
794 }
795 let (id, data) = WriteInput::decode_request(&payload)?;
796 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
797 let Some(channel) = channel else {
798 return Err(rpc_err(RPC_ERROR_UNATTACHED));
799 };
800 let ch = resolve_channel(state.as_ref(), &channel)
801 .await
802 .ok_or_else(|| rpc_err("channel not found"))?;
803 let writer = {
804 let guard = ch.lock().await;
805 guard
806 .session
807 .as_ref()
808 .filter(|s| s.id == id)
809 .map(|s| s.pty.writer_handle())
810 };
811 if let Some(writer) = writer {
815 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
816 }
817 WriteInput::encode_response(()).map_err(boxed_io)
818 }
819 })
820 .await
821 .map_err(|e| format!("register WriteInput: {e:?}"))?;
822
823 let st = Arc::clone(&state);
825 endpoint
826 .register_stream_handler(STREAM_INPUT_METHOD_ID, move |event, _responder, ctx| {
827 if let RpcStreamEvent::PayloadChunk { bytes, .. } = event {
828 let state = Arc::clone(&st);
829 let conn_id = ctx.conn_id;
830 tokio::spawn(async move {
831 let channel = bound_channel(state.as_ref(), conn_id).await;
832 let Some(channel) = channel else {
833 return;
834 };
835 let ch = resolve_channel(state.as_ref(), &channel).await;
836 let Some(ch) = ch else {
837 return;
838 };
839 let tx = {
840 let guard = ch.lock().await;
841 guard.input_tx.clone()
842 };
843 if let Err(e) = tx.try_send(bytes) {
844 tracing::warn!(error = %e, "gateway input buffer full; dropping input chunk");
845 }
846 });
847 }
848 })
850 .await
851 .map_err(|e| format!("register stream handler STREAM_INPUT: {e:?}"))?;
852
853 let st = Arc::clone(&state);
855 endpoint
856 .register_stream_handler(SUBSCRIBE_OUTPUT_METHOD_ID, move |event, respond, ctx| {
857 let is_new = matches!(&event, RpcStreamEvent::Header { .. });
858 if is_new {
859 let st = Arc::clone(&st);
860 let conn_id = ctx.conn_id;
861 tokio::spawn(async move {
862 let channel = bound_channel(&st, conn_id).await;
863 let Some(channel) = channel else {
864 return;
865 };
866 let ch = resolve_channel(&st, &channel).await;
867 let Some(ch) = ch else {
868 return;
869 };
870 let mut guard = ch.lock().await;
871 let early = guard.session.as_mut().and_then(|s| {
874 let data = s.read_output();
875 if data.is_empty() { None } else { Some(data) }
876 });
877 let snapshot = guard.session.as_mut().map(|s| s.generate_snapshot());
878 guard.subscribers.push(SubscriberEntry {
879 conn_id,
880 respond: respond.clone(),
881 });
882 guard.notify.notify_one();
883 let is_dead = guard.session.is_none();
884 drop(guard);
885 if let Some(data) = snapshot
886 && !data.is_empty()
887 {
888 respond.respond(data, false);
889 }
890 if let Some(data) = early {
891 respond.respond(data, false);
892 }
893 if is_dead {
894 respond.respond(Vec::new(), true);
895 }
896 });
897 }
898 })
899 .await
900 .map_err(|e| format!("register SubscribeOutput: {e:?}"))?;
901
902 let st = Arc::clone(&state);
904 let list_socket = socket_name.clone();
905 endpoint
906 .register_prebuffered(ListChannels::METHOD_ID, move |_payload, _ctx| {
907 let state = Arc::clone(&st);
908 let socket = list_socket.clone();
909 async move {
910 let channels = {
911 let chans = state.channels.read().await;
912 chans
913 .iter()
914 .map(|(k, v)| (k.clone(), v.clone()))
915 .collect::<Vec<_>>()
916 };
917 let mut out: Vec<(u64, ChannelInfo)> = Vec::with_capacity(channels.len());
921 for (name, ch) in channels {
922 let guard = ch.lock().await;
923 out.push((guard.created_seq, guard.to_info(&name)));
924 }
925 out.sort_by_key(|(seq, _)| *seq);
926 let out: Vec<ChannelInfo> = out.into_iter().map(|(_, info)| info).collect();
927 ListChannels::encode_response(ListChannelsResponse {
928 gateway_pid: std::process::id() as u64,
929 socket,
930 channels: out,
931 })
932 .map_err(boxed_io)
933 }
934 })
935 .await
936 .map_err(|e| format!("register ListChannels: {e:?}"))?;
937
938 let st = Arc::clone(&state);
940 endpoint
941 .register_prebuffered(KillChannel::METHOD_ID, move |payload, _ctx| {
942 let state = Arc::clone(&st);
943 async move {
944 if state.is_shutting_down.load(Ordering::SeqCst) {
945 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
946 }
947 let channel_str = KillChannel::decode_request(&payload)?;
948 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
949 let target_conns: Vec<usize> = {
951 let conns = state.conns.read().await;
952 conns
953 .iter()
954 .filter(|(_, entry)| matches!(entry.state, ConnState::Attached(ref n) if n == &name))
955 .map(|(conn_id, _)| *conn_id)
956 .collect()
957 };
958 if let Some(ch) = resolve_channel(state.as_ref(), &name).await {
960 let mut guard = ch.lock().await;
961 guard.request_session_kill(SIGTERM);
962 guard.finalize_subscribers();
963 for conn_id in &target_conns {
964 guard.clients.remove(conn_id);
965 guard.subscribers.retain(|s| s.conn_id != *conn_id);
966 }
967 drop(guard);
968 spawn_kill_escalation(&state, &name).await;
969 }
970 let mut conns = state.conns.write().await;
972 for conn_id in &target_conns {
973 conns.remove(conn_id);
974 }
975 KillChannel::encode_response(()).map_err(boxed_io)
976 }
977 })
978 .await
979 .map_err(|e| format!("register KillChannel: {e:?}"))?;
980
981 let st = Arc::clone(&state);
983 endpoint
984 .register_prebuffered(KillClient::METHOD_ID, move |payload, _ctx| {
985 let state = Arc::clone(&st);
986 async move {
987 if state.is_shutting_down.load(Ordering::SeqCst) {
988 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
989 }
990 let (channel_str, conn_id) = KillClient::decode_request(&payload)?;
991 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
992 let bound_channel = {
995 let conns = state.conns.read().await;
996 conns.get(&conn_id).and_then(|c| match &c.state {
997 ConnState::Attached(n) if n == &name => Some(n.clone()),
998 _ => None,
999 })
1000 };
1001 let Some(bound) = bound_channel else {
1002 return Err(rpc_err(&format!(
1003 "client {conn_id} is not attached to channel '{name}'"
1004 )));
1005 };
1006 {
1008 let mut conns = state.conns.write().await;
1009 conns.remove(&conn_id);
1010 }
1011 if let Some(ch) = resolve_channel(state.as_ref(), &bound).await {
1012 let mut guard = ch.lock().await;
1013 let mut evicted: Vec<StreamResponder> = Vec::new();
1015 let mut keep = Vec::with_capacity(guard.subscribers.len());
1016 for sub in guard.subscribers.drain(..) {
1017 if sub.conn_id == conn_id {
1018 evicted.push(sub.respond);
1019 } else {
1020 keep.push(sub);
1021 }
1022 }
1023 guard.subscribers = keep;
1024 for respond in evicted {
1025 respond.respond(Vec::new(), true);
1026 }
1027 guard.clients.remove(&conn_id);
1028 guard.recalculate_pty_size();
1029 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
1030 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
1031 drop(guard);
1032 if let (Some((ncols, nrows)), Some(ch)) =
1033 (session_size, resolve_channel(state.as_ref(), &bound).await)
1034 {
1035 let g = ch.lock().await;
1036 g.notify_clients(&targets, ncols, nrows);
1037 }
1038 }
1039 KillClient::encode_response(()).map_err(boxed_io)
1040 }
1041 })
1042 .await
1043 .map_err(|e| format!("register KillClient: {e:?}"))?;
1044
1045 let st = Arc::clone(&state);
1047 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
1048 let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
1049 endpoint
1050 .register_prebuffered(ShutdownGateway::METHOD_ID, move |payload, _ctx| {
1051 let state = Arc::clone(&st);
1052 let shutdown_tx = Arc::clone(&shutdown_tx);
1053 async move {
1054 let force = ShutdownGateway::decode_request(&payload).map_err(boxed_io)?;
1059 if !force {
1060 let live = {
1061 let chans = state.channels.read().await;
1062 let mut n = 0usize;
1063 for ch in chans.values() {
1064 let guard = ch.lock().await;
1065 if guard.session.as_ref().is_some_and(|s| !s.exited) {
1066 n += 1;
1067 }
1068 }
1069 n
1070 };
1071 if live > 0 {
1072 return Err(rpc_err(&format!(
1073 "{RPC_ERROR_LIVE_SESSIONS} ({live} live session(s))"
1074 )));
1075 }
1076 }
1077 state.is_shutting_down.store(true, Ordering::SeqCst);
1079 let channels: Vec<(ChannelName, Arc<Mutex<ChannelState>>)> = {
1081 let chans = state.channels.read().await;
1082 chans.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1083 };
1084 let mut escalations = Vec::new();
1088 for (name, ch) in channels {
1089 let mut guard = ch.lock().await;
1090 tracing::info!(channel = %name, "Shutdown: signaling session tree");
1091 guard.request_session_kill(SIGTERM);
1092 guard.finalize_subscribers();
1093 drop(guard);
1094 escalations.push(spawn_kill_escalation(&state, &name).await);
1095 }
1096 tokio::spawn(async move {
1102 for handle in escalations {
1103 let _ = handle.await;
1104 }
1105 tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_FLUSH_GRACE_MS))
1106 .await;
1107 let mut tx_guard = shutdown_tx.lock().await;
1108 if let Some(tx) = tx_guard.take() {
1109 let _ = tx.send(());
1110 }
1111 });
1112 ShutdownGateway::encode_response(()).map_err(boxed_io)
1113 }
1114 })
1115 .await
1116 .map_err(|e| format!("register ShutdownGateway: {e:?}"))?;
1117
1118 let st = Arc::clone(&state);
1120 tokio::spawn(async move {
1121 while let Some(event) = event_rx.recv().await {
1122 match event {
1123 RpcIpcServerEvent::ClientConnected(handle) => {
1124 tracing::info!("Client {} connected", handle.0.conn_id);
1125 let mut conns = st.conns.write().await;
1126 conns.entry(handle.0.conn_id).or_insert_with(|| ConnEntry {
1132 handle: handle.clone(),
1133 state: ConnState::Unattached,
1134 hostname: String::new(),
1135 connected_at_unix: now_unix(),
1136 pid: 0,
1137 user: String::new(),
1138 version: String::new(),
1139 ssh_ip: None,
1140 });
1141 }
1142 RpcIpcServerEvent::ClientDisconnected(conn_id) => {
1143 tracing::info!("Client {conn_id} disconnected");
1144 evict_conn(st.as_ref(), conn_id).await;
1145 }
1146 }
1147 }
1148 });
1149
1150 tracing::info!("Gateway listening on channel {gateway}");
1151
1152 let exit_code = tokio::select! {
1154 result = async {
1155 server.serve(&socket_name).await.map_err(|e| format!("serve: {e:?}"))
1156 } => {
1157 result?;
1158 0
1159 }
1160 _ = &mut shutdown_rx => {
1161 tokio::time::sleep(SESSION_EXIT_FLUSH_GRACE).await;
1163 0
1164 }
1165 };
1166
1167 Ok(exit_code)
1168}