diff --git a/Cargo.lock b/Cargo.lock index 591689251..59c1bc13e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +dependencies = [ + "cfg-if", + "crossbeam-channel 0.4.0", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils 0.7.0", +] + [[package]] name = "crossbeam-channel" version = "0.3.9" @@ -3401,7 +3415,7 @@ dependencies = [ "core-foundation 0.7.0", "core-graphics 0.19.0", "core-text 15.0.0", - "crossbeam-channel 0.3.9", + "crossbeam", "daemonize", "dirs 1.0.5", "downcast-rs", diff --git a/Cargo.toml b/Cargo.toml index 5e77b9f03..0645b4246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ base64 = "0.10" base91 = { path = "base91" } rangeset = { path = "rangeset" } bitflags = "1.0" -crossbeam-channel = "0.3" +crossbeam = "0.7" dirs = "1.0" downcast-rs = "1.0" euclid = "0.20" diff --git a/src/frontend/muxserver/mod.rs b/src/frontend/muxserver/mod.rs index 22ef8e508..9de5a4d86 100644 --- a/src/frontend/muxserver/mod.rs +++ b/src/frontend/muxserver/mod.rs @@ -6,7 +6,7 @@ use crate::mux::window::WindowId; use crate::mux::Mux; use crate::server::listener::spawn_listener; use anyhow::{bail, Error}; -use crossbeam_channel::{unbounded as channel, Receiver}; +use crossbeam::channel::{unbounded as channel, Receiver}; use log::info; use promise::*; use std::rc::Rc; diff --git a/src/server/client.rs b/src/server/client.rs index 7c83a038a..d6ec6b388 100644 --- a/src/server/client.rs +++ b/src/server/client.rs @@ -9,7 +9,7 @@ use crate::server::tab::ClientTab; use crate::server::UnixStream; use crate::ssh::ssh_connect; use anyhow::{anyhow, bail, Context, Error}; -use crossbeam_channel::TryRecvError; +use crossbeam::channel::TryRecvError; use filedescriptor::{pollfd, AsRawSocketDescriptor}; use log::info; use portable_pty::{CommandBuilder, NativePtySystem, PtySystem}; diff --git a/src/server/listener/clientsession.rs b/src/server/listener/clientsession.rs index c6ee5c2c8..5f88597ec 100644 --- a/src/server/listener/clientsession.rs +++ b/src/server/listener/clientsession.rs @@ -4,7 +4,7 @@ use crate::mux::{Mux, MuxNotification, MuxSubscriber}; use crate::server::codec::*; use crate::server::pollable::*; use anyhow::{anyhow, bail, Context, Error}; -use crossbeam_channel::TryRecvError; +use crossbeam::channel::TryRecvError; use log::error; use portable_pty::PtySize; use promise::spawn::spawn_into_main_thread; diff --git a/src/server/pollable.rs b/src/server/pollable.rs index 59e1ab121..56d7daf35 100644 --- a/src/server/pollable.rs +++ b/src/server/pollable.rs @@ -1,6 +1,6 @@ use crate::server::UnixStream; use anyhow::Error; -use crossbeam_channel::{unbounded as channel, Receiver, Sender, TryRecvError}; +use crossbeam::channel::{unbounded as channel, Receiver, Sender, TryRecvError}; use filedescriptor::*; use std::cell::RefCell; use std::io::{Read, Write}; diff --git a/src/ssh.rs b/src/ssh.rs index ef294bd11..29338040c 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -6,6 +6,7 @@ use crate::mux::Mux; use crate::termwiztermtab; use anyhow::{anyhow, bail, Context, Error}; use async_trait::async_trait; +use crossbeam::channel::{bounded, Receiver, Sender}; use portable_pty::cmdbuilder::CommandBuilder; use portable_pty::{PtySize, PtySystem}; use promise::{Future, Promise}; @@ -14,96 +15,99 @@ use std::io::Write; use std::net::TcpStream; use std::path::Path; use std::rc::Rc; -use termwiz::cell::{unicode_column_width, AttributeChange, Intensity}; +use std::time::Duration; +use termwiz::cell::unicode_column_width; use termwiz::lineedit::*; use termwiz::surface::Change; use termwiz::terminal::*; -fn password_prompt( - instructions: &str, - prompt: &str, - username: &str, - remote_address: &str, -) -> Option { - let title = "🔐 wezterm: SSH authentication".to_string(); - let text = format!( - "🔐 SSH Authentication for {} @ {}\n{}\n", - username, remote_address, instructions - ) - .replace("\n", "\r\n"); - let prompt = prompt.to_string(); - - #[derive(Default)] - struct PasswordPromptHost { - history: BasicHistory, +#[derive(Default)] +struct PasswordPromptHost { + history: BasicHistory, +} +impl LineEditorHost for PasswordPromptHost { + fn history(&mut self) -> &mut dyn History { + &mut self.history } - impl LineEditorHost for PasswordPromptHost { - fn history(&mut self) -> &mut dyn History { - &mut self.history - } - // Rewrite the input so that we can obscure the password - // characters when output to the terminal widget - fn highlight_line( - &self, - line: &str, - cursor_position: usize, - ) -> (Vec, usize) { - let placeholder = "🔑"; - let grapheme_count = unicode_column_width(line); - let mut output = vec![]; - for _ in 0..grapheme_count { - output.push(OutputElement::Text(placeholder.to_string())); + // Rewrite the input so that we can obscure the password + // characters when output to the terminal widget + fn highlight_line(&self, line: &str, cursor_position: usize) -> (Vec, usize) { + let placeholder = "🔑"; + let grapheme_count = unicode_column_width(line); + let mut output = vec![]; + for _ in 0..grapheme_count { + output.push(OutputElement::Text(placeholder.to_string())); + } + (output, unicode_column_width(placeholder) * cursor_position) + } +} + +enum UIRequest { + /// Display something + Output(Vec), + /// Request input + Input { + prompt: String, + echo: bool, + respond: Promise, + }, + Close, +} + +struct SshUIImpl { + term: termwiztermtab::TermWizTerminal, + rx: Receiver, +} + +impl SshUIImpl { + fn run(&mut self) -> anyhow::Result<()> { + let title = "🔐 wezterm: SSH authentication".to_string(); + self.term.render(&[Change::Title(title)])?; + + loop { + match self.rx.recv_timeout(Duration::from_millis(200)) { + Ok(UIRequest::Close) => break, + Ok(UIRequest::Output(changes)) => self.term.render(&changes)?, + Ok(UIRequest::Input { + prompt, + echo: true, + mut respond, + }) => { + respond.result(self.input_prompt(&prompt)); + } + Ok(UIRequest::Input { + prompt, + echo: false, + mut respond, + }) => { + respond.result(self.password_prompt(&prompt)); + } + Err(err) if err.is_timeout() => {} + Err(err) => bail!("recv_timeout: {}", err), } - (output, unicode_column_width(placeholder) * cursor_position) } - } - match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| { - term.render(&[ - // Change::Attribute(AttributeChange::Intensity(Intensity::Bold)), - Change::Title(title.to_string()), - Change::Text(text.to_string()), - Change::Attribute(AttributeChange::Intensity(Intensity::Normal)), - ])?; - let mut editor = LineEditor::new(term); - editor.set_prompt(&format!("{}: ", prompt)); + std::thread::sleep(Duration::new(2, 0)); + + Ok(()) + } + + fn password_prompt(&mut self, prompt: &str) -> anyhow::Result { + let mut editor = LineEditor::new(&mut self.term); + editor.set_prompt(prompt); let mut host = PasswordPromptHost::default(); if let Some(line) = editor.read_line(&mut host)? { Ok(line) } else { - bail!("prompt cancelled"); - } - })) { - Ok(p) => Some(p), - Err(p) => { - log::error!("failed to prompt for pw: {}", p); - None + bail!("password entry was cancelled"); } } -} -fn input_prompt( - instructions: &str, - prompt: &str, - username: &str, - remote_address: &str, -) -> Option { - let title = "🔐 wezterm: SSH authentication".to_string(); - let text = format!( - "SSH Authentication for {} @ {}\n{}\n{}\n", - username, remote_address, instructions, prompt - ) - .replace("\n", "\r\n"); - match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| { - term.render(&[ - Change::Title(title.to_string()), - Change::Text(text.to_string()), - Change::Attribute(AttributeChange::Intensity(Intensity::Normal)), - ])?; - - let mut editor = LineEditor::new(term); + fn input_prompt(&mut self, prompt: &str) -> anyhow::Result { + let mut editor = LineEditor::new(&mut self.term); + editor.set_prompt(prompt); let mut host = NopLineEditorHost::default(); if let Some(line) = editor.read_line(&mut host)? { @@ -111,21 +115,72 @@ fn input_prompt( } else { bail!("prompt cancelled"); } - })) { - Ok(p) => Some(p), - Err(p) => { - log::error!("failed to prompt for pw: {}", p); - None - } } } -struct Prompt<'a> { - username: &'a str, - remote_address: &'a str, +struct SshUI { + tx: Sender, } -impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> { +impl SshUI { + fn new() -> Self { + let (tx, rx) = bounded(16); + promise::spawn::spawn_into_main_thread(termwiztermtab::run(70, 15, move |term| { + let mut ui = SshUIImpl { term, rx }; + ui.run() + })); + Self { tx } + } + + fn output(&self, changes: Vec) { + self.tx + .send(UIRequest::Output(changes)) + .expect("send to SShUI failed"); + } + + fn output_str(&self, s: &str) { + let s = s.replace("\n", "\r\n"); + self.output(vec![Change::Text(s)]); + } + + fn input(&self, prompt: &str) -> anyhow::Result { + let mut promise = Promise::new(); + let future = promise.get_future().unwrap(); + + self.tx + .send(UIRequest::Input { + prompt: prompt.replace("\n", "\r\n"), + echo: true, + respond: promise, + }) + .expect("send to SshUI failed"); + + future.wait() + } + + fn password(&self, prompt: &str) -> anyhow::Result { + let mut promise = Promise::new(); + let future = promise.get_future().unwrap(); + + self.tx + .send(UIRequest::Input { + prompt: prompt.replace("\n", "\r\n"), + echo: false, + respond: promise, + }) + .expect("send to SshUI failed"); + + future.wait() + } + + fn close(&self) { + self.tx + .send(UIRequest::Close) + .expect("send to SshUI failed"); + } +} + +impl ssh2::KeyboardInteractivePrompt for SshUI { fn prompt<'b>( &mut self, _username: &str, @@ -135,14 +190,13 @@ impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> { prompts .iter() .map(|p| { - let func = if p.echo { - input_prompt + self.output_str(&format!("{}\n", instructions)); + if p.echo { + self.input(&p.text) } else { - password_prompt - }; - - func(instructions, &p.text, &self.username, &self.remote_address) - .unwrap_or_else(String::new) + self.password(&p.text) + } + .unwrap_or_else(|_| String::new()) }) .collect() } @@ -157,7 +211,11 @@ pub fn async_ssh_connect(remote_address: &str, username: &str) -> Future anyhow::Result { +fn ssh_connect_with_ui( + remote_address: &str, + username: &str, + ui: &mut SshUI, +) -> anyhow::Result { let mut sess = ssh2::Session::new()?; let (remote_address, remote_host_name, port) = { @@ -170,8 +228,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result anyhow::Result {} CheckResult::NotFound => { - let message = format!( - "SSH host {} is not yet trusted.\r\n\ - {:?} Fingerprint: {}.\r\n\ - Trust and continue connecting?\r\n", + ui.output_str(&format!( + "SSH host {} is not yet trusted.\n\ + {:?} Fingerprint: {}.\n\ + Trust and continue connecting?\n", remote_address, key_type, fingerprint - ); + )); - let allow = - promise::spawn::block_on(termwiztermtab::run(80, 10, move |mut term| { - let title = "🔐 wezterm: SSH authentication".to_string(); - term.render(&[Change::Title(title), Change::Text(message.to_string())])?; + loop { + let line = ui.input("Enter [Y/n]> ")?; - let mut editor = LineEditor::new(term); - editor.set_prompt("Enter [Y/n]> "); - - let mut host = NopLineEditorHost::default(); - loop { - let line = match editor.read_line(&mut host) { - Ok(Some(line)) => line, - Ok(None) | Err(_) => return Ok(false), - }; - match line.as_ref() { - "y" | "Y" | "yes" | "YES" => return Ok(true), - "n" | "N" | "no" | "NO" => return Ok(false), - _ => continue, - } - } - }))?; - - if !allow { - bail!("user declined to trust host"); + match line.as_ref() { + "y" | "Y" | "yes" | "YES" => break, + "n" | "N" | "no" | "NO" => bail!("user declined to trust host"), + _ => continue, + } } known_hosts @@ -263,11 +308,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result { - termwiztermtab::message_box_ok(&format!( + ui.output_str(&format!( "🛑 host key mismatch for ssh server {}.\n\ Got fingerprint {} instead of expected value from known_hosts\n\ file {}.\n\ - Refusing to connect.", + Refusing to connect.\n", remote_address, fingerprint, file.display() @@ -275,7 +320,7 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result { - termwiztermtab::message_box_ok("🛑 Failed to load and check known ssh hosts"); + ui.output_str("🛑 Failed to load and check known ssh hosts\n"); bail!("failed to check the known hosts"); } } @@ -295,24 +340,24 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result anyhow::Result anyhow::Result { + let mut ui = SshUI::new(); + let res = ssh_connect_with_ui(remote_address, username, &mut ui); + match res { + Ok(sess) => { + ui.close(); + Ok(sess) + } + Err(err) => { + ui.output_str(&format!("\nFailed: {}", err)); + Err(err) + } + } +} + pub struct RemoteSshDomain { pty_system: Box, id: DomainId, diff --git a/src/termwiztermtab.rs b/src/termwiztermtab.rs index 325a8a600..d12fef6da 100644 --- a/src/termwiztermtab.rs +++ b/src/termwiztermtab.rs @@ -13,7 +13,7 @@ use crate::mux::window::WindowId; use crate::mux::Mux; use anyhow::{bail, Error}; use async_trait::async_trait; -use crossbeam_channel::{unbounded as channel, Receiver, Sender}; +use crossbeam::channel::{unbounded as channel, Receiver, Sender}; use filedescriptor::Pipe; use portable_pty::*; use rangeset::RangeSet; @@ -370,6 +370,48 @@ impl termwiz::terminal::Terminal for TermWizTerminal { } } +impl termwiz::terminal::Terminal for &mut TermWizTerminal { + fn set_raw_mode(&mut self) -> anyhow::Result<()> { + (**self).set_raw_mode() + } + + fn set_cooked_mode(&mut self) -> anyhow::Result<()> { + (**self).set_cooked_mode() + } + + fn enter_alternate_screen(&mut self) -> anyhow::Result<()> { + (**self).enter_alternate_screen() + } + + fn exit_alternate_screen(&mut self) -> anyhow::Result<()> { + (**self).exit_alternate_screen() + } + + fn get_screen_size(&mut self) -> anyhow::Result { + (**self).get_screen_size() + } + + fn set_screen_size(&mut self, size: ScreenSize) -> anyhow::Result<()> { + (**self).set_screen_size(size) + } + + fn render(&mut self, changes: &[Change]) -> anyhow::Result<()> { + (**self).render(changes) + } + + fn flush(&mut self) -> anyhow::Result<()> { + (**self).flush() + } + + fn poll_input(&mut self, wait: Option) -> anyhow::Result> { + (**self).poll_input(wait) + } + + fn waker(&self) -> TerminalWaker { + (**self).waker() + } +} + /// This function spawns a thread and constructs a GUI window with an /// associated termwiz Terminal object to execute the provided function. /// The function is expected to run in a loop to manage input and output @@ -378,7 +420,7 @@ impl termwiz::terminal::Terminal for TermWizTerminal { /// the return value from the function. pub async fn run< T: Send + 'static, - F: Send + 'static + Fn(TermWizTerminal) -> anyhow::Result, + F: Send + 'static + FnOnce(TermWizTerminal) -> anyhow::Result, >( width: usize, height: usize, @@ -457,6 +499,7 @@ pub async fn run< result } +#[allow(unused)] pub fn message_box_ok(message: &str) { let title = "wezterm"; let message = message.to_string(); @@ -497,7 +540,7 @@ pub fn show_configuration_error_message(err: &str) { ]) .map_err(Error::msg)?; - let mut editor = LineEditor::new(term); + let mut editor = LineEditor::new(&mut term); editor.set_prompt("(press enter to close this window)"); let mut host = NopLineEditorHost::default();