1use std::io::{self, Write};
12use std::sync::mpsc::Receiver;
13use std::sync::Arc;
14
15use cliban_tenancy::Tenant;
16use cliban_tui::app::App;
17use cliban_tui::data::Data;
18use cliban_tui::remote::{ChannelSession, RemoteBackend, RemoteInput};
19use cliban_tui::{picker, runtime};
20use ratatui::Terminal;
21use russh::server::Handle;
22use russh::ChannelId;
23use tokio::sync::broadcast;
24
25use crate::server::AppState;
26
27pub const NO_TTY: &str = "cliband: the board requires a TTY; use ssh -t\r\n";
29
30struct ChannelWriter {
38 rt: tokio::runtime::Handle,
39 handle: Handle,
40 channel: ChannelId,
41 buf: Vec<u8>,
42}
43
44impl Write for ChannelWriter {
45 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
46 self.buf.extend_from_slice(data);
47 Ok(data.len())
48 }
49
50 fn flush(&mut self) -> io::Result<()> {
51 if self.buf.is_empty() {
52 return Ok(());
53 }
54 let data = std::mem::take(&mut self.buf);
55 self.rt
56 .block_on(self.handle.data(self.channel, data))
57 .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "ssh channel closed"))
58 }
59}
60
61pub struct BoardTask {
64 pub rt: tokio::runtime::Handle,
65 pub state: Arc<AppState>,
66 pub handle: Handle,
67 pub channel: ChannelId,
68 pub size: (u16, u16),
70 pub tenants: Vec<Tenant>,
72 pub input: Receiver<RemoteInput>,
73}
74
75pub fn run_board(task: BoardTask) {
79 let rt = task.rt.clone();
80 let handle = task.handle.clone();
81 let channel = task.channel;
82 let exit = match board_session(task) {
83 Ok(()) => 0,
84 Err(e) => {
85 let disconnect = e.downcast_ref::<io::Error>().is_some_and(|io| {
88 matches!(
89 io.kind(),
90 io::ErrorKind::UnexpectedEof | io::ErrorKind::BrokenPipe
91 )
92 });
93 if !disconnect {
94 eprintln!("cliband: board session ended: {e}");
95 }
96 1
97 }
98 };
99 rt.block_on(async {
100 let _ = handle.exit_status_request(channel, exit).await;
101 let _ = handle.eof(channel).await;
102 let _ = handle.close(channel).await;
103 });
104}
105
106fn board_session(task: BoardTask) -> Result<(), Box<dyn std::error::Error>> {
107 let writer = ChannelWriter {
108 rt: task.rt,
109 handle: task.handle,
110 channel: task.channel,
111 buf: Vec::new(),
112 };
113 let (backend, size) = RemoteBackend::new(writer, task.size.0, task.size.1)?;
114 let mut terminal = Terminal::new(backend)?;
115 let mut session = ChannelSession::new(task.input, size);
116
117 let res = (|| -> Result<(), Box<dyn std::error::Error>> {
118 let picked = if task.tenants.len() == 1 {
119 Some(0)
120 } else {
121 let slugs: Vec<String> = task.tenants.iter().map(|t| t.slug.clone()).collect();
122 picker::pick(&mut terminal, &mut session, " pick a board ", &slugs)?
123 };
124 let Some(i) = picked else { return Ok(()) };
125 let tenant_handle = task.state.manager.handle(&task.tenants[i].id)?;
130
131 let mut feed = tenant_handle.changes.subscribe();
137 session.set_dirty_check(move || {
138 let mut dirty = false;
139 loop {
140 match feed.try_recv() {
141 Ok(()) => dirty = true,
142 Err(broadcast::error::TryRecvError::Lagged(_)) => dirty = true,
144 Err(_) => break, }
146 }
147 dirty
148 });
149 let publish = tenant_handle.changes.clone();
150 let mut data = Data::from_store(tenant_handle.store.clone())?;
151 data.set_on_mutate(move || {
152 let _ = publish.send(());
154 });
155 let mut app = App::new();
156 runtime::reload(&data, &mut app)?;
157 runtime::event_loop(&mut terminal, &mut session, &data, &mut app)
158 })();
159
160 let _ = terminal.backend_mut().leave_screen();
163 res
164}