mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
implemented robust fuse mount
This commit is contained in:
1 parent
ca30dceae7
commit
99e80a7206
8 files changed
+276
-99
No files matched your search
@@ -38,6 +38,13 @@ enum Command {
|
||||
/// Mountpoint; defaults to ~/ProtonDrive.
|
||||
mountpoint: Option<PathBuf>,
|
||||
},
|
||||
/// Run the auto-mount daemon: wait for login, mount, and keep it mounted
|
||||
/// until stopped. This is what the systemd user service runs; you normally
|
||||
/// don't invoke it by hand.
|
||||
Daemon {
|
||||
/// Mountpoint; defaults to ~/ProtonDrive.
|
||||
mountpoint: Option<PathBuf>,
|
||||
},
|
||||
/// Keep a file on this device (download + cache it). Needs a running mount.
|
||||
Pin {
|
||||
/// File path, inside the mountpoint or relative to it.
|
||||
@@ -88,6 +95,7 @@ fn main() -> Result<()> {
|
||||
Command::Logout => cmd_logout(),
|
||||
Command::Status => cmd_status(),
|
||||
Command::Mount { mountpoint } => cmd_mount(mountpoint),
|
||||
Command::Daemon { mountpoint } => cmd_daemon(mountpoint),
|
||||
Command::Pin { path } => cmd_pin(path),
|
||||
Command::Unpin { path } => cmd_unpin(path),
|
||||
Command::Pins => cmd_pins(),
|
||||
@@ -158,6 +166,50 @@ fn cmd_status() -> Result<()> {
|
||||
}
|
||||
|
||||
fn cmd_mount(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
mount_once(mountpoint)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the auto-mount daemon loop. Waits for a stored login, mounts, and keeps
|
||||
/// the mount alive: a clean stop (SIGTERM via `systemctl --user stop`) exits 0;
|
||||
/// an external unmount triggers a remount; errors back off and retry. This is
|
||||
/// the entry point for the systemd user service.
|
||||
fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
loop {
|
||||
// Wait until a session is stored. The GUI enables this service on login,
|
||||
// but the service may also start at boot before the user has logged in.
|
||||
loop {
|
||||
match auth::load() {
|
||||
Ok(_) => break,
|
||||
Err(pdfs_core::Error::NotLoggedIn) => {
|
||||
tracing::info!("not logged in; waiting…");
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
match mount_once(mountpoint.clone()) {
|
||||
Ok(pdfs_fuse::MountOutcome::Shutdown) => {
|
||||
tracing::info!("daemon stopping");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(pdfs_fuse::MountOutcome::Unmounted) => {
|
||||
tracing::warn!("mount ended externally; remounting in 2s");
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "mount failed; retrying in 5s");
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One mount run: resume the session, mount, and block until the mount ends
|
||||
/// (clean shutdown or external unmount). Returns the [`MountOutcome`] so the
|
||||
/// daemon loop can decide whether to exit or remount.
|
||||
fn mount_once(mountpoint: Option<PathBuf>) -> Result<pdfs_fuse::MountOutcome> {
|
||||
let dirs = AppDirs::new()?;
|
||||
dirs.ensure()?;
|
||||
let mountpoint = mountpoint.unwrap_or_else(|| dirs.default_mountpoint());
|
||||
@@ -230,8 +282,8 @@ fn cmd_mount(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
tracing::warn!(error = %e, "failed to persist tokens on shutdown");
|
||||
}
|
||||
|
||||
result.context("mount failed")?;
|
||||
Ok(())
|
||||
let outcome = result.context("mount failed")?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
fn cmd_pin(path: PathBuf) -> Result<()> {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod cache;
|
||||
pub mod config;
|
||||
pub mod control;
|
||||
pub mod error;
|
||||
pub mod service;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Thin wrappers over `systemctl --user` for the auto-mount daemon.
|
||||
//!
|
||||
//! The mount is owned by a systemd *user* service (`proton-drive.service`, see
|
||||
//! `packaging/`). Front-ends never spawn `pdfs mount` directly anymore: logging
|
||||
//! in enables+starts the unit, logging out disables+stops it, and the tray's
|
||||
//! "Disconnect" just stops it. systemd then handles restart-on-failure and a
|
||||
//! clean SIGTERM stop (which the daemon turns into a lazy unmount).
|
||||
//!
|
||||
//! Every call is best-effort: a failure is logged but not fatal, since the user
|
||||
//! can always drive `systemctl --user` by hand.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// The systemd user unit that runs `pdfs daemon`.
|
||||
pub const SERVICE: &str = "proton-drive.service";
|
||||
|
||||
/// Run one `systemctl --user <args> proton-drive.service`, returning whether it
|
||||
/// succeeded. Output is suppressed; failures are logged.
|
||||
fn systemctl(args: &[&str]) -> bool {
|
||||
let ok = Command::new("systemctl")
|
||||
.arg("--user")
|
||||
.args(args)
|
||||
.arg(SERVICE)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if !ok {
|
||||
tracing::warn!(args = ?args, "systemctl --user {SERVICE} failed");
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
/// Enable the service so it survives reboots/logins and start it now. Called on
|
||||
/// successful login.
|
||||
pub fn enable_start() -> bool {
|
||||
systemctl(&["enable", "--now"])
|
||||
}
|
||||
|
||||
/// Stop the running mount without disabling it. Called by the tray's
|
||||
/// "Disconnect": the next login (or reboot) brings it back.
|
||||
pub fn stop() -> bool {
|
||||
systemctl(&["stop"])
|
||||
}
|
||||
|
||||
/// Disable the service so it no longer auto-starts, and stop it now. Called on
|
||||
/// logout.
|
||||
pub fn disable_stop() -> bool {
|
||||
systemctl(&["disable", "--now"])
|
||||
}
|
||||
+115
-11
@@ -1667,12 +1667,53 @@ fn run_control_socket(core: Core, username: String, mountpoint: PathBuf, listene
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount the filesystem at `mountpoint` and block until it is unmounted.
|
||||
/// Why a [`mount`] call returned. Lets the daemon decide whether to exit (clean
|
||||
/// shutdown) or remount (the mount went away under it).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MountOutcome {
|
||||
/// The daemon was asked to stop (SIGTERM/SIGINT) and we lazily unmounted
|
||||
/// ourselves. The caller should exit cleanly.
|
||||
Shutdown,
|
||||
/// The kernel mount ended on its own (e.g. an external `fusermount -u`).
|
||||
/// The caller may want to remount.
|
||||
Unmounted,
|
||||
}
|
||||
|
||||
/// Best-effort teardown of a stale mount left behind by a crashed daemon. A
|
||||
/// previous run that died without unmounting leaves the kernel mount in place,
|
||||
/// so the fresh `Session::new` below would fail with EBUSY ("Device or resource
|
||||
/// busy"). `fusermount3 -u -z` is the lazy (detach) unmount, which succeeds even
|
||||
/// when the old mount is still busy. Swallow all output/errors: if there is no
|
||||
/// stale mount this is simply a no-op.
|
||||
fn clear_stale_mount(mountpoint: &Path) {
|
||||
for bin in ["fusermount3", "fusermount"] {
|
||||
let ok = std::process::Command::new(bin)
|
||||
.arg("-u")
|
||||
.arg("-z")
|
||||
.arg(mountpoint)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
info!(mountpoint = %mountpoint.display(), "cleared stale mount before remount");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount the filesystem at `mountpoint` and block until it is unmounted or the
|
||||
/// daemon is asked to stop.
|
||||
///
|
||||
/// Fetches the My Files root up front (so an auth/network failure surfaces
|
||||
/// before the kernel mount), then spawns the Phase 2 event-sync task, the
|
||||
/// Phase 4 control socket, and takes over the calling thread in the FUSE session
|
||||
/// loop. `rt` must be a handle to a *running* multi-threaded runtime.
|
||||
/// Phase 4 control socket, and runs the FUSE session on its own thread while
|
||||
/// this thread waits for either a stop signal (SIGTERM/SIGINT) or the kernel
|
||||
/// mount ending on its own. On a stop signal we lazily unmount ourselves
|
||||
/// (`umount_and_join`, the MNT_DETACH path that succeeds even while downloads
|
||||
/// are in flight), so `systemctl --user stop` is always a clean teardown.
|
||||
/// `rt` must be a handle to a *running* multi-threaded runtime.
|
||||
pub fn mount(
|
||||
client: ProtonDriveClient,
|
||||
rt: tokio::runtime::Handle,
|
||||
@@ -1680,7 +1721,7 @@ pub fn mount(
|
||||
cache: ContentCache,
|
||||
control_socket: &Path,
|
||||
username: String,
|
||||
) -> std::io::Result<()> {
|
||||
) -> std::io::Result<MountOutcome> {
|
||||
let root = rt
|
||||
.block_on(client.get_my_files_folder())
|
||||
.map_err(|e| std::io::Error::other(format!("fetch My Files root: {e}")))?;
|
||||
@@ -1707,6 +1748,10 @@ pub fn mount(
|
||||
MountOption::Subtype("protondrive".to_string()),
|
||||
MountOption::DefaultPermissions,
|
||||
];
|
||||
|
||||
// A crashed previous run can leave the kernel mount in place, which makes
|
||||
// the fresh mount below fail with EBUSY. Lazily detach any leftover first.
|
||||
clear_stale_mount(mountpoint);
|
||||
info!(mountpoint = %mountpoint.display(), "mounting Proton Drive");
|
||||
|
||||
// Bind the control socket before the FUSE session takes over the thread. A
|
||||
@@ -1725,16 +1770,75 @@ pub fn mount(
|
||||
let fs = ProtonFs::new(core.clone(), root);
|
||||
|
||||
// Build the session explicitly (not `mount2`) so we can grab a `Notifier`
|
||||
// for the event task. `spawn` runs the session loop on its own thread;
|
||||
// `join` then blocks here until the filesystem is unmounted, preserving
|
||||
// `mount`'s blocking contract.
|
||||
let session = Session::new(fs, mountpoint, &config)?.spawn()?;
|
||||
let notifier = session.notifier();
|
||||
// for the event task. `spawn` runs the session loop on its own background
|
||||
// thread; we then wait here for either a stop signal or the mount ending.
|
||||
let bg = Session::new(fs, mountpoint, &config)?.spawn()?;
|
||||
let notifier = bg.notifier();
|
||||
rt.spawn(run_event_sync(
|
||||
client, scope, core.state, core.cache, notifier,
|
||||
));
|
||||
|
||||
let result = session.join();
|
||||
// Stop signals (SIGTERM from `systemctl --user stop`, SIGINT from Ctrl-C)
|
||||
// are delivered onto the async runtime; bridge them onto a sync channel so
|
||||
// the loop below can react without blocking a worker thread. A bounded
|
||||
// channel of 1 is enough — we only need to know that *a* stop arrived.
|
||||
let (sig_tx, sig_rx) = std::sync::mpsc::sync_channel::<()>(1);
|
||||
rt.spawn(async move {
|
||||
let mut sigterm = match tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::terminate(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "install SIGTERM handler failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut sigint = match tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::interrupt(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "install SIGINT handler failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
_ = sigterm.recv() => info!("received SIGTERM"),
|
||||
_ = sigint.recv() => info!("received SIGINT"),
|
||||
}
|
||||
let _ = sig_tx.try_send(());
|
||||
});
|
||||
|
||||
// Wait for whichever happens first: a stop signal (→ we unmount ourselves
|
||||
// via the lazy MNT_DETACH path, clean even mid-download), or the kernel
|
||||
// mount ending on its own (→ the session thread finishes). Poll instead of
|
||||
// blocking on `join` so we can also notice the signal.
|
||||
let outcome = loop {
|
||||
match sig_rx.recv_timeout(Duration::from_millis(500)) {
|
||||
Ok(()) => {
|
||||
info!("stop requested; unmounting");
|
||||
if let Err(e) = bg.umount_and_join() {
|
||||
warn!(error = %e, "umount_and_join failed");
|
||||
}
|
||||
break MountOutcome::Shutdown;
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
if bg.guard.is_finished() {
|
||||
info!("mount ended externally");
|
||||
if let Err(e) = bg.join() {
|
||||
warn!(error = %e, "session join failed");
|
||||
}
|
||||
break MountOutcome::Unmounted;
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
// Signal task gone (failed to install); fall back to join.
|
||||
let _ = bg.join();
|
||||
break MountOutcome::Unmounted;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _ = std::fs::remove_file(control_socket);
|
||||
result
|
||||
Ok(outcome)
|
||||
}
|
||||
+19
-57
@@ -6,8 +6,8 @@
|
||||
//! Google-Drive-style layout:
|
||||
//!
|
||||
//! * a **login form** (email / password / optional 2FA) driving
|
||||
//! [`pdfs_core::auth::login`];
|
||||
//! * a **mount toggle** that spawns `pdfs mount` / `fusermount3 -u`;
|
||||
//! [`pdfs_core::auth::login`]; logging in enables the systemd mount service;
|
||||
//! * a **read-only mount status** line (the mount is managed automatically);
|
||||
//! * a **cache usage** read-out from the on-disk [`ContentCache`]; and
|
||||
//! * the **pin list** with per-file unpin.
|
||||
//!
|
||||
@@ -25,12 +25,13 @@ use std::time::Duration;
|
||||
use adw::prelude::*;
|
||||
use gtk4::gio;
|
||||
use gtk4::glib;
|
||||
use gtk4::glib::{BoxedAnyObject, SignalHandlerId};
|
||||
use gtk4::glib::BoxedAnyObject;
|
||||
|
||||
use pdfs_core::auth;
|
||||
use pdfs_core::cache::ContentCache;
|
||||
use pdfs_core::config::{AppDirs, DEFAULT_CACHE_BUDGET_BYTES};
|
||||
use pdfs_core::control::{DirEntry, PhotoItem, Request, Response, send};
|
||||
use pdfs_core::service;
|
||||
|
||||
/// How many photos to pull per [`Request::PhotosTimeline`] page.
|
||||
const PHOTOS_PAGE: usize = 60;
|
||||
@@ -56,8 +57,9 @@ struct Ui {
|
||||
login_status: gtk4::Label,
|
||||
// Main page.
|
||||
account_row: adw::ActionRow,
|
||||
mount_row: adw::SwitchRow,
|
||||
mount_handler: RefCell<Option<SignalHandlerId>>,
|
||||
/// Read-only mount status line. The mount is driven by the systemd user
|
||||
/// service (enabled on login), not by the user — so this only reports.
|
||||
mount_row: adw::ActionRow,
|
||||
cache_bar: gtk4::ProgressBar,
|
||||
cache_label: gtk4::Label,
|
||||
pins_group: adw::PreferencesGroup,
|
||||
@@ -177,7 +179,6 @@ fn build_window(app: &adw::Application) {
|
||||
login_status: login_widgets.4,
|
||||
account_row: main_widgets.0,
|
||||
mount_row: main_widgets.1,
|
||||
mount_handler: RefCell::new(None),
|
||||
cache_bar: main_widgets.2,
|
||||
cache_label: main_widgets.3,
|
||||
pins_group: main_widgets.4,
|
||||
@@ -196,7 +197,6 @@ fn build_window(app: &adw::Application) {
|
||||
|
||||
wire_login(&ui);
|
||||
wire_logout(&ui, &main_widgets.5);
|
||||
wire_mount_switch(&ui);
|
||||
wire_browser(&ui, &browser_widgets.4, &browser_widgets.5);
|
||||
wire_gallery(&ui, &gallery_widgets.3);
|
||||
|
||||
@@ -331,7 +331,7 @@ fn build_main_page() -> (
|
||||
gtk4::Widget,
|
||||
(
|
||||
adw::ActionRow,
|
||||
adw::SwitchRow,
|
||||
adw::ActionRow,
|
||||
gtk4::ProgressBar,
|
||||
gtk4::Label,
|
||||
adw::PreferencesGroup,
|
||||
@@ -351,10 +351,11 @@ fn build_main_page() -> (
|
||||
account_row.add_suffix(&logout_button);
|
||||
account_group.add(&account_row);
|
||||
|
||||
// Mount group: a switch row driving the FUSE mount.
|
||||
// Mount group: a read-only status line. The mount is managed automatically
|
||||
// by the systemd user service; there is no toggle to fiddle with.
|
||||
let mount_group = adw::PreferencesGroup::builder().title("Drive").build();
|
||||
let mount_row = adw::SwitchRow::builder()
|
||||
.title("Mount Proton Drive")
|
||||
let mount_row = adw::ActionRow::builder()
|
||||
.title("Proton Drive")
|
||||
.subtitle("Not mounted")
|
||||
.build();
|
||||
mount_group.add(&mount_row);
|
||||
@@ -439,6 +440,8 @@ fn wire_login(ui: &Rc<Ui>) {
|
||||
ui.login_status.set_text("");
|
||||
ui.password.set_text("");
|
||||
ui.totp.set_text("");
|
||||
// Enable+start the mount service now that we have a session.
|
||||
service::enable_start();
|
||||
refresh(&ui);
|
||||
}
|
||||
Err(e) => ui.login_status.set_text(&format!("Sign-in failed: {e}")),
|
||||
@@ -485,14 +488,13 @@ fn spawn_login(
|
||||
rx
|
||||
}
|
||||
|
||||
/// Connect the sign-out button: forget the stored session and drop back to the
|
||||
/// login page. Unmounts first so the daemon isn't left without credentials.
|
||||
/// Connect the sign-out button: disable+stop the mount service (so the daemon
|
||||
/// isn't left running without credentials), forget the stored session, and drop
|
||||
/// back to the login page.
|
||||
fn wire_logout(ui: &Rc<Ui>, button: >k4::Button) {
|
||||
let ui = ui.clone();
|
||||
button.connect_clicked(move |_| {
|
||||
if *ui.mounted.borrow() {
|
||||
unmount(&ui.dirs.default_mountpoint());
|
||||
}
|
||||
service::disable_stop();
|
||||
if let Err(e) = auth::logout() {
|
||||
tracing::error!("logout failed: {e}");
|
||||
}
|
||||
@@ -500,22 +502,6 @@ fn wire_logout(ui: &Rc<Ui>, button: >k4::Button) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Connect the mount switch. The handler id is stored so the refresh loop can
|
||||
/// block it while syncing the switch to the real mount state (otherwise setting
|
||||
/// `active` would re-trigger a mount/unmount).
|
||||
fn wire_mount_switch(ui: &Rc<Ui>) {
|
||||
let ui_for_handler = ui.clone();
|
||||
let handler = ui.mount_row.connect_active_notify(move |row| {
|
||||
let mountpoint = ui_for_handler.dirs.default_mountpoint();
|
||||
if row.is_active() {
|
||||
spawn_mount();
|
||||
} else {
|
||||
unmount(&mountpoint);
|
||||
}
|
||||
});
|
||||
*ui.mount_handler.borrow_mut() = Some(handler);
|
||||
}
|
||||
|
||||
/// Re-read mount status, login identity, cache usage and the pin list, and
|
||||
/// repaint the window. Cheap, synchronous local reads — safe on the main loop.
|
||||
fn refresh(ui: &Rc<Ui>) {
|
||||
@@ -547,12 +533,7 @@ fn refresh(ui: &Rc<Ui>) {
|
||||
};
|
||||
*ui.mounted.borrow_mut() = mount.is_some();
|
||||
|
||||
// Sync the switch without re-triggering its handler.
|
||||
if let Some(id) = ui.mount_handler.borrow().as_ref() {
|
||||
ui.mount_row.block_signal(id);
|
||||
ui.mount_row.set_active(mount.is_some());
|
||||
ui.mount_row.unblock_signal(id);
|
||||
}
|
||||
// Read-only status: the service mounts automatically, we only report.
|
||||
ui.mount_row.set_subtitle(&match &mount {
|
||||
Some(mp) => format!("Mounted at {mp}"),
|
||||
None => "Not mounted".into(),
|
||||
@@ -1299,25 +1280,6 @@ fn open_path(path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch `pdfs mount` detached so the window keeps running independently.
|
||||
fn spawn_mount() {
|
||||
match Command::new("pdfs").arg("mount").spawn() {
|
||||
Ok(_) => tracing::info!("spawned `pdfs mount`"),
|
||||
Err(e) => tracing::error!("failed to spawn `pdfs mount`: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tear down the FUSE mount. `fusermount3 -u` is the native path; fall back to
|
||||
/// `fusermount` on older systems.
|
||||
fn unmount(mountpoint: &Path) {
|
||||
let try_unmount = |bin: &str| Command::new(bin).arg("-u").arg(mountpoint).status();
|
||||
match try_unmount("fusermount3").or_else(|_| try_unmount("fusermount")) {
|
||||
Ok(s) if s.success() => tracing::info!("unmounted {}", mountpoint.display()),
|
||||
Ok(s) => tracing::error!("unmount exited with {s}"),
|
||||
Err(e) => tracing::error!("failed to run fusermount: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a byte count as a short binary-unit string (e.g. `1.2 GiB`).
|
||||
fn human_bytes(bytes: u64) -> String {
|
||||
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
|
||||
+12
-29
@@ -19,6 +19,7 @@ use ksni::{MenuItem, Tray, TrayService};
|
||||
use pdfs_core::auth;
|
||||
use pdfs_core::config::AppDirs;
|
||||
use pdfs_core::control::{Request, Response, send};
|
||||
use pdfs_core::service;
|
||||
|
||||
/// How often the tray re-polls the daemon to refresh its menu.
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(3);
|
||||
@@ -71,26 +72,6 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch `pdfs mount` detached so the tray keeps running independently.
|
||||
fn spawn_mount() {
|
||||
match Command::new("pdfs").arg("mount").spawn() {
|
||||
Ok(_) => tracing::info!("spawned `pdfs mount`"),
|
||||
Err(e) => tracing::error!("failed to spawn `pdfs mount`: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unmount the FUSE filesystem. `fusermount3 -u` is the FUSE-native teardown;
|
||||
/// fall back to `fusermount` on older systems.
|
||||
fn unmount(mountpoint: &Path) {
|
||||
let try_unmount = |bin: &str| Command::new(bin).arg("-u").arg(mountpoint).status();
|
||||
let result = try_unmount("fusermount3").or_else(|_| try_unmount("fusermount"));
|
||||
match result {
|
||||
Ok(s) if s.success() => tracing::info!("unmounted {}", mountpoint.display()),
|
||||
Ok(s) => tracing::error!("unmount exited with {s}"),
|
||||
Err(e) => tracing::error!("failed to run fusermount: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_folder(mountpoint: &Path) {
|
||||
if let Err(e) = Command::new("xdg-open").arg(mountpoint).spawn() {
|
||||
tracing::error!("failed to xdg-open {}: {e}", mountpoint.display());
|
||||
@@ -111,9 +92,8 @@ impl Tray for DriveTray {
|
||||
}
|
||||
|
||||
fn menu(&self) -> Vec<MenuItem<Self>> {
|
||||
// Values captured into the action closures (which only get `&mut Self`).
|
||||
let mountpoint = self.state.mountpoint.clone();
|
||||
let open_mp = mountpoint.clone();
|
||||
// Value captured into the action closure (which only gets `&mut Self`).
|
||||
let open_mp = self.state.mountpoint.clone();
|
||||
|
||||
let mut items: Vec<MenuItem<Self>> = vec![
|
||||
StandardItem {
|
||||
@@ -134,25 +114,28 @@ impl Tray for DriveTray {
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
// Stop the systemd service (clean SIGTERM → lazy unmount). The next
|
||||
// login or reboot brings it back; this is a deliberate disconnect.
|
||||
items.push(
|
||||
StandardItem {
|
||||
label: "Unmount".into(),
|
||||
label: "Disconnect".into(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
unmount(&mountpoint);
|
||||
service::stop();
|
||||
this.state.mounted = false;
|
||||
this.state.line = "Unmounting…".into();
|
||||
this.state.line = "Disconnecting…".into();
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
// Enable+start the service so it mounts now and on future logins.
|
||||
items.push(
|
||||
StandardItem {
|
||||
label: "Mount".into(),
|
||||
label: "Connect".into(),
|
||||
activate: Box::new(|this: &mut Self| {
|
||||
spawn_mount();
|
||||
this.state.line = "Mounting…".into();
|
||||
service::enable_start();
|
||||
this.state.line = "Connecting…".into();
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user