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:
Andy Dixon
2026-10-12 09:07:32 +01:00
parent afec95bb36
commit f09300fd95
28 changed files with 252 additions and 221 deletions
+8 -7
View File
@@ -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();
}
+18 -17
View File
@@ -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");
}
+14 -16
View File
@@ -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));
}
+6 -6
View File
@@ -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)();
+5 -4
View File
@@ -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 {
+7 -6
View File
@@ -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);
+27 -26
View File
@@ -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 -------------------------------------------------------------------------------
+9 -8
View File
@@ -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:?}");
}
+11 -10
View File
@@ -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];