diff --git a/Cargo.toml b/Cargo.toml index 2ee8094..3b006ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ polling = "3" rusqlite = "0.33" rustix = "0.38" serde = "1" +signal-hook = "0.3" smol = "2" thiserror = "2" tracing = "0.1" diff --git a/crates/egui-term/Cargo.toml b/crates/egui-term/Cargo.toml index 7cfd585..e5edf22 100644 --- a/crates/egui-term/Cargo.toml +++ b/crates/egui-term/Cargo.toml @@ -23,10 +23,13 @@ thiserror.workspace = true tracing.workspace = true wezterm-ssh = { workspace = true, features = ["vendored-openssl"] } +[target.'cfg(unix)'.dependencies] +signal-hook.workspace = true + [dev-dependencies] eframe = { workspace = true, features = [ - "accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. - "default_fonts", # Embed the default egui fonts. - "wgpu", # Use the glow rendering backend. Alternative: "wgpu". - "persistence", # Enable restoring app state when restarting the app. -] } \ No newline at end of file + "accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. + "default_fonts", # Embed the default egui fonts. + "wgpu", # Use the glow rendering backend. Alternative: "wgpu". + "persistence", # Enable restoring app state when restarting the app. +] } diff --git a/crates/egui-term/src/ssh/mod.rs b/crates/egui-term/src/ssh/mod.rs index 6b4bfa3..2297f63 100644 --- a/crates/egui-term/src/ssh/mod.rs +++ b/crates/egui-term/src/ssh/mod.rs @@ -5,7 +5,6 @@ use alacritty_terminal::tty::{ChildEvent, EventedPty, EventedReadWrite}; use anyhow::Context; use polling::{Event, PollMode, Poller}; use std::collections::HashMap; -use std::net::{TcpListener, TcpStream}; use std::sync::Arc; use tracing::{error, trace}; use wezterm_ssh::{ @@ -14,10 +13,23 @@ use wezterm_ssh::{ }; #[cfg(unix)] -use std::os::fd::{AsFd, AsRawFd}; +use signal_hook::{ + consts, + low_level::{pipe, unregister}, + SigId, +}; + +#[cfg(unix)] +use std::os::{ + fd::{AsFd, AsRawFd}, + unix::net::UnixStream, +}; #[cfg(windows)] -use std::os::windows::io::{AsRawSocket, AsSocket}; +use std::{ + net::{TcpListener, TcpStream}, + os::windows::io::{AsRawSocket, AsSocket}, +}; // Interest in PTY read/writes. #[cfg(unix)] @@ -30,12 +42,22 @@ const PTY_CHILD_EVENT_TOKEN: usize = 1; pub struct Pty { pub pty: SshPty, pub child: SshChildProcess, - pub signal: TcpStream, + #[cfg(unix)] + pub signals: UnixStream, + #[cfg(unix)] + pub sig_id: SigId, + #[cfg(windows)] + pub signals: TcpStream, } impl Drop for Pty { fn drop(&mut self) { let _ = self.child.kill(); + + // Clear signal-hook handler. + #[cfg(unix)] + unregister(self.sig_id); + let _ = self.child.wait(); } } @@ -66,7 +88,7 @@ impl EventedReadWrite for Pty { interest.key = PTY_READ_WRITE_TOKEN; let _ = self.pty.reader.set_non_blocking(true); let _ = self.pty.writer.set_non_blocking(true); - let _ = self.signal.set_nonblocking(true); + let _ = self.signals.set_nonblocking(true); #[cfg(unix)] { @@ -74,8 +96,8 @@ impl EventedReadWrite for Pty { poller.add_with_mode(self.pty.writer.as_raw_fd(), interest, mode)?; poller.add_with_mode( - self.signal.as_raw_fd(), - Event::readable(PTY_CHILD_EVENT_TOKEN), + &self.signals, + Event::writable(PTY_CHILD_EVENT_TOKEN), PollMode::Level, )?; } @@ -86,8 +108,8 @@ impl EventedReadWrite for Pty { poller.add_with_mode(self.pty.writer.as_raw_socket(), interest, mode)?; poller.add_with_mode( - self.signal.as_raw_socket(), - Event::readable(crate::ssh::PTY_CHILD_EVENT_TOKEN), + self.signals.as_raw_socket(), + Event::readable(PTY_CHILD_EVENT_TOKEN), PollMode::Level, )?; } @@ -109,8 +131,8 @@ impl EventedReadWrite for Pty { poller.modify_with_mode(self.pty.writer.as_fd(), interest, mode)?; poller.modify_with_mode( - self.signal.as_fd(), - Event::readable(PTY_CHILD_EVENT_TOKEN), + &self.signals, + Event::writable(PTY_CHILD_EVENT_TOKEN), PollMode::Level, )?; } @@ -121,8 +143,8 @@ impl EventedReadWrite for Pty { poller.modify_with_mode(self.pty.writer.as_socket(), interest, mode)?; poller.modify_with_mode( - self.signal.as_socket(), - Event::readable(crate::ssh::PTY_CHILD_EVENT_TOKEN), + self.signals.as_socket(), + Event::readable(PTY_CHILD_EVENT_TOKEN), PollMode::Level, )?; } @@ -136,7 +158,7 @@ impl EventedReadWrite for Pty { poller.delete(self.pty.reader.as_fd())?; poller.delete(self.pty.writer.as_fd())?; - poller.delete(self.signal.as_fd())?; + poller.delete(&self.signals)?; } #[cfg(windows)] @@ -144,7 +166,7 @@ impl EventedReadWrite for Pty { poller.delete(self.pty.reader.as_socket())?; poller.delete(self.pty.writer.as_socket())?; - poller.delete(self.signal.as_socket())?; + poller.delete(self.signals.as_socket())?; } Ok(()) @@ -206,10 +228,6 @@ impl Pty { verify.answer(true).await.context("send verify response")?; } SessionEvent::Authenticate(auth) => { - for a in auth.prompts.iter() { - println!("prompt: {}", a.prompt); - } - let mut answers = vec![]; for prompt in auth.prompts.iter() { if prompt.prompt.contains("Password") { @@ -234,15 +252,43 @@ impl Pty { // FIXME: set in settings let mut env = HashMap::new(); - env.insert("LANG".to_string(), "zh_CN.utf8".to_string()); + env.insert("LANG".to_string(), "en_US.UTF-8".to_string()); env.insert("LC_COLLATE".to_string(), "C".to_string()); let (pty, child) = session .request_pty("xterm-256color", PtySize::default(), None, Some(env)) .await?; - let signal = tcp_signal()?; - Ok(Pty { pty, child, signal }) + #[cfg(unix)] + { + // Prepare signal handling before spawning child. + let (signals, sig_id) = { + let (sender, recv) = UnixStream::pair()?; + + // Register the recv end of the pipe for SIGCHLD. + let sig_id = pipe::register(consts::SIGCHLD, sender)?; + recv.set_nonblocking(true)?; + (recv, sig_id) + }; + + Ok(Pty { + pty, + child, + signals, + sig_id, + }) + } + + #[cfg(windows)] + { + let listener = TcpListener::bind("127.0.0.1:0")?; + let signals = TcpStream::connect(listener.local_addr()?)?; + Ok(Pty { + pty, + child, + signals, + }) + } }) } } @@ -261,8 +307,3 @@ pub enum Authentication { Password(String, String), Config, } - -fn tcp_signal() -> std::io::Result { - let listener = TcpListener::bind("127.0.0.1:0")?; - TcpStream::connect(listener.local_addr()?) -} diff --git a/nxshell/assets/imgs/screenshot.png b/nxshell/assets/imgs/screenshot.png index 17b2679..ad119c5 100644 Binary files a/nxshell/assets/imgs/screenshot.png and b/nxshell/assets/imgs/screenshot.png differ diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 6b783df..7682e52 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -79,6 +79,7 @@ impl egui_dock::TabViewer for TabViewer<'_> { type Tab = Tab; fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { + let tab_id = tab.id(); match &mut tab.inner { TabInner::Term(term) => match term.term_type { TermType::Ssh { ref options } => { @@ -86,9 +87,19 @@ impl egui_dock::TabViewer for TabViewer<'_> { Authentication::Config => DRONE, Authentication::Password(..) => NUMPAD, }; - format!("{icon} {}", options.name).into() + if tab_id > 0 { + format!("{icon} {} ({tab_id})", options.name).into() + } else { + format!("{icon} {}", options.name).into() + } + } + TermType::Regular { .. } => { + if tab_id > 0 { + format!("local ({tab_id})").into() + } else { + "local".into() + } } - TermType::Regular { .. } => "local".into(), }, TabInner::SessionList(_) => "sessions".into(), }