mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
Merge pull request #22 from kacperpaczos/fix/login-poll-backoff
fix(cli): back off while waiting for a stored session
This commit is contained in:
2 files changed
+135
-13
No files matched your search
+124
-10
@@ -17,6 +17,7 @@ use pdfs_core::control::{
|
||||
RestoreItem as CtlRestoreItem, ShareEntryKind, SyncPhase, pending_summary,
|
||||
};
|
||||
use pdfs_core::db::Db;
|
||||
use pdfs_core::service;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
@@ -1355,6 +1356,15 @@ fn cmd_login(username: Option<String>) -> Result<()> {
|
||||
other => anyhow::Error::new(other).context("login failed"),
|
||||
})?;
|
||||
|
||||
// A daemon that is already up is parked in `wait_for_session`, sleeping
|
||||
// between keyring checks; restart it so the mount appears now instead of
|
||||
// whenever the next poll fires. Only when it is already active — `pdfs
|
||||
// login` deliberately does not enable the unit the way the GUI does, so a
|
||||
// headless user driving `pdfs daemon` by hand keeps that setup.
|
||||
if service::is_active() {
|
||||
service::restart();
|
||||
}
|
||||
|
||||
println!("Logged in as {username}. Session stored in the system keyring.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1494,6 +1504,59 @@ fn cmd_mount(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How long to wait before the next login check, after `attempts` checks have
|
||||
/// already come back [`pdfs_core::Error::NotLoggedIn`].
|
||||
///
|
||||
/// Doubles from 3s to a 15s ceiling. The interval is not just a sleep:
|
||||
/// [`auth::load`] builds a fresh `keyring::Entry` on every call, so each check
|
||||
/// opens a new D-Bus connection to the Secret Service and negotiates an
|
||||
/// encrypted session over it. A flat 3s poll meant 28,800 of those a day from a
|
||||
/// daemon that had nothing to do; the ceiling cuts that by 80%.
|
||||
///
|
||||
/// The ceiling stays low because the sleep is also the worst-case login →
|
||||
/// mount latency for anything that does *not* poke systemd: front-ends restart
|
||||
/// the unit on login (`service::enable_start`), which interrupts the sleep
|
||||
/// immediately, but a hand-run `pdfs daemon` has no such wake-up path.
|
||||
fn login_poll_delay(attempts: u32) -> std::time::Duration {
|
||||
const BASE_SECS: u64 = 3;
|
||||
const MAX_SECS: u64 = 15;
|
||||
|
||||
// `checked_shl` rather than `<<`: the daemon can sit unauthenticated for
|
||||
// weeks, and a shift past the width of the type is a panic, not a big number.
|
||||
let factor = 1u64.checked_shl(attempts).unwrap_or(u64::MAX);
|
||||
std::time::Duration::from_secs(BASE_SECS.saturating_mul(factor).min(MAX_SECS))
|
||||
}
|
||||
|
||||
/// Block until a session is stored, backing off between checks.
|
||||
///
|
||||
/// `load` and `sleep` are parameters rather than direct calls so the backoff
|
||||
/// schedule can be tested without a keyring and without real time passing.
|
||||
fn wait_for_session<L, S>(mut load: L, mut sleep: S) -> Result<()>
|
||||
where
|
||||
L: FnMut() -> std::result::Result<(), pdfs_core::Error>,
|
||||
S: FnMut(std::time::Duration),
|
||||
{
|
||||
let mut attempts: u32 = 0;
|
||||
loop {
|
||||
match load() {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(pdfs_core::Error::NotLoggedIn) => {
|
||||
let delay = login_poll_delay(attempts);
|
||||
// Say it once at INFO. Repeating it every interval buried the
|
||||
// journal in tens of thousands of identical lines a day.
|
||||
if attempts == 0 {
|
||||
tracing::info!("not logged in; waiting for a session…");
|
||||
} else {
|
||||
tracing::debug!(delay_secs = delay.as_secs(), "still not logged in");
|
||||
}
|
||||
sleep(delay);
|
||||
attempts = attempts.saturating_add(1);
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -1502,16 +1565,7 @@ 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()),
|
||||
}
|
||||
}
|
||||
wait_for_session(|| auth::load().map(|_| ()), std::thread::sleep)?;
|
||||
|
||||
match mount_once(mountpoint.clone()) {
|
||||
Ok(pdfs_fuse::MountOutcome::Shutdown) => {
|
||||
@@ -2692,3 +2746,63 @@ mod diagnose_tests {
|
||||
std::fs::remove_dir(dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod login_wait_tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn poll_delay_doubles_then_holds_at_the_ceiling() {
|
||||
assert_eq!(login_poll_delay(0), Duration::from_secs(3));
|
||||
assert_eq!(login_poll_delay(1), Duration::from_secs(6));
|
||||
assert_eq!(login_poll_delay(2), Duration::from_secs(12));
|
||||
assert_eq!(login_poll_delay(3), Duration::from_secs(15));
|
||||
assert_eq!(login_poll_delay(4), Duration::from_secs(15));
|
||||
// A daemon left unauthenticated for weeks reaches attempt counts well
|
||||
// past the width of the shift; that must cap, not panic.
|
||||
assert_eq!(login_poll_delay(64), Duration::from_secs(15));
|
||||
assert_eq!(login_poll_delay(u32::MAX), Duration::from_secs(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiting_for_a_session_stops_hammering_the_keyring() {
|
||||
let mut checks_left = 20;
|
||||
let mut slept: Vec<Duration> = Vec::new();
|
||||
|
||||
wait_for_session(
|
||||
|| {
|
||||
if checks_left == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
checks_left -= 1;
|
||||
Err(pdfs_core::Error::NotLoggedIn)
|
||||
}
|
||||
},
|
||||
|delay| slept.push(delay),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(slept.len(), 20, "one wait per failed check");
|
||||
assert!(
|
||||
slept.windows(2).all(|w| w[1] >= w[0]),
|
||||
"the interval must never shrink: {slept:?}"
|
||||
);
|
||||
|
||||
// The point of the change. Every check is a fresh D-Bus session against
|
||||
// the Secret Service, so 20 of them must not fit inside a single minute
|
||||
// the way a flat 3s poll did.
|
||||
let total: Duration = slept.iter().sum();
|
||||
assert!(
|
||||
total >= Duration::from_secs(4 * 60),
|
||||
"20 checks should span at least 4 minutes, spanned {total:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stored_session_is_not_waited_for_at_all() {
|
||||
let mut slept = 0;
|
||||
wait_for_session(|| Ok(()), |_| slept += 1).unwrap();
|
||||
assert_eq!(slept, 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user