fix(core,fuse,cli,gui): say when an upgrade left the old daemon running

A package upgrade replaces the binaries but does not restart the user
service, so the daemon keeps the old release and its bugs until the next
login. On 3.2.0 the reporter of #33 still saw "Couldn't sync Documents:
read .../snapshots/37: Permission denied", which only a daemon older than
3.2.0 can produce: since then the scan leaves an unreadable subfolder out
and only an unreadable sync root fails the pass (B206).

The daemon now reports its release in Status; one from before the field
omits it, which reads as older. When the daemon is older than the
front-end, the tray says "Update not finished - restart Proton Drive" and
offers a restart, the sync card shows "Restart to finish updating" with a
Restart button, and pdfs status and pdfs diagnose print the systemctl
command. A newer daemon is not flagged, since then the front-end is the
stale one.

Refs #33
This commit is contained in:
nvrl committed 2026-10-11 21:20:51 +02:00
1 parent 87361a4573
commit 91fa1614ed
19 files changed
+3988 -3562

No files matched your search

+1
View File
@@ -581,6 +581,7 @@ fn build_window(app: &adw::Application) {
pause: locations_widgets.pause.clone(),
paused: Cell::new(false),
sign_in: locations_widgets.card_sign_in.clone(),
restart: locations_widgets.card_restart.clone(),
},
queue: QueueState {
group: locations_widgets.queue_group.clone(),
+20 -2
View File
@@ -48,6 +48,8 @@ pub(crate) struct SyncCard {
pub(crate) paused: Cell<bool>,
/// Shown only while Proton refuses the session; opens the sign-in page.
pub(crate) sign_in: gtk4::Button,
/// Shown only while the daemon is older than the app; restarts it.
pub(crate) restart: gtk4::Button,
}
/// What a folder row's controls are built from. The subtitle and progress bar
@@ -111,6 +113,7 @@ pub(crate) struct LocationsWidgets {
pub(crate) card_title: gtk4::Label,
pub(crate) card_detail: gtk4::Label,
pub(crate) card_sign_in: gtk4::Button,
pub(crate) card_restart: gtk4::Button,
pub(crate) pause: adw::SplitButton,
pub(crate) queue_group: adw::PreferencesGroup,
pub(crate) retry_all: gtk4::Button,
@@ -137,7 +140,7 @@ pub(crate) fn build_locations_page(history: &gtk4::Widget) -> (gtk4::Widget, Loc
.build();
add_folder.add_css_class("flat");
let transfers_group = build_transfers_group();
let (card, card_icon, card_title, card_detail, card_sign_in) = build_sync_card();
let (card, card_icon, card_title, card_detail, card_sign_in, card_restart) = build_sync_card();
let pause = build_pause_button();
let (queue_group, retry_all) = build_queue_group();
let conflicts_group = adw::PreferencesGroup::builder()
@@ -257,6 +260,7 @@ pub(crate) fn build_locations_page(history: &gtk4::Widget) -> (gtk4::Widget, Loc
card_title,
card_detail,
card_sign_in,
card_restart,
pause,
queue_group,
retry_all,
@@ -299,6 +303,11 @@ pub(crate) fn wire_locations(ui: &Rc<Ui>, retry: &gtk4::Button, add_folder: &gtk
.card
.sign_in
.connect_clicked(move |_| show_login(&ui_sign_in));
let ui_restart = ui.clone();
ui.locations
.card
.restart
.connect_clicked(move |_| restart_service_then(&ui_restart, |_| {}));
let ui_retry = ui.clone();
ui.locations
.queue
@@ -396,6 +405,7 @@ fn build_sync_card() -> (
gtk4::Label,
gtk4::Label,
gtk4::Button,
gtk4::Button,
) {
let icon = gtk4::Image::builder()
.icon_name("pdfs-sync-symbolic")
@@ -430,10 +440,18 @@ fn build_sync_card() -> (
.build();
sign_in.add_css_class("pill");
sign_in.add_css_class("suggested-action");
let restart = gtk4::Button::builder()
.label(gettext("Restart"))
.valign(gtk4::Align::Center)
.visible(false)
.build();
restart.add_css_class("pill");
restart.add_css_class("suggested-action");
card.append(&icon);
card.append(&text);
card.append(&sign_in);
(card, icon, title, detail, sign_in)
card.append(&restart);
(card, icon, title, detail, sign_in, restart)
}
/// Pause/Resume, with timed pauses in its menu.
+25
View File
@@ -1232,6 +1232,7 @@ pub(crate) fn paint_status(ui: &Rc<Ui>, status: Response) {
paused_until,
session_expired,
tokens_unsaved,
version,
..
} = status
else {
@@ -1246,6 +1247,8 @@ pub(crate) fn paint_status(ui: &Rc<Ui>, status: Response) {
SyncState::SessionExpired { queued }
} else if tokens_unsaved {
SyncState::TokensUnsaved
} else if pdfs_core::control::daemon_outdated(&version) {
SyncState::Outdated { running: version }
} else if paused {
SyncState::Paused {
until: paused_until,
@@ -1317,6 +1320,12 @@ pub(crate) enum SyncState {
/// The keyring refuses the session's refreshed tokens. Sync goes on, but
/// a reboot before the keyring takes them signs the user out.
TokensUnsaved,
/// The daemon is an older release than this window: an upgrade replaced
/// the files but left the service running, with the old release's bugs,
/// until it restarts (#33). `running` is empty when it is too old to say.
Outdated {
running: String,
},
/// The user paused syncing; nothing goes up until it resumes. Outranks
/// everything else, because it is the reason for everything else.
Paused {
@@ -1352,6 +1361,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
let paused = matches!(state, SyncState::Paused { .. });
let connected = !matches!(state, SyncState::Disconnected | SyncState::NotResponding);
let expired = matches!(state, SyncState::SessionExpired { .. });
let outdated = matches!(state, SyncState::Outdated { .. });
let (icon, class, title, detail) = match state {
SyncState::SessionExpired { queued } => (
"dialog-password-symbolic",
@@ -1374,6 +1384,20 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
"Unlock your keyring, or the next reboot signs you out",
)),
),
SyncState::Outdated { running } => (
"dialog-warning-symbolic",
Some("warning"),
gettext("Restart to finish updating"),
Some(if running.is_empty() {
gettext("An older version of Proton Drive is still running")
} else {
// Translators: {version} is a release number such as "3.2.0".
gettext_f(
"Proton Drive {version} is still running",
&[("version", &running)],
)
}),
),
SyncState::Paused { until, queued } => (
"media-playback-pause-symbolic",
Some("warning"),
@@ -1445,6 +1469,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
image.add_css_class(class);
}
ui.locations.card.sign_in.set_visible(expired);
ui.locations.card.restart.set_visible(outdated);
ui.status.status_title.set_label(&title);
ui.status.status_detail.set_visible(detail.is_some());
ui.status