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