From 6795ef4e1bce9613296b1cda1de8523e99a51490 Mon Sep 17 00:00:00 2001 From: Alessandro Ricottone Date: Sun, 11 Feb 2024 14:11:27 +0100 Subject: [PATCH 1/2] update toolchain and add ratatui to dev dependencies --- russh/Cargo.toml | 1 + rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/russh/Cargo.toml b/russh/Cargo.toml index fdbdaea9..f8351d6a 100644 --- a/russh/Cargo.toml +++ b/russh/Cargo.toml @@ -73,6 +73,7 @@ rand = "0.8.5" shell-escape = "0.1" tokio-fd = "0.3" termion = "2" +ratatui = "0.26.0" [package.metadata.docs.rs] features = ["openssl"] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 22048ac5..624eb0ea 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.70.0" +channel = "1.76.0" From dcbe4bad5d86cbf54e2da459df3636d80a123e4e Mon Sep 17 00:00:00 2001 From: Alessandro Ricottone Date: Sun, 11 Feb 2024 14:11:41 +0100 Subject: [PATCH 2/2] update examples to new APIs --- russh/examples/ratatui_app.rs | 212 +++++++++++++++++++++++++++ russh/examples/ratatui_shared_app.rs | 210 ++++++++++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 russh/examples/ratatui_app.rs create mode 100644 russh/examples/ratatui_shared_app.rs diff --git a/russh/examples/ratatui_app.rs b/russh/examples/ratatui_app.rs new file mode 100644 index 00000000..c4068d20 --- /dev/null +++ b/russh/examples/ratatui_app.rs @@ -0,0 +1,212 @@ +use async_trait::async_trait; +use ratatui::{ + backend::CrosstermBackend, + layout::Rect, + style::{Color, Style}, + widgets::{Block, Borders, Clear, Paragraph}, + Terminal, +}; +use russh::{server::*, Channel, ChannelId}; +use russh_keys::key::PublicKey; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +type SshTerminal = Terminal>; + +struct App { + pub counter: usize, +} + +impl App { + pub fn new() -> App { + Self { counter: 0 } + } +} + +#[derive(Clone)] +struct TerminalHandle { + handle: Handle, + // The sink collects the data which is finally flushed to the handle. + sink: Vec, + channel_id: ChannelId, +} + +// The crossterm backend writes to the terminal handle. +impl std::io::Write for TerminalHandle { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.sink.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + let handle = self.handle.clone(); + let channel_id = self.channel_id.clone(); + 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); + } + }); + + self.sink.clear(); + Ok(()) + } +} + +#[derive(Clone)] +struct AppServer { + clients: Arc>>, + id: usize, +} + +impl AppServer { + pub fn new() -> Self { + Self { + clients: Arc::new(Mutex::new(HashMap::new())), + id: 0, + } + } + + pub async fn run(&mut self) -> Result<(), anyhow::Error> { + let clients = self.clients.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + for (_, (terminal, app)) in clients.lock().await.iter_mut() { + app.counter += 1; + + terminal + .draw(|f| { + let size = f.size(); + f.render_widget(Clear, size); + let style = match app.counter % 3 { + 0 => Style::default().fg(Color::Red), + 1 => Style::default().fg(Color::Green), + _ => Style::default().fg(Color::Blue), + }; + let paragraph = Paragraph::new(format!("Counter: {}", app.counter)) + .alignment(ratatui::layout::Alignment::Center) + .style(style); + let block = Block::default() + .title("Press 'c' to reset the counter!") + .borders(Borders::ALL); + f.render_widget(paragraph.block(block), size); + }) + .unwrap(); + } + } + }); + + let config = Config { + inactivity_timeout: Some(std::time::Duration::from_secs(3600)), + auth_rejection_time: std::time::Duration::from_secs(3), + auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), + keys: vec![russh_keys::key::KeyPair::generate_ed25519().unwrap()], + ..Default::default() + }; + + self.run_on_address(Arc::new(config), ("0.0.0.0", 2222)) + .await?; + Ok(()) + } +} + +impl Server for AppServer { + type Handler = Self; + fn new_client(&mut self, _: Option) -> Self { + let s = self.clone(); + self.id += 1; + s + } +} + +#[async_trait] +impl Handler for AppServer { + type Error = anyhow::Error; + + async fn channel_open_session( + &mut self, + 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)); + } + + Ok(true) + } + + async fn auth_publickey(&mut self, _: &str, _: &PublicKey) -> Result { + Ok(Auth::Accept) + } + + async fn data( + &mut self, + channel: ChannelId, + data: &[u8], + session: &mut Session, + ) -> Result<(), Self::Error> { + match data { + // Pressing 'q' closes the connection. + b"q" => { + self.clients.lock().await.remove(&self.id); + session.close(channel); + } + // Pressing 'c' resets the counter for the app. + // Only the client with the id sees the counter reset. + b"c" => { + let mut clients = self.clients.lock().await; + let (_, app) = clients.get_mut(&self.id).unwrap(); + app.counter = 0; + } + _ => {} + } + + Ok(()) + } + + /// The client's window size has changed. + async fn window_change_request( + &mut self, + _: ChannelId, + col_width: u32, + row_height: u32, + _: u32, + _: 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)?; + } + + Ok(()) + } +} + +#[tokio::main] +async fn main() { + let mut server = AppServer::new(); + server.run().await.expect("Failed running server"); +} diff --git a/russh/examples/ratatui_shared_app.rs b/russh/examples/ratatui_shared_app.rs new file mode 100644 index 00000000..73786d7e --- /dev/null +++ b/russh/examples/ratatui_shared_app.rs @@ -0,0 +1,210 @@ +use async_trait::async_trait; +use ratatui::{ + backend::CrosstermBackend, + layout::Rect, + style::{Color, Style}, + widgets::{Block, Borders, Clear, Paragraph}, + Terminal, +}; +use russh::{server::*, Channel, ChannelId}; +use russh_keys::key::PublicKey; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +type SshTerminal = Terminal>; + +struct App { + pub counter: usize, +} + +impl App { + pub fn new() -> App { + Self { counter: 0 } + } +} + +#[derive(Clone)] +struct TerminalHandle { + handle: Handle, + // The sink collects the data which is finally flushed to the handle. + sink: Vec, + channel_id: ChannelId, +} + +// The crossterm backend writes to the terminal handle. +impl std::io::Write for TerminalHandle { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.sink.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + let handle = self.handle.clone(); + let channel_id = self.channel_id.clone(); + 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); + } + }); + + self.sink.clear(); + Ok(()) + } +} + +#[derive(Clone)] +struct AppServer { + clients: Arc>>, + id: usize, + app: Arc>, +} + +impl AppServer { + pub fn new() -> Self { + Self { + clients: Arc::new(Mutex::new(HashMap::new())), + id: 0, + app: Arc::new(Mutex::new(App::new())), + } + } + + pub async fn run(&mut self) -> Result<(), anyhow::Error> { + let app = self.app.clone(); + let clients = self.clients.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + app.lock().await.counter += 1; + let counter = app.lock().await.counter; + for (_, terminal) in clients.lock().await.iter_mut() { + terminal + .draw(|f| { + let size = f.size(); + f.render_widget(Clear, size); + let style = match counter % 3 { + 0 => Style::default().fg(Color::Red), + 1 => Style::default().fg(Color::Green), + _ => Style::default().fg(Color::Blue), + }; + let paragraph = Paragraph::new(format!("Counter: {counter}")) + .alignment(ratatui::layout::Alignment::Center) + .style(style); + let block = Block::default() + .title("Press 'c' to reset the counter!") + .borders(Borders::ALL); + f.render_widget(paragraph.block(block), size); + }) + .unwrap(); + } + } + }); + + let config = Config { + inactivity_timeout: Some(std::time::Duration::from_secs(3600)), + auth_rejection_time: std::time::Duration::from_secs(3), + auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), + keys: vec![russh_keys::key::KeyPair::generate_ed25519().unwrap()], + ..Default::default() + }; + + self.run_on_address(Arc::new(config), ("0.0.0.0", 2222)) + .await?; + Ok(()) + } +} + +impl Server for AppServer { + type Handler = Self; + fn new_client(&mut self, _: Option) -> Self { + let s = self.clone(); + self.id += 1; + s + } +} + +#[async_trait] +impl Handler for AppServer { + type Error = anyhow::Error; + + async fn channel_open_session( + &mut self, + 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); + } + + Ok(true) + } + + async fn auth_publickey(&mut self, _: &str, _: &PublicKey) -> Result { + Ok(Auth::Accept) + } + + async fn data( + &mut self, + channel: ChannelId, + data: &[u8], + session: &mut Session, + ) -> Result<(), Self::Error> { + let app = self.app.clone(); + match data { + // Pressing 'q' closes the connection. + b"q" => { + self.clients.lock().await.remove(&self.id); + session.close(channel); + } + // Pressing 'c' resets the counter for the app. + // Every client sees the counter reset. + b"c" => { + app.lock().await.counter = 0; + } + _ => {} + } + + Ok(()) + } + + /// The client's pseudo-terminal window size has changed. + async fn window_change_request( + &mut self, + _: ChannelId, + col_width: u32, + row_height: u32, + _: u32, + _: 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, + }; + terminal.resize(rect)?; + + Ok(()) + } +} + +#[tokio::main] +async fn main() { + let mut server = AppServer::new(); + server.run().await.expect("Failed running server"); +}