diff --git a/russh/Cargo.toml b/russh/Cargo.toml index 8b58ad83..429fad0a 100644 --- a/russh/Cargo.toml +++ b/russh/Cargo.toml @@ -74,7 +74,7 @@ rand = "0.8.5" shell-escape = "0.1" tokio-fd = "0.3" termion = "2" -ratatui = "0.26.0" +ratatui = "0.29.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] russh-sftp = "2.0.5" diff --git a/russh/examples/ratatui_app.rs b/russh/examples/ratatui_app.rs index 13454b26..e63fe1cb 100644 --- a/russh/examples/ratatui_app.rs +++ b/russh/examples/ratatui_app.rs @@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::Terminal; +use ratatui::{Terminal, TerminalOptions, Viewport}; use russh::keys::ssh_key::PublicKey; use russh::server::*; -use russh::{Channel, ChannelId}; +use russh::{Channel, ChannelId, Pty}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -25,12 +26,28 @@ impl App { } } -#[derive(Clone)] struct TerminalHandle { - handle: Handle, - // The sink collects the data which is finally flushed to the handle. + sender: UnboundedSender>, + // The sink collects the data which is finally sent to sender. sink: Vec, - channel_id: ChannelId, +} + +impl TerminalHandle { + async fn start(handle: Handle, channel_id: ChannelId) -> Self { + let (sender, mut receiver) = unbounded_channel::>(); + tokio::spawn(async move { + while let Some(data) = receiver.recv().await { + let result = handle.data(channel_id, data.into()).await; + if result.is_err() { + eprintln!("Failed to send data: {:?}", result); + } + } + }); + Self { + sender, + sink: Vec::new(), + } + } } // The crossterm backend writes to the terminal handle. @@ -41,15 +58,13 @@ impl std::io::Write for TerminalHandle { } fn flush(&mut self) -> std::io::Result<()> { - let handle = self.handle.clone(); - let channel_id = self.channel_id; - let data = self.sink.clone().into(); - futures::executor::block_on(async move { - let result = handle.data(channel_id, data).await; - if result.is_err() { - eprintln!("Failed to send data: {:?}", result); - } - }); + let result = self.sender.send(self.sink.clone()); + if result.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + result.unwrap_err(), + )); + } self.sink.clear(); Ok(()) @@ -81,8 +96,8 @@ impl AppServer { terminal .draw(|f| { - let size = f.size(); - f.render_widget(Clear, size); + let area = f.area(); + f.render_widget(Clear, area); let style = match app.counter % 3 { 0 => Style::default().fg(Color::Red), 1 => Style::default().fg(Color::Green), @@ -94,7 +109,7 @@ impl AppServer { let block = Block::default() .title("Press 'c' to reset the counter!") .borders(Borders::ALL); - f.render_widget(paragraph.block(block), size); + f.render_widget(paragraph.block(block), area); }) .unwrap(); } @@ -135,20 +150,20 @@ impl Handler for AppServer { channel: Channel, session: &mut Session, ) -> Result { - { - let mut clients = self.clients.lock().await; - let terminal_handle = TerminalHandle { - handle: session.handle(), - sink: Vec::new(), - channel_id: channel.id(), - }; - - let backend = CrosstermBackend::new(terminal_handle.clone()); - let terminal = Terminal::new(backend)?; - let app = App::new(); - - clients.insert(self.id, (terminal, app)); - } + let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await; + + let backend = CrosstermBackend::new(terminal_handle); + + // the correct viewport area will be set when the client request a pty + let options = TerminalOptions { + viewport: Viewport::Fixed(Rect::default()), + }; + + let terminal = Terminal::with_options(backend, options)?; + let app = App::new(); + + let mut clients = self.clients.lock().await; + clients.insert(self.id, (terminal, app)); Ok(true) } @@ -192,17 +207,48 @@ impl Handler for AppServer { _: u32, _: &mut Session, ) -> Result<(), Self::Error> { - { - let mut clients = self.clients.lock().await; - let (terminal, _) = clients.get_mut(&self.id).unwrap(); - let rect = Rect { - x: 0, - y: 0, - width: col_width as u16, - height: row_height as u16, - }; - terminal.resize(rect)?; - } + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, + }; + + let mut clients = self.clients.lock().await; + let (terminal, _) = clients.get_mut(&self.id).unwrap(); + terminal.resize(rect)?; + + Ok(()) + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + /// + /// **Note:** Success or failure should be communicated to the client by calling + /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. + async fn pty_request( + &mut self, + channel: ChannelId, + _: &str, + col_width: u32, + row_height: u32, + _: u32, + _: u32, + _: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, + }; + + let mut clients = self.clients.lock().await; + let (terminal, _) = clients.get_mut(&self.id).unwrap(); + terminal.resize(rect)?; + + session.channel_success(channel)?; Ok(()) } diff --git a/russh/examples/ratatui_shared_app.rs b/russh/examples/ratatui_shared_app.rs index e09e745f..e023d862 100644 --- a/russh/examples/ratatui_shared_app.rs +++ b/russh/examples/ratatui_shared_app.rs @@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::Terminal; +use ratatui::{Terminal, TerminalOptions, Viewport}; use russh::keys::ssh_key::PublicKey; use russh::server::*; -use russh::{Channel, ChannelId}; +use russh::{Channel, ChannelId, Pty}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -25,12 +26,28 @@ impl App { } } -#[derive(Clone)] struct TerminalHandle { - handle: Handle, - // The sink collects the data which is finally flushed to the handle. + sender: UnboundedSender>, + // The sink collects the data which is finally sent to sender. sink: Vec, - channel_id: ChannelId, +} + +impl TerminalHandle { + async fn start(handle: Handle, channel_id: ChannelId) -> Self { + let (sender, mut receiver) = unbounded_channel::>(); + tokio::spawn(async move { + while let Some(data) = receiver.recv().await { + let result = handle.data(channel_id, data.into()).await; + if result.is_err() { + eprintln!("Failed to send data: {:?}", result); + } + } + }); + Self { + sender, + sink: Vec::new(), + } + } } // The crossterm backend writes to the terminal handle. @@ -41,15 +58,13 @@ impl std::io::Write for TerminalHandle { } fn flush(&mut self) -> std::io::Result<()> { - let handle = self.handle.clone(); - let channel_id = self.channel_id; - let data = self.sink.clone().into(); - futures::executor::block_on(async move { - let result = handle.data(channel_id, data).await; - if result.is_err() { - eprintln!("Failed to send data: {:?}", result); - } - }); + let result = self.sender.send(self.sink.clone()); + if result.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + result.unwrap_err(), + )); + } self.sink.clear(); Ok(()) @@ -83,8 +98,8 @@ impl AppServer { for (_, terminal) in clients.lock().await.iter_mut() { terminal .draw(|f| { - let size = f.size(); - f.render_widget(Clear, size); + let area = f.area(); + f.render_widget(Clear, area); let style = match counter % 3 { 0 => Style::default().fg(Color::Red), 1 => Style::default().fg(Color::Green), @@ -96,7 +111,7 @@ impl AppServer { let block = Block::default() .title("Press 'c' to reset the counter!") .borders(Borders::ALL); - f.render_widget(paragraph.block(block), size); + f.render_widget(paragraph.block(block), area); }) .unwrap(); } @@ -137,18 +152,19 @@ impl Handler for AppServer { channel: Channel, session: &mut Session, ) -> Result { - { - let mut clients = self.clients.lock().await; - let terminal_handle = TerminalHandle { - handle: session.handle(), - sink: Vec::new(), - channel_id: channel.id(), - }; - - let backend = CrosstermBackend::new(terminal_handle.clone()); - let terminal = Terminal::new(backend)?; - clients.insert(self.id, terminal); - } + let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await; + + let backend = CrosstermBackend::new(terminal_handle); + + // the correct viewport area will be set when the client request a pty + let options = TerminalOptions { + viewport: Viewport::Fixed(Rect::default()), + }; + + let terminal = Terminal::with_options(backend, options)?; + + let mut clients = self.clients.lock().await; + clients.insert(self.id, terminal); Ok(true) } @@ -191,18 +207,48 @@ impl Handler for AppServer { _: u32, _: &mut Session, ) -> Result<(), Self::Error> { - let mut terminal = { - let clients = self.clients.lock().await; - clients.get(&self.id).unwrap().clone() + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, }; + + let mut clients = self.clients.lock().await; + clients.get_mut(&self.id).unwrap().resize(rect)?; + + Ok(()) + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + /// + /// **Note:** Success or failure should be communicated to the client by calling + /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. + async fn pty_request( + &mut self, + channel: ChannelId, + _: &str, + col_width: u32, + row_height: u32, + _: u32, + _: u32, + _: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { let rect = Rect { x: 0, y: 0, width: col_width as u16, height: row_height as u16, }; + + let mut clients = self.clients.lock().await; + let terminal = clients.get_mut(&self.id).unwrap(); terminal.resize(rect)?; + session.channel_success(channel)?; + Ok(()) } }