fix(gui,core): ask the mount off the main thread when opening, reap children

Opening a file or a location stat'ed the mount on the GTK main thread, so
a hung mount hung the app. The browser now uses the launcher's bounded
worker check (`present_within`, 2 s), and opening a location runs on a
thread. Every child the GUI starts is also waited for on a thread, so it
no longer stays a zombie (B243).
This commit is contained in:
nvrl committed 2026-10-16 12:07:48 +02:00
1 parent 6ec8043934
commit 48c8910e7b
8 files changed
+137 -72

No files matched your search

+9 -3
View File
@@ -297,7 +297,10 @@ fn present_window(app: &adw::Application) {
/// Spawn the tray icon process in the background.
pub(crate) fn spawn_tray() {
match Command::new("pdfs-tray").spawn() {
Ok(_) => tracing::info!("spawned `pdfs-tray`"),
Ok(child) => {
tracing::info!("spawned `pdfs-tray`");
pdfs_core::opener::reap(child);
}
Err(e) => tracing::error!("failed to spawn `pdfs-tray`: {e}"),
}
}
@@ -1713,9 +1716,12 @@ pub(crate) fn flash_copied(button: &gtk4::Button, icon: &'static str) {
/// Open a local path with the handler the user configured — `xdg-open` unless
/// `open_with` in `config.json` overrides it for this kind of file.
///
/// On a thread of its own: telling a folder from a file is a `stat`, and the
/// path is often in the mount, which may hang.
fn open_path(path: &str) {
let path = Path::new(path);
pdfs_core::opener::open_default(path, path.is_dir());
let path = PathBuf::from(path);
std::thread::spawn(move || pdfs_core::opener::open_default(&path, path.is_dir()));
}
/// [`open_path`] for a file the daemon materialised into the content cache,
+34 -33
View File
@@ -1,4 +1,4 @@
use crate::activation::{DriveActivation, drive_activation, mounted_path, mounted_target_rel};
use crate::activation::{DriveActivation, drive_activation, mounted_path, present_within};
use crate::*;
pub(crate) struct BrowserState {
@@ -983,28 +983,27 @@ pub(crate) fn background_context_menu(ui: &Rc<Ui>) -> ActionMenu {
pub(crate) fn open_entry_with(ui: &Rc<Ui>, entry: &DirEntry) {
let rel = entry_rel(ui, entry);
let mountpoint = ui.dirs.resolved_mountpoint(&ui.dirs.load_config());
let Some(path) = mounted_target_rel(&mountpoint, &rel) else {
toast_error(
ui,
&gettext("Couldn't open file"),
&gettext("Open With needs the Proton Drive service to be running."),
);
return;
};
let launcher = gtk4::FileLauncher::new(Some(&gio::File::for_path(path)));
launcher.set_always_ask(true);
let ui = ui.clone();
launcher.launch(
ui_window(&ui).as_ref(),
gio::Cancellable::NONE,
move |result| {
glib::spawn_future_local(async move {
let Some(path) = present_within(mounted_path(&mountpoint, &rel)).await else {
toast_error(
&ui,
&gettext("Couldn't open file"),
&gettext("Open With needs the Proton Drive service to be running."),
);
return;
};
let launcher = gtk4::FileLauncher::new(Some(&gio::File::for_path(path)));
launcher.set_always_ask(true);
let window = ui_window(&ui);
launcher.launch(window.as_ref(), gio::Cancellable::NONE, move |result| {
if let Err(e) = result
&& !e.matches(gtk4::DialogError::Dismissed)
{
toast_error(&ui, &gettext("Couldn't open file"), &e.to_string());
}
},
);
});
});
}
/// Copy the entry's public link, when it has one this client can read the
@@ -1112,27 +1111,29 @@ pub(crate) fn activate_entry(ui: &Rc<Ui>, entry: &DirEntry) {
/// [`mounted_target`](crate::activation::mounted_target).
pub(crate) fn download_and_open(ui: &Rc<Ui>, entry: &DirEntry) {
let rel = entry_rel(ui, entry);
let mountpoint = ui.dirs.resolved_mountpoint(&ui.dirs.load_config());
if let Some(path) = mounted_target_rel(&mountpoint, &rel) {
open_path(&path.to_string_lossy());
return;
}
// Ignore a repeat activation of a file already downloading, so an impatient
// double-click doesn't kick off a second round-trip.
// Ignore a repeat activation of a file already opening, so an impatient
// double-click doesn't open it twice or kick off a second round-trip.
if !ui.opening.borrow_mut().insert(rel.clone()) {
return;
}
ui.busy_begin();
let name = entry.name.clone();
let rx = spawn_request(
ui.dirs.control_socket(),
Request::OpenFile {
path: rel.clone(),
uid: Some(entry.uid.clone()),
},
);
let mountpoint = ui.dirs.resolved_mountpoint(&ui.dirs.load_config());
let (name, uid) = (entry.name.clone(), entry.uid.clone());
let ui = ui.clone();
glib::spawn_future_local(async move {
// Asked off the main thread: a mount that hangs used to hang the app.
if let Some(path) = present_within(mounted_path(&mountpoint, &rel)).await {
ui.opening.borrow_mut().remove(&rel);
open_path(&path.to_string_lossy());
return;
}
ui.busy_begin();
let rx = spawn_request(
ui.dirs.control_socket(),
Request::OpenFile {
path: rel.clone(),
uid: Some(uid),
},
);
let result = rx.recv().await;
ui.busy_end();
ui.opening.borrow_mut().remove(&rel);
+3 -3
View File
@@ -3481,10 +3481,10 @@ pub(crate) fn gallery_unreachable(ui: &Rc<Ui>) {
/// right tool for the HEVC `.mkv`s this is aimed at — and falls back to the
/// user's default handler when mpv isn't installed.
pub(crate) fn play_external(path: &str) {
if Command::new("mpv").arg(path).spawn().is_ok() {
return;
match Command::new("mpv").arg(path).spawn() {
Ok(child) => pdfs_core::opener::reap(child),
Err(_) => open_path(path),
}
open_path(path);
}
#[cfg(test)]