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(
350 state: SharedState,
351 conn_id: usize,
352 mut rx: mpsc::Receiver<Vec<u8>>,
353) {
354 while let Some(bytes) = rx.recv().await {
355 let Some(channel) = bound_channel(state.as_ref(), conn_id).await else {
356 continue;
357 };
358 let Some(ch) = resolve_channel(state.as_ref(), &channel).await else {
359 continue;
360 };
361 let tx = {
362 let guard = ch.lock().await;
363 guard.input_tx.clone()
364 };
365 if tx.send(bytes).await.is_err() {
368 break;
369 }
370 }
371}
372
373async fn get_or_create_channel(
377 state: &SharedState,
378 name: &ChannelName,
379) -> Arc<Mutex<ChannelState>> {
380 {
381 let channels = state.channels.read().await;
382 if let Some(existing) = channels.get(name) {
383 let arc = existing.clone();
384 drop(channels);
385 let is_reaped = arc.lock().await.is_reaped;
386 if !is_reaped {
387 return arc;
388 }
389 }
390 }
391
392 let mut channels = state.channels.write().await;
393 if let Some(existing) = channels.get(name) {
397 let arc = existing.clone();
398 let is_reaped = arc.lock().await.is_reaped;
399 if !is_reaped {
400 return arc;
401 }
402 }
403 let (input_tx, input_rx) = mpsc::channel::<Vec<u8>>(INPUT_CHANNEL_CAPACITY);
404 let notify = Arc::new(Notify::new());
405 let created_seq = state.next_channel_seq.fetch_add(1, Ordering::Relaxed);
406 let channel = Arc::new(Mutex::new(ChannelState::new(
407 Vec::new(),
408 input_tx,
409 notify,
410 created_seq,
411 )));
412 let ch = Arc::clone(&channel);
413 tokio::spawn(async move {
414 let mut input_rx = input_rx;
415 while let Some(data) = input_rx.recv().await {
416 let writer = {
417 let guard = ch.lock().await;
418 guard.session.as_ref().map(|s| s.pty.writer_handle())
419 };
420 if let Some(writer) = writer {
421 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
422 }
423 }
424 });
427
428 {
432 let st = Arc::clone(state);
433 let ch = Arc::clone(&channel);
434 let notify = {
435 let locked = ch.lock().await;
436 locked.notify.clone()
437 };
438 let name_for_task = name.clone();
439 tokio::spawn(async move {
440 loop {
441 tokio::select! {
442 _ = notify.notified() => {}
443 _ = tokio::time::sleep(SESSION_EXIT_POLL_INTERVAL) => {}
444 }
445 let mut guard = ch.lock().await;
446 if guard.is_reaped {
447 break;
448 }
449 if guard.subscribers.is_empty() {
450 if let Some(session) = guard.session.as_mut() {
451 session.sync_screen();
452 if session.check_exited() {
453 tracing::info!(channel = %name_for_task, "Session exited");
454 guard.session = None;
455 guard.kill_pending = false;
456 }
457 }
458 } else {
459 let (raw, exited, code) = {
460 let Some(session) = guard.session.as_mut() else {
461 for sub in &guard.subscribers {
463 sub.respond.respond(Vec::new(), true);
464 }
465 guard.subscribers.clear();
466 guard.notify.notify_one();
467 continue;
468 };
469 let raw = session.read_output();
470 let exited = session.check_exited();
471 let code = session.exit_code;
472 (raw, exited, code)
473 };
474 if !raw.is_empty() {
475 for sub in &guard.subscribers {
476 sub.respond.respond(raw.clone(), false);
477 }
478 }
479 if exited {
480 tracing::info!(channel = %name_for_task, "Session exited with code {:?}", code);
481 for sub in &guard.subscribers {
482 sub.respond.respond(Vec::new(), true);
483 }
484 guard.subscribers.clear();
485 guard.session = None;
486 guard.kill_pending = false;
487 guard.notify.notify_one();
488 }
489 }
490 let should_reap = guard.session.is_none() && guard.clients.is_empty();
491 drop(guard);
492
493 if should_reap {
494 let mut channels = st.channels.write().await;
497 if let Some(arc) = channels.get(&name_for_task) {
498 let mut locked = arc.lock().await;
499 if locked.session.is_none() && locked.clients.is_empty() {
500 locked.is_reaped = true;
501 drop(locked);
502 channels.remove(&name_for_task);
503 tracing::info!(channel = %name_for_task, "Reaped idle channel");
504 }
505 }
506 }
507 }
512 });
513 }
514
515 channels.insert(name.clone(), Arc::clone(&channel));
516 channel
517}
518
519async fn evict_conn(state: &ServerState, conn_id: usize) {
522 let channel = {
523 let mut conns = state.conns.write().await;
524 let entry = conns.remove(&conn_id);
525 entry.and_then(|e| match e.state {
526 ConnState::Attached(name) => Some(name),
527 ConnState::Unattached => None,
528 })
529 };
530 let Some(channel) = channel else {
531 return;
532 };
533 let Some(ch) = resolve_channel(state, &channel).await else {
534 return;
535 };
536 let mut guard = ch.lock().await;
537 guard.clients.remove(&conn_id);
538 guard.subscribers.retain(|s| s.conn_id != conn_id);
539 guard.recalculate_pty_size();
540 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
543 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
544 drop(guard);
545 let Some((ncols, nrows)) = session_size else {
546 return;
547 };
548 if let Some(ch) = resolve_channel(state, &channel).await {
551 let guard = ch.lock().await;
552 guard.notify_clients(&targets, ncols, nrows);
553 }
554}
555
556async fn spawn_kill_escalation(
565 state: &SharedState,
566 name: &ChannelName,
567) -> tokio::task::JoinHandle<()> {
568 let state = Arc::clone(state);
569 let name = name.clone();
570 tokio::spawn(async move {
571 tokio::time::sleep(SIGKILL_GRACE).await;
572 let Some(ch) = resolve_channel(&state, &name).await else {
573 return;
574 };
575 let mut guard = ch.lock().await;
576 if !guard.kill_pending {
577 return;
578 }
579 let alive = guard
581 .session
582 .as_ref()
583 .is_some_and(|s| !s.exited && s.pty.reader_is_alive());
584 guard.kill_pending = false;
585 if !alive {
586 return;
587 }
588 if let Some(session) = guard.session.as_mut() {
589 #[cfg(unix)]
590 let _ = session.pty.signal_process_group(SIGKILL);
591 #[cfg(not(unix))]
592 let _ = session.pty.kill_child();
593 }
594 })
595}
596
597pub async fn run_gateway(
600 gateway: ChannelName,
601) -> Result<i32, Box<dyn std::error::Error + Send + Sync>> {
602 let socket_name = gateway.to_string();
603 let state: SharedState = Arc::new(ServerState {
604 conns: RwLock::new(HashMap::new()),
605 channels: RwLock::new(HashMap::new()),
606 is_shutting_down: AtomicBool::new(false),
607 next_channel_seq: AtomicU64::new(0),
608 input_forwarders: std::sync::Mutex::new(HashMap::new()),
609 });
610
611 let (event_tx, mut event_rx) = mpsc::unbounded_channel();
612 let server = RpcIpcServer::new(Some(event_tx));
613 let endpoint = server.endpoint();
614
615 let st = Arc::clone(&state);
617 endpoint
618 .register_prebuffered(Attach::METHOD_ID, move |payload, ctx| {
619 let state = Arc::clone(&st);
620 async move {
621 if state.is_shutting_down.load(Ordering::SeqCst) {
622 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
623 }
624 let req = Attach::decode_request(&payload)?;
625 let name = ChannelName::parse(&req.channel).map_err(|e| rpc_err(&e))?;
626 let _channel = get_or_create_channel(&state, &name).await;
627 let conn_id = ctx.conn_id;
628 let mut conns = state.conns.write().await;
629 let entry = conns.entry(conn_id).or_insert_with(|| ConnEntry {
633 handle: RpcIpcConnectionContextHandle(ctx.clone()),
634 state: ConnState::Unattached,
635 hostname: String::new(),
636 connected_at_unix: now_unix(),
637 pid: 0,
638 user: String::new(),
639 version: String::new(),
640 ssh_ip: None,
641 });
642 entry.state = ConnState::Attached(name);
643 entry.hostname = req.hostname;
644 entry.connected_at_unix = now_unix();
645 entry.pid = req.pid;
646 entry.user = req.user;
647 entry.version = req.version;
648 entry.ssh_ip = req.ssh_ip;
649 Attach::encode_response(conn_id).map_err(boxed_io)
650 }
651 })
652 .await
653 .map_err(|e| format!("register Attach: {e:?}"))?;
654
655 let st = Arc::clone(&state);
657 endpoint
658 .register_prebuffered(Spawn::METHOD_ID, move |payload, ctx| {
659 let state = Arc::clone(&st);
660 async move {
661 if state.is_shutting_down.load(Ordering::SeqCst) {
662 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
663 }
664 let SpawnRequest {
665 cmd,
666 cols,
667 rows,
668 cwd,
669 } = Spawn::decode_request(&payload)?;
670 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
671 let Some(channel) = channel else {
672 return Err(rpc_err(RPC_ERROR_UNATTACHED));
673 };
674 let ch = get_or_create_channel(&state, &channel).await;
675 let conn_meta = {
678 let conns = state.conns.read().await;
679 conns.get(&ctx.conn_id).cloned()
680 };
681 let mut guard = ch.lock().await;
682 let entry = guard
683 .clients
684 .entry(ctx.conn_id)
685 .or_insert_with(|| ClientEntry {
686 caller: conn_meta.as_ref().map(|c| c.handle.clone()),
687 hostname: conn_meta
688 .as_ref()
689 .map(|c| c.hostname.clone())
690 .unwrap_or_default(),
691 connected_at_unix: conn_meta
692 .as_ref()
693 .map(|c| c.connected_at_unix)
694 .unwrap_or(0),
695 pid: conn_meta.as_ref().map(|c| c.pid).unwrap_or(0),
696 user: conn_meta
697 .as_ref()
698 .map(|c| c.user.clone())
699 .unwrap_or_default(),
700 version: conn_meta
701 .as_ref()
702 .map(|c| c.version.clone())
703 .unwrap_or_default(),
704 ssh_ip: conn_meta.as_ref().and_then(|c| c.ssh_ip.clone()),
705 cols,
706 rows,
707 });
708 entry.cols = cols;
709 entry.rows = rows;
710
711 if guard.session.as_ref().is_some_and(|s| !s.exited) {
713 guard.recalculate_pty_size();
714 let session = guard.session.as_ref().unwrap();
715 let (ncols, nrows) = (session.cols, session.rows);
716 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
717 let id = session.id;
718 let cols = session.cols;
719 let rows = session.rows;
720 drop(guard);
721 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
722 let g = ch.lock().await;
723 g.notify_clients(&targets, ncols, nrows);
724 }
725 return Spawn::encode_response(SpawnResponse { id, cols, rows })
726 .map_err(boxed_io);
727 }
728
729 let effective_cmd = if let Some(c) = cmd
732 && !c.is_empty()
733 {
734 guard.cmd = c.clone();
735 Some(c)
736 } else if !guard.cmd.is_empty() {
737 Some(guard.cmd.clone())
738 } else {
739 None
740 };
741 let effective_cwd = cwd.filter(|c| !c.is_empty());
745 let id = SESSION_ID;
746 let session = Session::spawn(
747 id,
748 effective_cmd,
749 cols,
750 rows,
751 Some(&channel),
752 effective_cwd.as_ref(),
753 )?;
754 guard.set_session(session);
755 guard.recalculate_pty_size();
756 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
757 let session = guard.session.as_ref().unwrap();
758 let (sid, scol, srow) = (session.id, session.cols, session.rows);
759 let (ncols, nrows) = (scol, srow);
760 drop(guard);
761 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
762 let g = ch.lock().await;
763 g.notify_clients(&targets, ncols, nrows);
764 }
765 Spawn::encode_response(SpawnResponse {
766 id: sid,
767 cols: scol,
768 rows: srow,
769 })
770 .map_err(boxed_io)
771 }
772 })
773 .await
774 .map_err(|e| format!("register Spawn: {e:?}"))?;
775
776 let st = Arc::clone(&state);
778 endpoint
779 .register_prebuffered(ResizePty::METHOD_ID, move |payload, ctx| {
780 let state = Arc::clone(&st);
781 async move {
782 if state.is_shutting_down.load(Ordering::SeqCst) {
783 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
784 }
785 let (_id, cols, rows) = ResizePty::decode_request(&payload)?;
786 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
787 let Some(channel) = channel else {
788 return Err(rpc_err(RPC_ERROR_UNATTACHED));
789 };
790 let ch = resolve_channel(state.as_ref(), &channel)
791 .await
792 .ok_or_else(|| rpc_err("channel not found"))?;
793 let mut guard = ch.lock().await;
794 if let Some(client) = guard.clients.get_mut(&ctx.conn_id) {
795 client.cols = cols;
796 client.rows = rows;
797 }
798 guard.recalculate_pty_size();
799 let (ncols, nrows) = guard
800 .session
801 .as_ref()
802 .map(|s| (s.cols, s.rows))
803 .unwrap_or((cols, rows));
804 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
805 drop(guard);
806 if let Some(ch) = resolve_channel(state.as_ref(), &channel).await {
807 let g = ch.lock().await;
808 g.notify_clients(&targets, ncols, nrows);
809 }
810 ResizePty::encode_response((ncols, nrows)).map_err(boxed_io)
811 }
812 })
813 .await
814 .map_err(|e| format!("register ResizePty: {e:?}"))?;
815
816 let st = Arc::clone(&state);
818 endpoint
819 .register_prebuffered(CloseSession::METHOD_ID, move |payload, ctx| {
820 let state = Arc::clone(&st);
821 async move {
822 if state.is_shutting_down.load(Ordering::SeqCst) {
823 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
824 }
825 let _id = CloseSession::decode_request(&payload)?;
826 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
827 let Some(channel) = channel else {
828 return Err(rpc_err(RPC_ERROR_UNATTACHED));
829 };
830 let ch = resolve_channel(state.as_ref(), &channel)
831 .await
832 .ok_or_else(|| rpc_err("channel not found"))?;
833 let mut guard = ch.lock().await;
834 guard.request_session_kill(SIGTERM);
835 guard.finalize_subscribers();
836 drop(guard);
837 spawn_kill_escalation(&state, &channel).await;
839 CloseSession::encode_response(()).map_err(boxed_io)
840 }
841 })
842 .await
843 .map_err(|e| format!("register CloseSession: {e:?}"))?;
844
845 let st = Arc::clone(&state);
847 endpoint
848 .register_prebuffered(WriteInput::METHOD_ID, move |payload, ctx| {
849 let state = Arc::clone(&st);
850 async move {
851 if state.is_shutting_down.load(Ordering::SeqCst) {
852 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
853 }
854 let (id, data) = WriteInput::decode_request(&payload)?;
855 let channel = bound_channel(state.as_ref(), ctx.conn_id).await;
856 let Some(channel) = channel else {
857 return Err(rpc_err(RPC_ERROR_UNATTACHED));
858 };
859 let ch = resolve_channel(state.as_ref(), &channel)
860 .await
861 .ok_or_else(|| rpc_err("channel not found"))?;
862 let writer = {
863 let guard = ch.lock().await;
864 guard
865 .session
866 .as_ref()
867 .filter(|s| s.id == id)
868 .map(|s| s.pty.writer_handle())
869 };
870 if let Some(writer) = writer {
874 let _ = tokio::task::spawn_blocking(move || writer.write_bytes(&data)).await;
875 }
876 WriteInput::encode_response(()).map_err(boxed_io)
877 }
878 })
879 .await
880 .map_err(|e| format!("register WriteInput: {e:?}"))?;
881
882 let st = Arc::clone(&state);
884 endpoint
885 .register_stream_handler(STREAM_INPUT_METHOD_ID, move |event, _responder, ctx| {
886 let state = Arc::clone(&st);
887 let conn_id = ctx.conn_id;
888 match event {
889 RpcStreamEvent::PayloadChunk { bytes, .. } => {
890 let forwarder = {
898 let mut fwd = state
899 .input_forwarders
900 .lock()
901 .unwrap_or_else(|e| e.into_inner());
902 if let Some(tx) = fwd.get(&conn_id) {
903 tx.clone()
904 } else {
905 let (tx, rx) = mpsc::channel(INPUT_CHANNEL_CAPACITY);
906 fwd.insert(conn_id, tx.clone());
907 let fwd_state = Arc::clone(&state);
908 tokio::spawn(async move {
909 drain_input_forwarder(fwd_state, conn_id, rx).await;
910 });
911 tx
912 }
913 };
914 if let Err(e) = forwarder.try_send(bytes) {
915 tracing::warn!(error = %e, "gateway input buffer full; dropping input chunk");
916 }
917 }
918 RpcStreamEvent::End { .. } | RpcStreamEvent::Error { .. } => {
919 if let Ok(mut fwd) = state.input_forwarders.lock() {
924 fwd.remove(&conn_id);
925 }
926 }
927 _ => {}
928 }
929 })
930 .await
931 .map_err(|e| format!("register stream handler STREAM_INPUT: {e:?}"))?;
932
933 let st = Arc::clone(&state);
935 endpoint
936 .register_stream_handler(SUBSCRIBE_OUTPUT_METHOD_ID, move |event, respond, ctx| {
937 let is_new = matches!(&event, RpcStreamEvent::Header { .. });
938 if is_new {
939 let st = Arc::clone(&st);
940 let conn_id = ctx.conn_id;
941 tokio::spawn(async move {
942 let channel = bound_channel(&st, conn_id).await;
943 let Some(channel) = channel else {
944 return;
945 };
946 let ch = resolve_channel(&st, &channel).await;
947 let Some(ch) = ch else {
948 return;
949 };
950 let mut guard = ch.lock().await;
951 let early = guard.session.as_mut().and_then(|s| {
954 let data = s.read_output();
955 if data.is_empty() { None } else { Some(data) }
956 });
957 let snapshot = guard.session.as_mut().map(|s| s.generate_snapshot());
958 guard.subscribers.push(SubscriberEntry {
959 conn_id,
960 respond: respond.clone(),
961 });
962 guard.notify.notify_one();
963 let is_dead = guard.session.is_none();
964 drop(guard);
965 if let Some(data) = snapshot
966 && !data.is_empty()
967 {
968 respond.respond(data, false);
969 }
970 if let Some(data) = early {
971 respond.respond(data, false);
972 }
973 if is_dead {
974 respond.respond(Vec::new(), true);
975 }
976 });
977 }
978 })
979 .await
980 .map_err(|e| format!("register SubscribeOutput: {e:?}"))?;
981
982 let st = Arc::clone(&state);
984 let list_socket = socket_name.clone();
985 endpoint
986 .register_prebuffered(ListChannels::METHOD_ID, move |_payload, _ctx| {
987 let state = Arc::clone(&st);
988 let socket = list_socket.clone();
989 async move {
990 let channels = {
991 let chans = state.channels.read().await;
992 chans
993 .iter()
994 .map(|(k, v)| (k.clone(), v.clone()))
995 .collect::<Vec<_>>()
996 };
997 let mut out: Vec<(u64, ChannelInfo)> = Vec::with_capacity(channels.len());
1001 for (name, ch) in channels {
1002 let guard = ch.lock().await;
1003 out.push((guard.created_seq, guard.to_info(&name)));
1004 }
1005 out.sort_by_key(|(seq, _)| *seq);
1006 let out: Vec<ChannelInfo> = out.into_iter().map(|(_, info)| info).collect();
1007 ListChannels::encode_response(ListChannelsResponse {
1008 gateway_pid: std::process::id() as u64,
1009 socket,
1010 channels: out,
1011 })
1012 .map_err(boxed_io)
1013 }
1014 })
1015 .await
1016 .map_err(|e| format!("register ListChannels: {e:?}"))?;
1017
1018 let st = Arc::clone(&state);
1020 endpoint
1021 .register_prebuffered(KillChannel::METHOD_ID, move |payload, _ctx| {
1022 let state = Arc::clone(&st);
1023 async move {
1024 if state.is_shutting_down.load(Ordering::SeqCst) {
1025 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
1026 }
1027 let channel_str = KillChannel::decode_request(&payload)?;
1028 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
1029 let target_conns: Vec<usize> = {
1031 let conns = state.conns.read().await;
1032 conns
1033 .iter()
1034 .filter(|(_, entry)| matches!(entry.state, ConnState::Attached(ref n) if n == &name))
1035 .map(|(conn_id, _)| *conn_id)
1036 .collect()
1037 };
1038 if let Some(ch) = resolve_channel(state.as_ref(), &name).await {
1040 let mut guard = ch.lock().await;
1041 guard.request_session_kill(SIGTERM);
1042 guard.finalize_subscribers();
1043 for conn_id in &target_conns {
1044 guard.clients.remove(conn_id);
1045 guard.subscribers.retain(|s| s.conn_id != *conn_id);
1046 }
1047 drop(guard);
1048 spawn_kill_escalation(&state, &name).await;
1049 }
1050 let mut conns = state.conns.write().await;
1052 for conn_id in &target_conns {
1053 conns.remove(conn_id);
1054 }
1055 KillChannel::encode_response(()).map_err(boxed_io)
1056 }
1057 })
1058 .await
1059 .map_err(|e| format!("register KillChannel: {e:?}"))?;
1060
1061 let st = Arc::clone(&state);
1063 endpoint
1064 .register_prebuffered(KillClient::METHOD_ID, move |payload, _ctx| {
1065 let state = Arc::clone(&st);
1066 async move {
1067 if state.is_shutting_down.load(Ordering::SeqCst) {
1068 return Err(rpc_err(RPC_ERROR_SHUTTING_DOWN));
1069 }
1070 let (channel_str, conn_id) = KillClient::decode_request(&payload)?;
1071 let name = ChannelName::parse(&channel_str).map_err(|e| rpc_err(&e))?;
1072 let bound_channel = {
1075 let conns = state.conns.read().await;
1076 conns.get(&conn_id).and_then(|c| match &c.state {
1077 ConnState::Attached(n) if n == &name => Some(n.clone()),
1078 _ => None,
1079 })
1080 };
1081 let Some(bound) = bound_channel else {
1082 return Err(rpc_err(&format!(
1083 "client {conn_id} is not attached to channel '{name}'"
1084 )));
1085 };
1086 {
1088 let mut conns = state.conns.write().await;
1089 conns.remove(&conn_id);
1090 }
1091 if let Some(ch) = resolve_channel(state.as_ref(), &bound).await {
1092 let mut guard = ch.lock().await;
1093 let mut evicted: Vec<StreamResponder> = Vec::new();
1095 let mut keep = Vec::with_capacity(guard.subscribers.len());
1096 for sub in guard.subscribers.drain(..) {
1097 if sub.conn_id == conn_id {
1098 evicted.push(sub.respond);
1099 } else {
1100 keep.push(sub);
1101 }
1102 }
1103 guard.subscribers = keep;
1104 for respond in evicted {
1105 respond.respond(Vec::new(), true);
1106 }
1107 guard.clients.remove(&conn_id);
1108 guard.recalculate_pty_size();
1109 let session_size = guard.session.as_ref().map(|s| (s.cols, s.rows));
1110 let targets: Vec<ClientEntry> = guard.clients.values().cloned().collect();
1111 drop(guard);
1112 if let (Some((ncols, nrows)), Some(ch)) =
1113 (session_size, resolve_channel(state.as_ref(), &bound).await)
1114 {
1115 let g = ch.lock().await;
1116 g.notify_clients(&targets, ncols, nrows);
1117 }
1118 }
1119 KillClient::encode_response(()).map_err(boxed_io)
1120 }
1121 })
1122 .await
1123 .map_err(|e| format!("register KillClient: {e:?}"))?;
1124
1125 let st = Arc::clone(&state);
1127 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
1128 let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
1129 endpoint
1130 .register_prebuffered(ShutdownGateway::METHOD_ID, move |payload, _ctx| {
1131 let state = Arc::clone(&st);
1132 let shutdown_tx = Arc::clone(&shutdown_tx);
1133 async move {
1134 let force = ShutdownGateway::decode_request(&payload).map_err(boxed_io)?;
1139 if !force {
1140 let live = {
1141 let chans = state.channels.read().await;
1142 let mut n = 0usize;
1143 for ch in chans.values() {
1144 let guard = ch.lock().await;
1145 if guard.session.as_ref().is_some_and(|s| !s.exited) {
1146 n += 1;
1147 }
1148 }
1149 n
1150 };
1151 if live > 0 {
1152 return Err(rpc_err(&format!(
1153 "{RPC_ERROR_LIVE_SESSIONS} ({live} live session(s))"
1154 )));
1155 }
1156 }
1157 state.is_shutting_down.store(true, Ordering::SeqCst);
1159 let channels: Vec<(ChannelName, Arc<Mutex<ChannelState>>)> = {
1161 let chans = state.channels.read().await;
1162 chans.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1163 };
1164 let mut escalations = Vec::new();
1168 for (name, ch) in channels {
1169 let mut guard = ch.lock().await;
1170 tracing::info!(channel = %name, "Shutdown: signaling session tree");
1171 guard.request_session_kill(SIGTERM);
1172 guard.finalize_subscribers();
1173 drop(guard);
1174 escalations.push(spawn_kill_escalation(&state, &name).await);
1175 }
1176 tokio::spawn(async move {
1182 for handle in escalations {
1183 let _ = handle.await;
1184 }
1185 tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_FLUSH_GRACE_MS))
1186 .await;
1187 let mut tx_guard = shutdown_tx.lock().await;
1188 if let Some(tx) = tx_guard.take() {
1189 let _ = tx.send(());
1190 }
1191 });
1192 ShutdownGateway::encode_response(()).map_err(boxed_io)
1193 }
1194 })
1195 .await
1196 .map_err(|e| format!("register ShutdownGateway: {e:?}"))?;
1197
1198 let st = Arc::clone(&state);
1200 tokio::spawn(async move {
1201 while let Some(event) = event_rx.recv().await {
1202 match event {
1203 RpcIpcServerEvent::ClientConnected(handle) => {
1204 tracing::info!("Client {} connected", handle.0.conn_id);
1205 let mut conns = st.conns.write().await;
1206 conns.entry(handle.0.conn_id).or_insert_with(|| ConnEntry {
1212 handle: handle.clone(),
1213 state: ConnState::Unattached,
1214 hostname: String::new(),
1215 connected_at_unix: now_unix(),
1216 pid: 0,
1217 user: String::new(),
1218 version: String::new(),
1219 ssh_ip: None,
1220 });
1221 }
1222 RpcIpcServerEvent::ClientDisconnected(conn_id) => {
1223 tracing::info!("Client {conn_id} disconnected");
1224 evict_conn(st.as_ref(), conn_id).await;
1225 }
1226 }
1227 }
1228 });
1229
1230 tracing::info!("Gateway listening on channel {gateway}");
1231
1232 let exit_code = tokio::select! {
1234 result = async {
1235 server.serve(&socket_name).await.map_err(|e| format!("serve: {e:?}"))
1236 } => {
1237 result?;
1238 0
1239 }
1240 _ = &mut shutdown_rx => {
1241 tokio::time::sleep(SESSION_EXIT_FLUSH_GRACE).await;
1243 0
1244 }
1245 };
1246
1247 Ok(exit_code)
1248}