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:
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
||||
nexus-core = { path = "../core" }
|
||||
eframe = { version = "0.36", default-features = false, features = ["default_fonts", "glow", "wayland", "x11", "accesskit", "persistence"] }
|
||||
egui = "0.36"
|
||||
parking_lot = "0.12"
|
||||
egui-phosphor = "0.14"
|
||||
rfd = "0.17"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
+12
-4
@@ -11,9 +11,10 @@ use egui::{Key, KeyboardShortcut, Modifiers, Ui};
|
||||
use nexus_core::models::{Profile, Protocol, SessionInfo, SessionState};
|
||||
use nexus_core::service::Service;
|
||||
use nexus_core::sessions::{Event, HostKeyDecision};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::sync::{Arc, mpsc};
|
||||
|
||||
pub struct Options {
|
||||
pub screenshot: Option<PathBuf>,
|
||||
@@ -37,7 +38,14 @@ pub struct Options {
|
||||
static POSTED: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
pub fn post_error(e: String) {
|
||||
POSTED.lock().unwrap().push(e);
|
||||
POSTED.lock().push(e);
|
||||
}
|
||||
|
||||
/// For the panic hook: the panicking thread may already hold the lock.
|
||||
pub fn post_error_nonblocking(e: String) {
|
||||
if let Some(mut p) = POSTED.try_lock() {
|
||||
p.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// A credential or host-key question from a connecting session.
|
||||
@@ -141,7 +149,7 @@ impl App {
|
||||
let ctx = cc.egui_ctx.clone();
|
||||
let tx = Mutex::new(tx);
|
||||
svc.sessions.set_sink(Arc::new(move |e| {
|
||||
let _ = tx.lock().unwrap().send(e);
|
||||
let _ = tx.lock().send(e);
|
||||
ctx.request_repaint();
|
||||
}));
|
||||
let s = Shared::new(svc.clone(), cc.egui_ctx.clone());
|
||||
@@ -405,7 +413,7 @@ impl App {
|
||||
// --- events ---------------------------------------------------------------------------
|
||||
|
||||
fn drain_events(&mut self) {
|
||||
for e in POSTED.lock().unwrap().drain(..) {
|
||||
for e in POSTED.lock().drain(..) {
|
||||
self.s.error(e);
|
||||
}
|
||||
while let Ok(e) = self.rx.try_recv() {
|
||||
|
||||
@@ -370,13 +370,13 @@ pub fn status_bar(app: &mut App, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
static LATENCY: std::sync::Mutex<Option<(String, Result<i64, String>)>> = std::sync::Mutex::new(None);
|
||||
static LATENCY: parking_lot::Mutex<Option<(String, Result<i64, String>)>> = parking_lot::Mutex::new(None);
|
||||
|
||||
/// Pings the active SSH session every 10 s for the status-bar latency.
|
||||
fn latency_tick(app: &mut App) {
|
||||
let id = app.s.active.clone();
|
||||
let ssh = id.as_deref().and_then(|a| app.s.pane(a)).is_some_and(|p| p.info.protocol == Protocol::Ssh);
|
||||
if let Some(r) = LATENCY.lock().unwrap().take() {
|
||||
if let Some(r) = LATENCY.lock().take() {
|
||||
app.latency = Some(r);
|
||||
}
|
||||
let Some(id) = id.filter(|_| ssh) else { return };
|
||||
@@ -395,7 +395,7 @@ fn latency_tick(app: &mut App) {
|
||||
let (svc, ctx) = (app.s.svc.clone(), app.s.ctx.clone());
|
||||
std::thread::spawn(move || {
|
||||
let r = svc.sessions.ping(&id).map_err(|e| e.to_string());
|
||||
*LATENCY.lock().unwrap() = Some((id, r));
|
||||
*LATENCY.lock() = Some((id, r));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::widgets;
|
||||
use egui::{Align, Key, Layout, RichText};
|
||||
use egui_phosphor::regular as icon;
|
||||
use nexus_core::models::{ForwardDirection, PortForward, new_id};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Form {
|
||||
remote: bool,
|
||||
@@ -49,7 +50,7 @@ impl Forwards {
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ctx: &egui::Context, s: &mut Shared) {
|
||||
for started in std::mem::take(&mut *self.done.lock().unwrap()) {
|
||||
for started in std::mem::take(&mut *self.done.lock()) {
|
||||
if started {
|
||||
s.success("Forward started");
|
||||
self.form.bind_port.clear();
|
||||
@@ -131,7 +132,7 @@ impl Forwards {
|
||||
|
||||
if let Some(id) = stop {
|
||||
let (svc, sid, done) = (s.svc.clone(), sid.clone(), self.done.clone());
|
||||
s.spawn(move || svc.sessions.stop_forward(&sid, &id).map_err(|e| format!("Failed to stop: {e}").into()), move |_| done.lock().unwrap().push(false));
|
||||
s.spawn(move || svc.sessions.stop_forward(&sid, &id).map_err(|e| format!("Failed to stop: {e}").into()), move |_| done.lock().push(false));
|
||||
}
|
||||
if start {
|
||||
let f = &self.form;
|
||||
@@ -153,7 +154,7 @@ impl Forwards {
|
||||
let (svc, done) = (s.svc.clone(), self.done.clone());
|
||||
s.spawn(
|
||||
move || svc.sessions.start_forward(&sid, rule).map_err(|e| format!("Failed to start forward: {e}").into()),
|
||||
move |_| done.lock().unwrap().push(true),
|
||||
move |_| done.lock().push(true),
|
||||
);
|
||||
}
|
||||
_ => s.error("Bind port and destination port are required"),
|
||||
|
||||
@@ -8,7 +8,8 @@ use chrono::{DateTime, Utc};
|
||||
use egui::{Color32, RichText, Sense, Ui, vec2};
|
||||
use egui_phosphor::regular as icon;
|
||||
use nexus_core::models::GitHubRepo;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
enum Msg {
|
||||
Repos(Vec<GitHubRepo>),
|
||||
@@ -175,7 +176,7 @@ impl GithubView {
|
||||
Ok(r) => Msg::Repos(r),
|
||||
Err(e) => Msg::RefreshFailed(e.to_string()),
|
||||
};
|
||||
inbox.lock().unwrap().push(msg);
|
||||
inbox.lock().push(msg);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
@@ -193,13 +194,13 @@ impl GithubView {
|
||||
Ok(()) => Msg::Downloaded(full, path.display().to_string()),
|
||||
Err(e) => Msg::DownloadFailed(e.to_string()),
|
||||
};
|
||||
inbox.lock().unwrap().push(msg);
|
||||
inbox.lock().push(msg);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui, s: &mut Shared) {
|
||||
let msgs = std::mem::take(&mut *self.inbox.lock().unwrap());
|
||||
let msgs = std::mem::take(&mut *self.inbox.lock());
|
||||
for m in msgs {
|
||||
match m {
|
||||
Msg::Repos(r) => {
|
||||
|
||||
@@ -33,6 +33,16 @@ use nexus_core::service::Service;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
// A panic on a worker thread (a session's reader, a transfer) ends only that thread; say so in
|
||||
// the UI rather than letting the session just stop.
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
default_hook(info);
|
||||
let what =
|
||||
info.payload().downcast_ref::<&str>().copied().or_else(|| info.payload().downcast_ref::<String>().map(String::as_str)).unwrap_or("unknown error");
|
||||
let thread = std::thread::current().name().unwrap_or("worker").to_string();
|
||||
app::post_error_nonblocking(format!("Internal error in {thread} thread: {what}. Other sessions are unaffected; reconnect the affected one."));
|
||||
}));
|
||||
let mut opts = app::Options {
|
||||
screenshot: None,
|
||||
theme: None,
|
||||
|
||||
@@ -11,9 +11,10 @@ use nexus_core::clients::serial;
|
||||
use nexus_core::keyfiles;
|
||||
use nexus_core::models::*;
|
||||
use nexus_core::validation::{ValidationError, validate_profile};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
type KeyChecks = HashMap<String, Result<&'static str, &'static str>>;
|
||||
|
||||
@@ -184,7 +185,7 @@ impl ProfileEditor {
|
||||
/// Drawn inside the right dock while open.
|
||||
pub fn ui(&mut self, ui: &mut Ui, s: &mut Shared) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
if let Some(r) = self.test.lock().unwrap().take() {
|
||||
if let Some(r) = self.test.lock().take() {
|
||||
self.testing = false;
|
||||
match r {
|
||||
Ok(ms) => s.success(format!("Reachable — {ms} ms")),
|
||||
@@ -336,7 +337,7 @@ impl ProfileEditor {
|
||||
let (slot, svc, ctx) = (self.test.clone(), s.svc.clone(), s.ctx.clone());
|
||||
std::thread::spawn(move || {
|
||||
let r = svc.test_connection(&host, port).map_err(|e| e.to_string());
|
||||
*slot.lock().unwrap() = Some(r);
|
||||
*slot.lock() = Some(r);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ use egui::{RichText, Ui, vec2};
|
||||
use egui_phosphor::regular as icon;
|
||||
use nexus_core::models::{AppPreferences, KnownHost, Protocol};
|
||||
use nexus_core::updater::UpdateInfo;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A result from a worker thread, polled each frame (`Some` while the work is in flight).
|
||||
pub struct Job<T>(Arc<Mutex<Option<T>>>);
|
||||
@@ -20,14 +21,14 @@ impl<T: Send + 'static> Job<T> {
|
||||
let (out, ctx) = (slot.clone(), ctx.clone());
|
||||
std::thread::spawn(move || {
|
||||
let v = f();
|
||||
*out.lock().unwrap() = Some(v);
|
||||
*out.lock() = Some(v);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
Job(slot)
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Option<T> {
|
||||
self.0.lock().unwrap().take()
|
||||
self.0.lock().take()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ use nexus_core::clients::sftp::{self as rsftp, FileEntry, Progress};
|
||||
use nexus_core::keyfiles::safe_sftp_upload_name;
|
||||
use nexus_core::models::Protocol;
|
||||
use nexus_core::service::Service;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Worker results, applied on the UI thread.
|
||||
enum Done {
|
||||
@@ -47,7 +48,7 @@ struct Inbox(Arc<Mutex<Vec<Done>>>, egui::Context);
|
||||
|
||||
impl Inbox {
|
||||
fn push(&self, d: Done) {
|
||||
self.0.lock().unwrap().push(d);
|
||||
self.0.lock().push(d);
|
||||
self.1.request_repaint();
|
||||
}
|
||||
}
|
||||
@@ -250,7 +251,7 @@ impl Sftp {
|
||||
let tr = self.transfers.remove(i);
|
||||
if self.open_for.as_deref() != Some(session) {
|
||||
let verb = if tr.upload { "↑ Uploaded" } else { "↓ Downloaded" };
|
||||
self.inbox.lock().unwrap().push(Done::Toast(format!("{verb} {}", base_name(&tr.path))));
|
||||
self.inbox.lock().push(Done::Toast(format!("{verb} {}", base_name(&tr.path))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +270,7 @@ impl Sftp {
|
||||
}
|
||||
|
||||
fn drain(&mut self, s: &mut Shared) {
|
||||
let items = std::mem::take(&mut *self.inbox.lock().unwrap());
|
||||
let items = std::mem::take(&mut *self.inbox.lock());
|
||||
for d in items {
|
||||
match d {
|
||||
Done::Listed { session, path, files } => {
|
||||
|
||||
@@ -12,7 +12,8 @@ use alacritty_terminal::term::test::TermSize;
|
||||
use alacritty_terminal::term::{Config, Term, TermMode};
|
||||
use alacritty_terminal::vte::ansi::{self, Color, CursorShape, NamedColor, Processor, StdSyncHandler};
|
||||
use egui::{Color32, Event, FontId, Key, Modifiers, PointerButton, Pos2, Rect, Sense, Stroke, Ui, pos2, vec2};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Listener(Arc<Mutex<Vec<TermEvent>>>);
|
||||
@@ -20,7 +21,7 @@ struct Listener(Arc<Mutex<Vec<TermEvent>>>);
|
||||
impl EventListener for Listener {
|
||||
fn send_event(&self, e: TermEvent) {
|
||||
if matches!(e, TermEvent::PtyWrite(_) | TermEvent::Title(_) | TermEvent::Bell | TermEvent::ClipboardStore(..)) {
|
||||
self.0.lock().unwrap().push(e);
|
||||
self.0.lock().push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +295,7 @@ impl TermView {
|
||||
self.mouse(ui, &resp, origin, cw, ch, &mut out);
|
||||
|
||||
// Terminal-originated events.
|
||||
for e in std::mem::take(&mut *self.events.0.lock().unwrap()) {
|
||||
for e in std::mem::take(&mut *self.events.0.lock()) {
|
||||
match e {
|
||||
TermEvent::PtyWrite(s) => out.reply.extend_from_slice(s.as_bytes()),
|
||||
TermEvent::Title(t) => out.title = Some(t),
|
||||
@@ -918,7 +919,7 @@ mod tests {
|
||||
fn replies_to_device_attributes() {
|
||||
let mut t = TermView::new(10, 14.0, CursorStyle::Block);
|
||||
t.feed(b"\x1b[6n");
|
||||
let ev = t.events.0.lock().unwrap();
|
||||
let ev = t.events.0.lock();
|
||||
assert!(ev.iter().any(|e| matches!(e, TermEvent::PtyWrite(s) if s == "\x1b[1;1R")));
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ impl VncView {
|
||||
fn upload(&mut self, ui: &Ui) {
|
||||
let Some(c) = &self.client else { return };
|
||||
let fb = c.framebuffer();
|
||||
let mut fb = fb.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut fb = fb.lock();
|
||||
if !fb.dirty || fb.width == 0 || fb.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user