fix: a panic on one thread no longer takes the whole app down
Every shared lock was std::sync::Mutex with lock().unwrap() (~150 sites), so a panic in any session's reader or callback poisoned the lock and the next UI frame panicked too, dropping every open session. Locks are now parking_lot::Mutex (no poisoning; already in the dependency tree), and a panic hook reports worker panics as an error toast instead of the session silently stopping (F3, F4).
This commit is contained in:
@@ -355,7 +355,8 @@ fn copy_limited_archive(dst: &mut impl Write, src: impl Read, max_bytes: u64) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn read_limited_rejects_oversized_response() {
|
||||
@@ -410,7 +411,7 @@ mod tests {
|
||||
std::thread::spawn(move || {
|
||||
for req in server.incoming_requests() {
|
||||
let header = |name: &'static str| req.headers().iter().find(|h| h.field.equiv(name)).map(|h| h.value.to_string());
|
||||
log.lock().unwrap().push((req.url().to_string(), header("Authorization"), header("User-Agent")));
|
||||
log.lock().push((req.url().to_string(), header("Authorization"), header("User-Agent")));
|
||||
let (status, body) = handler(req.url());
|
||||
let _ = req.respond(tiny_http::Response::from_string(body).with_status_code(status));
|
||||
}
|
||||
@@ -422,7 +423,7 @@ mod tests {
|
||||
fn validate_token_returns_login_and_reports_status() {
|
||||
let (base, seen) = serve(|_| (200, r#"{"login":"octocat"}"#.into()));
|
||||
assert_eq!(Client::new(&base, "tok").validate_token().unwrap(), "octocat");
|
||||
let (url, auth, ua) = seen.lock().unwrap()[0].clone();
|
||||
let (url, auth, ua) = seen.lock()[0].clone();
|
||||
assert_eq!(url, "/user");
|
||||
assert_eq!(auth.as_deref(), Some("Bearer tok"));
|
||||
assert_eq!(ua.as_deref(), Some(USER_AGENT));
|
||||
@@ -459,7 +460,7 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let calls = Mutex::new(Vec::new());
|
||||
let repos = Client::new(&base, "t").fetch_repos_with_progress(|done, total| calls.lock().unwrap().push((done, total))).unwrap();
|
||||
let repos = Client::new(&base, "t").fetch_repos_with_progress(|done, total| calls.lock().push((done, total))).unwrap();
|
||||
assert_eq!(repos.len(), 101);
|
||||
assert_eq!(repos[0].full_name, "me/r100");
|
||||
assert_eq!(repos[0].pushed_at, "2025-06-01T12:00:00Z");
|
||||
@@ -472,12 +473,12 @@ mod tests {
|
||||
assert!(r0.private && r0.description.is_empty() && r0.language.is_empty());
|
||||
assert!(repos.iter().filter(|r| r.latest_committer.is_empty()).count() == 99);
|
||||
|
||||
let mut calls = calls.into_inner().unwrap();
|
||||
let mut calls = calls.into_inner();
|
||||
assert_eq!(calls[0], (0, 101));
|
||||
calls.sort();
|
||||
assert_eq!(calls.last(), Some(&(101, 101)));
|
||||
assert_eq!(calls.len(), 102);
|
||||
assert_eq!(seen.lock().unwrap().iter().filter(|s| s.0.starts_with("/user/repos")).count(), 2);
|
||||
assert_eq!(seen.lock().iter().filter(|s| s.0.starts_with("/user/repos")).count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -505,7 +506,7 @@ mod tests {
|
||||
assert_eq!(std::fs::read(&dest).unwrap(), b"ZIPDATA");
|
||||
let err = client.download_repo_archive("me/r", "", &dest).unwrap_err().to_string();
|
||||
assert_eq!(err, "GitHub API returned HTTP 404: missing");
|
||||
assert_eq!(seen.lock().unwrap()[1].0, "/repos/me/r/zipball");
|
||||
assert_eq!(seen.lock()[1].0, "/repos/me/r/zipball");
|
||||
assert!(client.download_repo_archive("bad", "", &dest).is_err());
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
//! optionally wrapped in TLS). It alternates between short reads and draining the outgoing
|
||||
//! queue, so the transport never has to be split.
|
||||
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -197,7 +198,7 @@ impl Client {
|
||||
}
|
||||
|
||||
fn st(&self) -> MutexGuard<'_, State> {
|
||||
self.state.lock().unwrap_or_else(|e| e.into_inner())
|
||||
self.state.lock()
|
||||
}
|
||||
|
||||
/// Connects and blocks until registration succeeds (RPL_WELCOME), fails, or times out.
|
||||
@@ -205,7 +206,7 @@ impl Client {
|
||||
let (ready_tx, ready_rx) = mpsc::channel();
|
||||
let (out_tx, out_rx) = mpsc::channel();
|
||||
self.st().ready = Some(ready_tx);
|
||||
*self.out.lock().unwrap_or_else(|e| e.into_inner()) = Some(out_tx);
|
||||
*self.out.lock() = Some(out_tx);
|
||||
|
||||
let client = self.clone();
|
||||
std::thread::spawn(move || client.io_thread(out_rx));
|
||||
@@ -234,7 +235,7 @@ impl Client {
|
||||
}
|
||||
|
||||
fn queue(&self, out: Out) -> bool {
|
||||
let guard = self.out.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let guard = self.out.lock();
|
||||
guard.as_ref().is_some_and(|tx| tx.send(out).is_ok())
|
||||
}
|
||||
|
||||
@@ -471,7 +472,7 @@ impl Client {
|
||||
|
||||
fn io_thread(self: Arc<Self>, out_rx: Receiver<Out>) {
|
||||
let result = self.open_transport().and_then(|stream| self.run(stream, &out_rx));
|
||||
*self.out.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
*self.out.lock() = None;
|
||||
let (was_closing, was_connected, ready) = {
|
||||
let mut st = self.st();
|
||||
let r = (st.closing, st.connected, st.ready.take());
|
||||
@@ -1037,13 +1038,13 @@ mod tests {
|
||||
fn remember_room_membership_tracks_confirmed_channels() {
|
||||
let updates = Arc::new(Mutex::new(Vec::new()));
|
||||
let log = updates.clone();
|
||||
let client = Client::new(Config { on_rooms_changed: Arc::new(move |rooms| log.lock().unwrap().push(rooms.join(","))), ..Default::default() });
|
||||
let client = Client::new(Config { on_rooms_changed: Arc::new(move |rooms| log.lock().push(rooms.join(","))), ..Default::default() });
|
||||
client.remember_room_membership("#One", true);
|
||||
client.remember_room_membership("#two", true);
|
||||
client.remember_room_membership("#one", true);
|
||||
client.remember_room_membership("#missing", false);
|
||||
client.remember_room_membership("#ONE", false);
|
||||
assert_eq!(updates.lock().unwrap().join("|"), "#One|#One,#two|#two");
|
||||
assert_eq!(updates.lock().join("|"), "#One|#One,#two|#two");
|
||||
assert_eq!(client.st().room_names, vec!["#two"]);
|
||||
}
|
||||
|
||||
@@ -1109,9 +1110,9 @@ mod tests {
|
||||
nick: "me".into(),
|
||||
connect_timeout: Duration::from_secs(5),
|
||||
on_event: Arc::new(move |e| {
|
||||
let _ = tx.lock().unwrap().send(e);
|
||||
let _ = tx.lock().send(e);
|
||||
}),
|
||||
on_close: Arc::new(move || *c.lock().unwrap() += 1),
|
||||
on_close: Arc::new(move || *c.lock() += 1),
|
||||
..cfg
|
||||
});
|
||||
(client, listener, rx, closes)
|
||||
@@ -1224,11 +1225,11 @@ mod tests {
|
||||
client.close();
|
||||
srv.expect("QUIT :Nexus Terminal disconnected");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while *closes.lock().unwrap() == 0 && Instant::now() < deadline {
|
||||
while *closes.lock() == 0 && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
assert_eq!(*closes.lock().unwrap(), 1);
|
||||
assert_eq!(*closes.lock(), 1);
|
||||
assert!(client.handle_input("#a", "late").is_err());
|
||||
}
|
||||
|
||||
@@ -1236,7 +1237,7 @@ mod tests {
|
||||
fn remote_close_reports_error_and_close() {
|
||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
||||
let log = errors.clone();
|
||||
let (client, listener, rx, closes) = harness(Config { on_error: Arc::new(move |e| log.lock().unwrap().push(e)), ..Default::default() });
|
||||
let (client, listener, rx, closes) = harness(Config { on_error: Arc::new(move |e| log.lock().push(e)), ..Default::default() });
|
||||
let c = client.clone();
|
||||
let connecting = std::thread::spawn(move || c.connect());
|
||||
let mut srv = accept(&listener);
|
||||
@@ -1248,8 +1249,8 @@ mod tests {
|
||||
drop(srv);
|
||||
assert_eq!(wait_for(&rx, |e| e.kind == "error").text, "connection closed by server");
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
assert_eq!(*errors.lock().unwrap(), vec!["connection closed by server"]);
|
||||
assert_eq!(*closes.lock().unwrap(), 1);
|
||||
assert_eq!(*errors.lock(), vec!["connection closed by server"]);
|
||||
assert_eq!(*closes.lock(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1259,7 +1260,7 @@ mod tests {
|
||||
let err = client.connect().unwrap_err().to_string();
|
||||
assert_eq!(err, "IRC connection timed out after 300ms");
|
||||
drop(listener);
|
||||
assert_eq!(*closes.lock().unwrap(), 0);
|
||||
assert_eq!(*closes.lock(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1273,7 +1274,7 @@ mod tests {
|
||||
nick: "me".into(),
|
||||
connect_timeout: Duration::from_secs(5),
|
||||
connector: Some(Arc::new(move || {
|
||||
*flag.lock().unwrap() = true;
|
||||
*flag.lock() = true;
|
||||
let s = TcpStream::connect(addr)?;
|
||||
s.set_read_timeout(Some(Duration::from_millis(20)))?;
|
||||
Ok(Box::new(s) as Box<dyn ReadWrite>)
|
||||
@@ -1286,7 +1287,7 @@ mod tests {
|
||||
srv.expect("USER me * * me");
|
||||
srv.send(":srv 001 me :Welcome");
|
||||
connecting.join().unwrap().unwrap();
|
||||
assert!(*used.lock().unwrap());
|
||||
assert!(*used.lock());
|
||||
client.handle_input("", "/quit gone").unwrap();
|
||||
srv.expect("QUIT gone");
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
use super::Callbacks;
|
||||
use crate::Result;
|
||||
use parking_lot::Mutex;
|
||||
use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn default_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
@@ -36,7 +37,7 @@ impl LocalShell {
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>) -> Result<()> {
|
||||
let mut st = self.state.lock().unwrap();
|
||||
let mut st = self.state.lock();
|
||||
if st.4 {
|
||||
return Err("shell already closed".into());
|
||||
}
|
||||
@@ -77,7 +78,7 @@ impl LocalShell {
|
||||
std::thread::Builder::new().name("nexus-local-wait".into()).spawn(move || {
|
||||
let _ = child.wait();
|
||||
let was_closed = {
|
||||
let mut st = me.state.lock().unwrap();
|
||||
let mut st = me.state.lock();
|
||||
let was = st.4;
|
||||
st.4 = true;
|
||||
st.0 = None;
|
||||
@@ -94,14 +95,14 @@ impl LocalShell {
|
||||
if data.iter().any(|b| matches!(b, b'\r' | b'\n' | 4)) {
|
||||
self.capture_cwd();
|
||||
}
|
||||
let mut st = self.state.lock().unwrap();
|
||||
let mut st = self.state.lock();
|
||||
let r = st.0.as_mut().ok_or("shell not running")?;
|
||||
r.writer.write_all(data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
|
||||
let mut st = self.state.lock().unwrap();
|
||||
let mut st = self.state.lock();
|
||||
if cols > 0 {
|
||||
st.2 = cols;
|
||||
}
|
||||
@@ -117,7 +118,7 @@ impl LocalShell {
|
||||
|
||||
/// Idempotent; does not fire on_close (matches Go: an explicit close is not an exit).
|
||||
pub fn close(&self) {
|
||||
let mut st = self.state.lock().unwrap();
|
||||
let mut st = self.state.lock();
|
||||
if st.4 {
|
||||
return;
|
||||
}
|
||||
@@ -129,13 +130,13 @@ impl LocalShell {
|
||||
|
||||
/// The last directory the shell was seen in (used to restart in the same place).
|
||||
pub fn working_directory(&self) -> PathBuf {
|
||||
self.state.lock().unwrap().1.clone()
|
||||
self.state.lock().1.clone()
|
||||
}
|
||||
|
||||
fn capture_cwd(&self) {
|
||||
let pid = self.state.lock().unwrap().0.as_ref().and_then(|r| r.pid);
|
||||
let pid = self.state.lock().0.as_ref().and_then(|r| r.pid);
|
||||
if let Some(dir) = pid.and_then(process_cwd).filter(|d| d.is_dir()) {
|
||||
self.state.lock().unwrap().1 = dir;
|
||||
self.state.lock().1 = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,22 +166,19 @@ mod tests {
|
||||
let seen: Arc<Mutex<Vec<u8>>> = Arc::default();
|
||||
let closed = Arc::new(Mutex::new(0));
|
||||
let (s2, c2) = (seen.clone(), closed.clone());
|
||||
let cb = Callbacks {
|
||||
on_data: Arc::new(move |d| s2.lock().unwrap().extend_from_slice(d)),
|
||||
on_error: Arc::new(|_| {}),
|
||||
on_close: Arc::new(move || *c2.lock().unwrap() += 1),
|
||||
};
|
||||
let cb =
|
||||
Callbacks { on_data: Arc::new(move |d| s2.lock().extend_from_slice(d)), on_error: Arc::new(|_| {}), on_close: Arc::new(move || *c2.lock() += 1) };
|
||||
let sh = LocalShell::new("/bin/sh", Some("/tmp".into()), 80, 24, cb);
|
||||
sh.start().unwrap();
|
||||
sh.write(b"cd / && echo local-$((2*21))\n").unwrap();
|
||||
let t = Instant::now();
|
||||
while !String::from_utf8_lossy(&seen.lock().unwrap()).contains("local-42") {
|
||||
while !String::from_utf8_lossy(&seen.lock()).contains("local-42") {
|
||||
assert!(t.elapsed() < Duration::from_secs(5));
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
sh.resize(100, 30).unwrap();
|
||||
sh.write(b"exit\n").unwrap();
|
||||
while *closed.lock().unwrap() == 0 {
|
||||
while *closed.lock() == 0 {
|
||||
assert!(t.elapsed() < Duration::from_secs(5));
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
||||
@@ -69,20 +69,20 @@ pub(crate) fn dial(host: &str, port: i64, timeout_secs: i64) -> (String, std::io
|
||||
/// a lock, a reader thread that feeds `on_read`, and Go's error/close semantics (a read error or EOF
|
||||
/// reports `read error: …` then closes; `close` is idempotent and fires `on_close` once).
|
||||
pub(crate) struct TcpConn {
|
||||
stream: std::sync::Mutex<Option<std::net::TcpStream>>,
|
||||
stream: parking_lot::Mutex<Option<std::net::TcpStream>>,
|
||||
on_error: ErrorFn,
|
||||
on_close: CloseFn,
|
||||
}
|
||||
|
||||
impl TcpConn {
|
||||
pub(crate) fn new(on_error: ErrorFn, on_close: CloseFn) -> TcpConn {
|
||||
TcpConn { stream: std::sync::Mutex::new(None), on_error, on_close }
|
||||
TcpConn { stream: parking_lot::Mutex::new(None), on_error, on_close }
|
||||
}
|
||||
|
||||
pub(crate) fn connect(self: &Arc<Self>, stream: std::net::TcpStream, mut on_read: impl FnMut(&[u8]) + Send + 'static) -> crate::Result<()> {
|
||||
let mut reader = stream.try_clone()?;
|
||||
{
|
||||
let mut guard = self.stream.lock().unwrap();
|
||||
let mut guard = self.stream.lock();
|
||||
if guard.is_some() {
|
||||
return Err("already connected".into());
|
||||
}
|
||||
@@ -115,18 +115,18 @@ impl TcpConn {
|
||||
|
||||
pub(crate) fn write(&self, data: &[u8]) -> crate::Result<()> {
|
||||
use std::io::Write;
|
||||
let guard = self.stream.lock().unwrap();
|
||||
let guard = self.stream.lock();
|
||||
let mut s = guard.as_ref().ok_or("not connected")?;
|
||||
s.write_all(data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_connected(&self) -> bool {
|
||||
self.stream.lock().unwrap().is_some()
|
||||
self.stream.lock().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn close(&self) {
|
||||
let taken = self.stream.lock().unwrap().take();
|
||||
let taken = self.stream.lock().take();
|
||||
if let Some(s) = taken {
|
||||
let _ = s.shutdown(std::net::Shutdown::Both);
|
||||
(self.on_close)();
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
//! of sent ("send") and received ("recv") frames, JSON capture, and the hex/escape encoders the
|
||||
//! RAW view reuses.
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::Engine;
|
||||
use serde::Serialize;
|
||||
@@ -115,11 +116,11 @@ impl RawClient {
|
||||
}
|
||||
|
||||
pub fn history(&self) -> Vec<HistoryEntry> {
|
||||
self.inner.history.lock().unwrap().entries.iter().cloned().collect()
|
||||
self.inner.history.lock().entries.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn clear_history(&self) {
|
||||
*self.inner.history.lock().unwrap() = History::default();
|
||||
*self.inner.history.lock() = History::default();
|
||||
}
|
||||
|
||||
/// Writes the history to `path` as indented JSON (mode 0600 on Unix), as Go did.
|
||||
@@ -146,7 +147,7 @@ impl Inner {
|
||||
if entry.data.len() > MAX_HISTORY_BYTES {
|
||||
entry.data.drain(..entry.data.len() - MAX_HISTORY_BYTES);
|
||||
}
|
||||
let mut h = self.history.lock().unwrap();
|
||||
let mut h = self.history.lock();
|
||||
h.bytes += entry.data.len();
|
||||
h.entries.push_back(entry);
|
||||
while h.entries.len() > MAX_HISTORY_ENTRIES || h.bytes > MAX_HISTORY_BYTES {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
//! `Profile::host`, line settings come from `Profile::serial`. Same conventions as the other
|
||||
//! clients: blocking `connect`, an own reader thread, `on_close` exactly once (unplug included).
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use serialport::{DataBits, FlowControl, Parity, SerialPort, StopBits};
|
||||
@@ -45,7 +46,7 @@ impl SerialClient {
|
||||
/// Opens and configures the port, applies DTR/RTS, and starts the reader thread.
|
||||
pub fn connect(&self) -> crate::Result<()> {
|
||||
let inner = &self.inner;
|
||||
if inner.port.lock().unwrap().is_some() {
|
||||
if inner.port.lock().is_some() {
|
||||
return Err("already connected".into());
|
||||
}
|
||||
if inner.path.is_empty() {
|
||||
@@ -82,7 +83,7 @@ impl SerialClient {
|
||||
reader.set_timeout(READ_POLL)?;
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
let mut guard = inner.port.lock().unwrap();
|
||||
let mut guard = inner.port.lock();
|
||||
if guard.is_some() {
|
||||
return Err("already connected".into());
|
||||
}
|
||||
@@ -128,11 +129,11 @@ impl SerialClient {
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.inner.port.lock().unwrap().is_some()
|
||||
self.inner.port.lock().is_some()
|
||||
}
|
||||
|
||||
fn with_port(&self, f: impl FnOnce(&mut Box<dyn SerialPort>) -> crate::Result<()>) -> crate::Result<()> {
|
||||
let mut guard = self.inner.port.lock().unwrap();
|
||||
let mut guard = self.inner.port.lock();
|
||||
let (port, _) = guard.as_mut().ok_or("not connected")?;
|
||||
f(port)
|
||||
}
|
||||
@@ -165,7 +166,7 @@ impl Inner {
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let taken = self.port.lock().unwrap().take();
|
||||
let taken = self.port.lock().take();
|
||||
if let Some((port, stop)) = taken {
|
||||
stop.store(true, Ordering::SeqCst);
|
||||
drop(port);
|
||||
|
||||
@@ -6,11 +6,12 @@ use super::{Callbacks, rt};
|
||||
use crate::Result;
|
||||
use crate::models::{ForwardDirection, JumpHost, PortForward, Profile, SshConfig};
|
||||
use base64::Engine;
|
||||
use parking_lot::Mutex;
|
||||
use russh::client::{self, Handle, Msg};
|
||||
use russh::keys::{HashAlg, PrivateKeyWithHashAlg, PublicKey};
|
||||
use russh::{Channel, ChannelMsg, Pty};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -75,7 +76,7 @@ impl client::Handler for Handler {
|
||||
match res {
|
||||
Ok(()) => Ok(true),
|
||||
Err(why) => {
|
||||
*self.shared.rejection.lock().unwrap() = Some(why);
|
||||
*self.shared.rejection.lock() = Some(why);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -91,7 +92,7 @@ impl client::Handler for Handler {
|
||||
reply: client::ChannelOpenHandle,
|
||||
_session: &mut client::Session,
|
||||
) -> std::result::Result<(), Self::Error> {
|
||||
let dest = self.shared.remote_targets.lock().unwrap().get(&(connected_address.to_string(), connected_port)).cloned();
|
||||
let dest = self.shared.remote_targets.lock().get(&(connected_address.to_string(), connected_port)).cloned();
|
||||
let Some((host, port)) = dest else {
|
||||
reply.reject(russh::ChannelOpenFailure::AdministrativelyProhibited).await;
|
||||
return Ok(());
|
||||
@@ -485,7 +486,7 @@ impl SshClient {
|
||||
match tokio::time::timeout(timeout, client::connect_stream(config(&self.sc()), stream, handler)).await {
|
||||
Err(_) => Err(format!("SSH handshake with {host}:{port} timed out").into()),
|
||||
Ok(Ok(h)) => Ok(h),
|
||||
Ok(Err(e)) => match self.shared.rejection.lock().unwrap().take() {
|
||||
Ok(Err(e)) => match self.shared.rejection.lock().take() {
|
||||
Some(why) => Err(why.into()),
|
||||
None => Err(format!("SSH handshake failed: {e}").into()),
|
||||
},
|
||||
@@ -518,15 +519,15 @@ impl SshClient {
|
||||
.channel_open_direct_tcpip(p.host.clone(), port as u32, "127.0.0.1", 0)
|
||||
.await
|
||||
.map_err(|e| format!("failed to dial target {}:{port} through jump hosts: {e}", p.host))?;
|
||||
self.hops.lock().unwrap().extend(h);
|
||||
self.hops.lock().unwrap().push(prev);
|
||||
self.hops.lock().extend(h);
|
||||
self.hops.lock().push(prev);
|
||||
self.handshake(ch.into_stream(), &p.host, port, &check).await?
|
||||
};
|
||||
|
||||
authenticate(&mut handle, Auth { user: &p.username, methods, agent: sc.agent_auth, key, password, totp_secret: sc.totp_secret.clone() }).await?;
|
||||
let handle = Arc::new(handle);
|
||||
if !shell {
|
||||
*self.handle.lock().unwrap() = Some(handle);
|
||||
*self.handle.lock() = Some(handle);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -551,8 +552,8 @@ impl SshClient {
|
||||
|
||||
let (mut read, write) = ch.split();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Cmd>();
|
||||
*self.tx.lock().unwrap() = Some(tx);
|
||||
*self.handle.lock().unwrap() = Some(handle);
|
||||
*self.tx.lock() = Some(tx);
|
||||
*self.handle.lock() = Some(handle);
|
||||
tokio::spawn(async move {
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
let r = match cmd {
|
||||
@@ -633,16 +634,16 @@ impl SshClient {
|
||||
}
|
||||
|
||||
fn handle(&self) -> Result<Arc<Handle<Handler>>> {
|
||||
self.handle.lock().unwrap().clone().ok_or_else(|| "not connected".into())
|
||||
self.handle.lock().clone().ok_or_else(|| "not connected".into())
|
||||
}
|
||||
|
||||
pub fn write(&self, data: &[u8]) -> Result<()> {
|
||||
let tx = self.tx.lock().unwrap();
|
||||
let tx = self.tx.lock();
|
||||
tx.as_ref().ok_or("not connected")?.send(Cmd::Data(data.to_vec())).map_err(|_| "not connected".into())
|
||||
}
|
||||
|
||||
pub fn resize(&self, cols: u32, rows: u32) -> Result<()> {
|
||||
let tx = self.tx.lock().unwrap();
|
||||
let tx = self.tx.lock();
|
||||
tx.as_ref().ok_or("not connected")?.send(Cmd::Resize(cols, rows)).map_err(|_| "not connected".into())
|
||||
}
|
||||
|
||||
@@ -679,7 +680,7 @@ impl SshClient {
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.handle.lock().unwrap().as_ref().is_some_and(|h| !h.is_closed())
|
||||
self.handle.lock().as_ref().is_some_and(|h| !h.is_closed())
|
||||
}
|
||||
|
||||
/// Idempotent; fires on_close once.
|
||||
@@ -688,9 +689,9 @@ impl SshClient {
|
||||
return;
|
||||
}
|
||||
self.stop_all_forwards();
|
||||
*self.tx.lock().unwrap() = None;
|
||||
let h = self.handle.lock().unwrap().take();
|
||||
let hops = std::mem::take(&mut *self.hops.lock().unwrap());
|
||||
*self.tx.lock() = None;
|
||||
let h = self.handle.lock().take();
|
||||
let hops = std::mem::take(&mut *self.hops.lock());
|
||||
rt().spawn(async move {
|
||||
if let Some(h) = h {
|
||||
let _ = h.disconnect(russh::Disconnect::ByApplication, "", "en").await;
|
||||
@@ -705,7 +706,7 @@ impl SshClient {
|
||||
// --- port forwarding -------------------------------------------------------------------
|
||||
|
||||
pub fn start_forward(&self, rule: PortForward) -> Result<()> {
|
||||
if self.forwards.lock().unwrap().contains_key(&rule.id) {
|
||||
if self.forwards.lock().contains_key(&rule.id) {
|
||||
return Err(format!("forward {:?} is already active", rule.id).into());
|
||||
}
|
||||
let h = self.handle()?;
|
||||
@@ -727,12 +728,12 @@ impl SshClient {
|
||||
let _ = tokio::io::copy_bidirectional(&mut sock, &mut remote).await;
|
||||
}
|
||||
});
|
||||
let mut handles = t2.lock().unwrap();
|
||||
let mut handles = t2.lock();
|
||||
handles.retain(|h| !h.is_finished());
|
||||
handles.push(conn.abort_handle());
|
||||
}
|
||||
});
|
||||
tasks.lock().unwrap().push(task.abort_handle());
|
||||
tasks.lock().push(task.abort_handle());
|
||||
}
|
||||
ForwardDirection::Remote => {
|
||||
let bind = if rule.bind_host.is_empty() { "0.0.0.0".to_string() } else { rule.bind_host.clone() };
|
||||
@@ -740,26 +741,26 @@ impl SshClient {
|
||||
.block_on(h.tcpip_forward(bind.clone(), rule.bind_port as u32))
|
||||
.map_err(|e| format!("remote forward: server listen on {bind}:{} failed: {e}", rule.bind_port))?;
|
||||
let port = if rule.bind_port == 0 { port } else { rule.bind_port as u32 };
|
||||
self.shared.remote_targets.lock().unwrap().insert((bind, port), (rule.dest_host.clone(), rule.dest_port as u16));
|
||||
self.shared.remote_targets.lock().insert((bind, port), (rule.dest_host.clone(), rule.dest_port as u16));
|
||||
}
|
||||
}
|
||||
self.forwards.lock().unwrap().insert(rule.id.clone(), Forward { rule, tasks });
|
||||
self.forwards.lock().insert(rule.id.clone(), Forward { rule, tasks });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_forward(&self, id: &str) -> Result<()> {
|
||||
let f = self.forwards.lock().unwrap().remove(id).ok_or_else(|| format!("forward {id:?} not found"))?;
|
||||
let f = self.forwards.lock().remove(id).ok_or_else(|| format!("forward {id:?} not found"))?;
|
||||
self.shutdown_forward(f);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shutdown_forward(&self, f: Forward) {
|
||||
for t in f.tasks.lock().unwrap().drain(..) {
|
||||
for t in f.tasks.lock().drain(..) {
|
||||
t.abort();
|
||||
}
|
||||
if f.rule.direction == ForwardDirection::Remote {
|
||||
let bind = if f.rule.bind_host.is_empty() { "0.0.0.0".to_string() } else { f.rule.bind_host.clone() };
|
||||
self.shared.remote_targets.lock().unwrap().retain(|(a, p), _| !(a == &bind && *p == f.rule.bind_port as u32));
|
||||
self.shared.remote_targets.lock().retain(|(a, p), _| !(a == &bind && *p == f.rule.bind_port as u32));
|
||||
if let Ok(h) = self.handle() {
|
||||
let port = f.rule.bind_port as u32;
|
||||
rt().spawn(async move {
|
||||
@@ -770,14 +771,14 @@ impl SshClient {
|
||||
}
|
||||
|
||||
fn stop_all_forwards(&self) {
|
||||
let all: Vec<Forward> = self.forwards.lock().unwrap().drain().map(|(_, f)| f).collect();
|
||||
let all: Vec<Forward> = self.forwards.lock().drain().map(|(_, f)| f).collect();
|
||||
for f in all {
|
||||
self.shutdown_forward(f);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_forwards(&self) -> Vec<PortForward> {
|
||||
self.forwards.lock().unwrap().values().map(|f| f.rule.clone()).collect()
|
||||
self.forwards.lock().values().map(|f| f.rule.clone()).collect()
|
||||
}
|
||||
|
||||
// --- SFTP -------------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! TTYPE, NAWS; everything else refused), IAC escaping, line-ending conversion, ISO 8859-1 output
|
||||
//! encoding and BRK. Go read but never acted on `lineMode`, and neither do we.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Callbacks, DataFn, DebugFn, TcpConn};
|
||||
use crate::models::{LineEnding, Profile, TelnetConfig};
|
||||
@@ -96,7 +97,7 @@ impl TelnetClient {
|
||||
return Err("not connected".into());
|
||||
}
|
||||
{
|
||||
let mut st = self.inner.state.lock().unwrap();
|
||||
let mut st = self.inner.state.lock();
|
||||
st.cols = cols;
|
||||
st.rows = rows;
|
||||
}
|
||||
@@ -119,7 +120,7 @@ impl TelnetClient {
|
||||
/// The negotiated local-echo state: starts at the profile's `localEcho`, cleared by the
|
||||
/// server's WILL ECHO and set by WONT ECHO. Like Go, the client itself never echoes.
|
||||
pub fn local_echo(&self) -> bool {
|
||||
self.inner.state.lock().unwrap().local_echo
|
||||
self.inner.state.lock().local_echo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ impl Inner {
|
||||
OPT_NAWS => {
|
||||
self.negotiate(WILL, opt);
|
||||
let (cols, rows) = {
|
||||
let st = self.state.lock().unwrap();
|
||||
let st = self.state.lock();
|
||||
(st.cols, st.rows)
|
||||
};
|
||||
let _ = self.send_naws(cols, rows);
|
||||
@@ -215,7 +216,7 @@ impl Inner {
|
||||
match opt {
|
||||
OPT_ECHO => {
|
||||
self.negotiate(DO, opt);
|
||||
self.state.lock().unwrap().local_echo = false;
|
||||
self.state.lock().local_echo = false;
|
||||
}
|
||||
OPT_SGA => self.negotiate(DO, opt),
|
||||
_ => self.negotiate(DONT, opt),
|
||||
@@ -225,7 +226,7 @@ impl Inner {
|
||||
fn handle_wont(&self, opt: u8) {
|
||||
self.debug(format!("received WONT {opt}"));
|
||||
if opt == OPT_ECHO {
|
||||
self.state.lock().unwrap().local_echo = true;
|
||||
self.state.lock().local_echo = true;
|
||||
}
|
||||
self.negotiate(DONT, opt);
|
||||
}
|
||||
@@ -362,7 +363,7 @@ mod tests {
|
||||
on_error: Arc::new(move |e| etx.send(e).unwrap()),
|
||||
on_close: Arc::new(move || ctx.send(()).unwrap()),
|
||||
};
|
||||
let client = TelnetClient::new(&profile, cb, Some(Arc::new(move |m| dbg.lock().unwrap().push(m))));
|
||||
let client = TelnetClient::new(&profile, cb, Some(Arc::new(move |m| dbg.lock().push(m))));
|
||||
client.connect().unwrap();
|
||||
let (mut srv, _) = listener.accept().unwrap();
|
||||
srv.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
@@ -399,7 +400,7 @@ mod tests {
|
||||
client.write(b"ls\n\xff").unwrap();
|
||||
assert_eq!(read_n(&mut srv, 6), b"ls\r\n\xff\xff");
|
||||
|
||||
let log = debug.lock().unwrap().clone();
|
||||
let log = debug.lock().clone();
|
||||
for m in ["received DO 24", "sending WILL 31", "sending NAWS: 80x24", "sending WONT 99", "sending terminal type: xterm-256color"] {
|
||||
assert!(log.iter().any(|l| l == m), "missing debug {m:?} in {log:?}");
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
//! the shared `Arc<Mutex<Framebuffer>>` the UI reads, so the lock is never held across network reads.
|
||||
//! Outgoing input is queued and written whenever a read polls out, so the transport is never split.
|
||||
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use std::io::{BufReader, ErrorKind, Read, Write};
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use flate2::{Decompress, FlushDecompress};
|
||||
@@ -152,7 +153,7 @@ impl Client {
|
||||
}
|
||||
|
||||
fn st(&self) -> MutexGuard<'_, State> {
|
||||
self.state.lock().unwrap_or_else(|e| e.into_inner())
|
||||
self.state.lock()
|
||||
}
|
||||
|
||||
/// Connects and blocks until the handshake completes (ServerInit received), fails, or times out.
|
||||
@@ -160,7 +161,7 @@ impl Client {
|
||||
let (ready_tx, ready_rx) = mpsc::channel();
|
||||
let (out_tx, out_rx) = mpsc::channel();
|
||||
self.st().ready = Some(ready_tx);
|
||||
*self.out.lock().unwrap_or_else(|e| e.into_inner()) = Some(out_tx);
|
||||
*self.out.lock() = Some(out_tx);
|
||||
|
||||
let client = self.clone();
|
||||
std::thread::spawn(move || client.io_thread(out_rx));
|
||||
@@ -186,7 +187,7 @@ impl Client {
|
||||
st.closing = true;
|
||||
}
|
||||
// Dropping the sender makes the I/O thread's next poll end the connection.
|
||||
*self.out.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
*self.out.lock() = None;
|
||||
}
|
||||
|
||||
pub fn framebuffer(&self) -> Arc<Mutex<Framebuffer>> {
|
||||
@@ -233,20 +234,20 @@ impl Client {
|
||||
/// Asks for a non-incremental update of the whole screen.
|
||||
pub fn request_full_update(&self) -> crate::Result<()> {
|
||||
let (w, h) = {
|
||||
let fb = self.fb.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let fb = self.fb.lock();
|
||||
(fb.width, fb.height)
|
||||
};
|
||||
self.queue(update_request(false, w, h))
|
||||
}
|
||||
|
||||
fn queue(&self, msg: Vec<u8>) -> crate::Result<()> {
|
||||
let guard = self.out.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let guard = self.out.lock();
|
||||
if guard.as_ref().is_some_and(|tx| tx.send(msg).is_ok()) { Ok(()) } else { Err("not connected".into()) }
|
||||
}
|
||||
|
||||
fn io_thread(self: Arc<Self>, out_rx: Receiver<Vec<u8>>) {
|
||||
let result = self.open_transport().and_then(|io| self.run(io, out_rx));
|
||||
*self.out.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
*self.out.lock() = None;
|
||||
let (was_closing, was_connected, ready) = {
|
||||
let mut st = self.st();
|
||||
let r = (st.closing, st.connected, st.ready.take());
|
||||
@@ -283,7 +284,7 @@ impl Client {
|
||||
let mut r = BufReader::with_capacity(1 << 16, Conn { io, out_rx });
|
||||
let (w, h) = self.handshake(&mut r)?;
|
||||
let mut local = Framebuffer::new_checked(w, h)?;
|
||||
*self.fb.lock().unwrap_or_else(|e| e.into_inner()) = local.clone();
|
||||
*self.fb.lock() = local.clone();
|
||||
(self.cfg.on_resize)(w, h);
|
||||
{
|
||||
let mut st = self.st();
|
||||
@@ -302,7 +303,7 @@ impl Client {
|
||||
0 => {
|
||||
let (y0, y1, resized) = read_update(&mut r, &mut local, &mut dec)?;
|
||||
{
|
||||
let mut fb = self.fb.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut fb = self.fb.lock();
|
||||
if resized {
|
||||
*fb = local.clone();
|
||||
} else if y0 < y1 {
|
||||
@@ -983,7 +984,7 @@ mod tests {
|
||||
frame_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
{
|
||||
let fb = client.framebuffer();
|
||||
let fb = fb.lock().unwrap();
|
||||
let fb = fb.lock();
|
||||
assert_eq!((fb.width, fb.height), (4, 4));
|
||||
assert!(fb.dirty);
|
||||
let px = |x: usize, y: usize| &fb.rgba[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4];
|
||||
|
||||
Reference in New Issue
Block a user