feat: add prompt to restart daemon on version mismatch

This commit is contained in:
Ilya Zlobintsev
2026-05-16 23:34:50 +03:00
parent 05094b12fe
commit 307b1ac2c0
14 changed files with 282 additions and 101 deletions
Generated
+1
View File
@@ -1765,6 +1765,7 @@ dependencies = [
"thread-priority",
"tracing",
"tracing-subscriber",
"zbus",
]
[[package]]
+1
View File
@@ -39,6 +39,7 @@ i18n-embed = { version = "0.16.0", features = [
] }
i18n-embed-fl = "0.10.0"
rust-embed = { version = "8.11.0", features = ["debug-embed"] }
zbus = { version = "5.14.0", default-features = false, features = ["tokio"] }
[profile.release]
strip = "symbols"
+1 -1
View File
@@ -32,11 +32,11 @@ futures = { workspace = true }
indexmap = { workspace = true }
divan = { workspace = true, optional = true }
serde_norway = { workspace = true }
zbus = { workspace = true }
nvml-wrapper = "0.12.1"
bitflags = "2.11.1"
pciid-parser = { version = "0.8", features = ["serde"] }
zbus = { version = "5.14.0", default-features = false, features = ["tokio"] }
libdrm_amdgpu_sys = { version = "0.8.13", default-features = false, features = [
"dynamic_loading",
] }
+2
View File
@@ -48,5 +48,7 @@ thread-priority = "3.0.0"
divan = { workspace = true, optional = true }
zbus = { workspace = true }
[dev-dependencies]
pretty_assertions = "1.4.1"
+5 -1
View File
@@ -227,7 +227,10 @@ embedded-daemon-info =
version-mismatch = Version mismatch
version-mismatch-description =
Version mismatch between GUI and Daemon ({$gui_version}-{$gui_commit} vs {$daemon_version}-{$daemon_commit})!
If you have updated LACT, you need to restart the service with:
If you have updated LACT, you need to restart the system service with:
version-mismatch-restart-prompt =
Version mismatch between GUI and Daemon ({$gui_version}-{$gui_commit} vs {$daemon_version}-{$daemon_commit})!
This usually happens when LACT was updated, and the system service has not yet been restarted. Do you want to restart the service?
plot-show-detailed-info = Show detailed info
@@ -243,6 +246,7 @@ create = Create
cancel = Cancel
close = Close
save = Save
restart = Restart
default-profile = Default
rename-profile = Rename Profile
rename-profile-from = Rename profile <b>{$old_name}</b> to:
+102 -58
View File
@@ -14,12 +14,14 @@ pub(crate) mod pages;
mod preferences_dialog;
mod process_monitor;
mod profiles;
mod service_setup;
pub(crate) mod styles;
use crate::{
APP_ID, CONFIG, GUI_VERSION, I18N,
app::{
about_dialog::{AboutDialog, AboutDialogMsg},
ext::RelmLaunchable as _,
gpu_selector::GpuSelector,
info_dialog::{
InfoDialog, InfoDialogConfirmation, InfoDialogData, InfoDialogId, InfoDialogMsg,
@@ -111,7 +113,7 @@ pub struct AppModel {
settings_changed: BoolBinding,
system_info: SystemInfo,
system_info: Arc<SystemInfo>,
device_flags: Vec<DeviceFlag>,
device_driver: String,
}
@@ -433,72 +435,38 @@ impl AsyncComponent for AppModel {
let settings_changed = BoolBinding::new(false);
let system_info = daemon_client
.get_system_info()
.await
.expect("Could not fetch system info");
let devices = daemon_client
.list_devices()
.await
.expect("Could not list devices");
let initial_gpu_id = AppModel::init_gpu_selection(&devices);
let version_mismatch_info = (system_info.version != GUI_VERSION
|| system_info.commit.as_deref() != Some(GIT_COMMIT))
.then(|| InfoDialogData {
id: InfoDialogId::VersionMismatch,
heading: fl!(I18N, "version-mismatch"),
body: fl!(
I18N,
"version-mismatch-description",
gui_version = GUI_VERSION,
gui_commit = GIT_COMMIT,
daemon_version = system_info.version.as_str(),
daemon_commit = system_info.commit.as_deref().unwrap_or_default()
),
selectable_text: Some("sudo systemctl restart lactd".to_string()),
..Default::default()
});
let info_page = InformationPage::detach_default();
let oc_page = OcPage::builder()
.launch(settings_changed.clone())
.forward(sender.input_sender(), |msg| msg);
let thermals_page = ThermalsPage::builder().launch(()).detach();
let oc_page =
OcPage::launch(settings_changed.clone()).forward(sender.input_sender(), |msg| msg);
let thermals_page = ThermalsPage::detach_default();
let software_page = SoftwarePage::builder()
.launch((system_info.clone(), daemon_client.embedded))
.detach();
let software_page = SoftwarePage::detach_default();
let crash_page = CrashPage::builder()
.launch(String::new())
.forward(sender.input_sender(), |msg| msg);
let crash_page = CrashPage::launch_default().forward(sender.input_sender(), |msg| msg);
let overdrive_dialog = OverdriveDialog::builder()
.launch((system_info.clone(), root.clone().upcast()))
.detach();
let overdrive_dialog = OverdriveDialog::detach(root.clone().upcast());
let preferences_dialog = PreferencesDialog::builder()
.launch((system_info.clone(), root.clone()))
.detach();
let preferences_dialog = PreferencesDialog::detach(root.clone());
let about_dialog = AboutDialog::builder().launch(root.clone()).detach();
let info_dialog = InfoDialog::builder()
.launch(root.clone())
.forward(sender.input_sender(), |msg| msg);
let about_dialog = AboutDialog::detach(root.clone());
let info_dialog =
InfoDialog::launch(root.clone()).forward(sender.input_sender(), |msg| msg);
let graphs_window = GraphsWindow::detach_default();
let process_monitor_window = ProcessMonitorWindow::detach_default();
let gpu_selector = GpuSelector::builder()
.launch((devices, initial_gpu_id.clone()))
let gpu_selector = GpuSelector::launch((devices, initial_gpu_id.clone()))
.forward(sender.input_sender(), AppMsg::SelectGpu);
let profile_selector = ProfileSelector::builder()
.launch(())
.forward(sender.input_sender(), |msg| msg);
let profile_selector =
ProfileSelector::launch(()).forward(sender.input_sender(), |msg| msg);
let mut model = AppModel {
daemon_client,
@@ -518,19 +486,23 @@ impl AsyncComponent for AppModel {
ui_sensitive: BoolBinding::new(false),
stats_task_handle: None,
settings_changed,
system_info,
system_info: Arc::default(),
device_flags: vec![],
device_driver: String::new(),
};
if let Err(err) = model.update_system_info(true).await {
sender.input(AppMsg::Error(Arc::new(err)));
}
if let Err(err) = model.reload_profiles(None).await {
sender.input(AppMsg::Error(Arc::new(err)));
}
if let Some(gpu_id) = initial_gpu_id {
if let Err(err) = model.update_gpu_data_full(gpu_id, sender.clone()).await {
sender.input(AppMsg::Error(Arc::new(err)));
}
if let Some(gpu_id) = initial_gpu_id
&& let Err(err) = model.update_gpu_data_full(gpu_id, sender.clone()).await
{
sender.input(AppMsg::Error(Arc::new(err)));
}
let widgets = view_output!();
@@ -571,10 +543,6 @@ impl AsyncComponent for AppModel {
})));
}
if let Some(info) = version_mismatch_info {
model.info_dialog.emit(InfoDialogMsg::Show(Box::new(info)));
}
let task_sender = sender.clone();
sender.command(move |_, shutdown| {
shutdown
@@ -884,7 +852,10 @@ impl AppModel {
ConnectionStatusMsg::Disconnected => {
widgets.reconnecting_dialog.present(Some(root))
}
ConnectionStatusMsg::Reconnected => widgets.reconnecting_dialog.force_close(),
ConnectionStatusMsg::Reconnected => {
self.update_system_info(false).await?;
widgets.reconnecting_dialog.force_close();
}
},
AppMsg::EvaluateProfile(rule, sender) => {
match self.daemon_client.evaluate_profile_rule(rule).await {
@@ -902,6 +873,12 @@ impl AppModel {
.await?;
self.reload_profiles(None).await?;
}
AppMsg::RestartDaemon(proxy) => {
proxy
.restart("replace")
.await
.context("Could not restart daemon")?;
}
AppMsg::Crash(message) => {
// we cannot be sure that the application is fully functional after a crash
// even though the main loop is restored via crash handler, we want user to restart
@@ -1150,6 +1127,28 @@ impl AppModel {
Ok(stats)
}
async fn update_system_info(&mut self, init: bool) -> anyhow::Result<()> {
let info = self.daemon_client.get_system_info().await?;
let info = Arc::new(info);
self.preferences_dialog
.emit(PreferencesDialogMsg::SystemInfo(info.clone()));
self.overdrive_dialog
.emit(OverdriveDialogMsg::SystemInfo(info.clone()));
self.software_page.emit(SoftwarePageMsg::SystemInfo {
info: info.clone(),
daemon_embedded: self.daemon_client.embedded,
});
if init && (info.version != GUI_VERSION || info.commit.as_deref() != Some(GIT_COMMIT)) {
self.handle_mismatched_dameon_version().await;
}
Ok(())
}
async fn apply_settings(
&self,
gpu_id: String,
@@ -1202,6 +1201,51 @@ impl AppModel {
Ok(())
}
async fn handle_mismatched_dameon_version(&self) {
let dialog = match service_setup::systemd::connect_unit_proxy().await {
Ok(proxy) => InfoDialogData {
id: InfoDialogId::VersionMismatch,
heading: fl!(I18N, "version-mismatch"),
body: fl!(
I18N,
"version-mismatch-restart-prompt",
gui_version = GUI_VERSION,
gui_commit = GIT_COMMIT,
daemon_version = self.system_info.version.as_str(),
daemon_commit = self.system_info.commit.as_deref().unwrap_or_default()
),
confirmation: Some(InfoDialogConfirmation {
confirm_label: fl!(I18N, "restart"),
cancel_label: fl!(I18N, "cancel"),
appearance: adw::ResponseAppearance::Suggested,
confirm_msg: AppMsg::RestartDaemon(proxy),
}),
..Default::default()
},
Err(err) => {
warn!(
"could not connect to SystemD for unit management: {err:#}, falling back to just showing an error"
);
InfoDialogData {
id: InfoDialogId::VersionMismatch,
heading: fl!(I18N, "version-mismatch"),
body: fl!(
I18N,
"version-mismatch-description",
gui_version = GUI_VERSION,
gui_commit = GIT_COMMIT,
daemon_version = self.system_info.version.as_str(),
daemon_commit = self.system_info.commit.as_deref().unwrap_or_default()
),
selectable_text: Some("sudo systemctl restart lactd".to_string()),
..Default::default()
}
}
};
self.info_dialog.emit(InfoDialogMsg::Show(Box::new(dialog)));
}
fn ask_settings_confirmation(
&self,
mut delay: u64,
+18 -3
View File
@@ -21,15 +21,30 @@ impl FlowBoxExt for FlowBox {
.unwrap()
}
}
pub trait RelmDefaultLauchable: Component {
fn detach_default() -> relm4::Controller<Self>;
pub trait RelmLaunchable: Component {
fn launch(init: <Self as Component>::Init) -> relm4::component::Connector<Self>;
fn detach(init: <Self as Component>::Init) -> relm4::Controller<Self> {
Self::launch(init).detach()
}
}
impl<T: Component> RelmLaunchable for T {
fn launch(init: <Self as Component>::Init) -> relm4::component::Connector<Self> {
Self::builder().launch(init)
}
}
pub trait RelmDefaultLauchable: RelmLaunchable {
fn launch_default() -> relm4::component::Connector<Self>;
fn detach_default() -> relm4::Controller<Self>;
}
impl<S: Default, T: Component<Init = S>> RelmDefaultLauchable for T {
fn detach_default() -> relm4::Controller<Self> {
Self::builder().launch(S::default()).detach()
Self::detach(S::default())
}
fn launch_default() -> relm4::component::Connector<Self> {
+2
View File
@@ -1,6 +1,7 @@
use super::profiles::profile_rule_window::{
ProfileRuleWindowMsg, profile_rule_row::ProfileRuleRowMsg,
};
use crate::app::service_setup::systemd::UnitProxy;
use lact_client::ConnectionStatusMsg;
use lact_schema::{
DeviceStats, ProfileRule, ProfilesInfo, config::ProfileHooks, request::ProfileBase,
@@ -53,5 +54,6 @@ pub enum AppMsg {
ImportProfile,
ExportProfile(Option<String>),
ConnectionStatus(ConnectionStatusMsg),
RestartDaemon(UnitProxy<'static>),
Crash(String),
}
+13 -5
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use crate::{
I18N,
app::{APP_BROKER, msg::AppMsg},
@@ -10,7 +12,7 @@ use lact_schema::{AmdgpuParamsConfigurator, BootArgConfigurator, SystemInfo};
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt};
pub struct OverdriveDialog {
pub system_info: SystemInfo,
pub system_info: Arc<SystemInfo>,
pub parent: gtk::Widget,
pub is_loading: bool,
pub is_done: bool,
@@ -21,11 +23,12 @@ pub enum OverdriveDialogMsg {
Show,
Loading,
Loaded,
SystemInfo(Arc<SystemInfo>),
}
#[relm4::component(pub)]
impl relm4::Component for OverdriveDialog {
type Init = (SystemInfo, gtk::Widget);
type Init = gtk::Widget;
type Input = OverdriveDialogMsg;
type Output = ();
type CommandOutput = ();
@@ -45,6 +48,7 @@ impl relm4::Component for OverdriveDialog {
add = &adw::PreferencesGroup {
gtk::Label {
set_xalign: 0.0,
#[watch]
set_markup: &fl!(
I18N,
"amd-oc-description",
@@ -62,6 +66,7 @@ impl relm4::Component for OverdriveDialog {
adw::ActionRow {
set_title_lines: 0,
set_use_markup: true,
#[watch]
set_title: &fl!(
I18N,
"amd-oc-status",
@@ -76,6 +81,7 @@ impl relm4::Component for OverdriveDialog {
adw::ActionRow {
set_title_lines: 0,
set_use_markup: true,
#[watch]
set_title: &fl!(I18N, "amd-oc-detected-system-config", config = format_config(&model.system_info)),
},
@@ -139,13 +145,12 @@ impl relm4::Component for OverdriveDialog {
}
fn init(
init: Self::Init,
parent: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let (system_info, parent) = init;
let model = Self {
system_info,
system_info: Arc::default(),
parent,
is_loading: false,
is_done: false,
@@ -169,6 +174,9 @@ impl relm4::Component for OverdriveDialog {
self.is_loading = false;
self.is_done = true;
}
OverdriveDialogMsg::SystemInfo(info) => {
self.system_info = info;
}
}
self.update_view(widgets, sender);
+62 -28
View File
@@ -20,6 +20,8 @@ use std::{fmt::Write, sync::Arc};
use vulkan::feature_window::{VulkanFeature, VulkanFeaturesWindow};
pub struct SoftwarePage {
system_info: Arc<SystemInfo>,
daemon_embedded: bool,
device_info: Option<Arc<DeviceInfo>>,
vulkan_driver_selector: relm4::Controller<SimpleComboBox<String>>,
@@ -30,6 +32,10 @@ pub struct SoftwarePage {
#[derive(Debug)]
pub enum SoftwarePageMsg {
SystemInfo {
info: Arc<SystemInfo>,
daemon_embedded: bool,
},
DeviceInfo(Arc<DeviceInfo>),
ShowVulkanFeatures,
ShowVulkanExtensions,
@@ -38,7 +44,7 @@ pub enum SoftwarePageMsg {
#[relm4::component(pub)]
impl relm4::SimpleComponent for SoftwarePage {
type Init = (SystemInfo, bool);
type Init = ();
type Input = SoftwarePageMsg;
type Output = ();
@@ -57,9 +63,51 @@ impl relm4::SimpleComponent for SoftwarePage {
set_max_children_per_line: 4,
set_selection_mode: gtk::SelectionMode::None,
append_child = &InfoRow::new_selectable(&fl!(I18N, "lact-daemon"), &daemon_version),
append_child = &InfoRow::new_selectable(&fl!(I18N, "lact-gui"), &gui_version),
append_child = &InfoRow::new_selectable(&fl!(I18N, "kernel-version"), &system_info.kernel_version),
append_child = &InfoRow {
set_name: fl!(I18N, "lact-daemon"),
set_selectable: true,
#[watch]
set_value: {
let mut daemon_version = format!("{}-{}", model.system_info.version, model.system_info.profile);
if model.daemon_embedded {
daemon_version.push_str("-embedded");
}
if let Some(commit) = &model.system_info.commit {
let daemon_commit_link = format!("{REPO_URL}/commit/{commit}");
write!(
daemon_version,
" (commit <a href=\"{daemon_commit_link}\">{commit}</a>)"
)
.unwrap();
}
daemon_version
},
},
append_child = &InfoRow {
set_name: fl!(I18N, "lact-gui"),
set_selectable: true,
#[watch]
set_value: {
let gui_profile = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
let gui_commit_link = format!("{REPO_URL}/commit/{GIT_COMMIT}");
format!(
"{GUI_VERSION}-{gui_profile} (commit <a href=\"{gui_commit_link}\">{GIT_COMMIT}</a>)"
)
}
},
append_child = &InfoRow {
set_name: fl!(I18N, "kernel-version"),
set_selectable: true,
#[watch]
set_value: model.system_info.kernel_version.clone(),
},
},
},
@@ -225,7 +273,7 @@ impl relm4::SimpleComponent for SoftwarePage {
}
fn init(
(system_info, embedded): Self::Init,
_: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
@@ -244,35 +292,14 @@ impl relm4::SimpleComponent for SoftwarePage {
.forward(sender.input_sender(), |_| SoftwarePageMsg::SelectionChanged);
let model = Self {
system_info: Arc::default(),
daemon_embedded: false,
device_info: None,
vulkan_driver_selector,
opencl_platform_selector,
vulkan_window: None,
};
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
if embedded {
daemon_version.push_str("-embedded");
}
if let Some(commit) = &system_info.commit {
let daemon_commit_link = format!("{REPO_URL}/commit/{commit}");
write!(
daemon_version,
" (commit <a href=\"{daemon_commit_link}\">{commit}</a>)"
)
.unwrap();
}
let gui_profile = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
let gui_commit_link = format!("{REPO_URL}/commit/{GIT_COMMIT}");
let gui_version = format!(
"{GUI_VERSION}-{gui_profile} (commit <a href=\"{gui_commit_link}\">{GIT_COMMIT}</a>)"
);
let widgets = view_output!();
widgets.vulkan_stack.set_vhomogeneous(false);
@@ -325,6 +352,13 @@ impl relm4::SimpleComponent for SoftwarePage {
self.device_info = Some(info);
}
SoftwarePageMsg::SystemInfo {
info,
daemon_embedded,
} => {
self.system_info = info;
self.daemon_embedded = daemon_embedded;
}
SoftwarePageMsg::ShowVulkanFeatures => {
if let Some(vulkan_info) = &self.selected_vulkan_info() {
self.vulkan_window = Some(show_features_window(
+9 -4
View File
@@ -13,21 +13,23 @@ use gtk::prelude::{
use i18n_embed_fl::fl;
use lact_schema::SystemInfo;
use relm4::{ComponentParts, ComponentSender};
use std::sync::Arc;
pub struct PreferencesDialog {
parent: adw::ApplicationWindow,
system_info: SystemInfo,
system_info: Arc<SystemInfo>,
}
#[derive(Debug)]
pub enum PreferencesDialogMsg {
Show,
ThemeSelected(AppTheme),
SystemInfo(Arc<SystemInfo>),
}
#[relm4::component(pub)]
impl relm4::Component for PreferencesDialog {
type Init = (SystemInfo, adw::ApplicationWindow);
type Init = adw::ApplicationWindow;
type Input = PreferencesDialogMsg;
type Output = ();
type CommandOutput = ();
@@ -140,13 +142,13 @@ impl relm4::Component for PreferencesDialog {
}
fn init(
(system_info, parent): Self::Init,
parent: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = PreferencesDialog {
parent,
system_info,
system_info: Arc::default(),
};
let widgets = view_output!();
ComponentParts { model, widgets }
@@ -170,6 +172,9 @@ impl relm4::Component for PreferencesDialog {
config.theme = theme;
});
}
PreferencesDialogMsg::SystemInfo(info) => {
self.system_info = info;
}
}
self.update_view(widgets, sender);
}
+1
View File
@@ -0,0 +1 @@
pub mod systemd;
+64
View File
@@ -0,0 +1,64 @@
//! # D-Bus interface proxy for: `org.freedesktop.systemd1.Manager`
use anyhow::Context;
use zbus::{proxy, zvariant::OwnedObjectPath};
const UNIT_NAME: &str = "lactd.service";
pub async fn connect_unit_proxy() -> anyhow::Result<UnitProxy<'static>> {
let conn = zbus::Connection::system()
.await
.context("Could not establish DBus connection")?;
let manager = ManagerProxy::new(&conn)
.await
.context("Could not connect to systemd manager interface")?;
let path = manager
.get_unit(UNIT_NAME)
.await
.context("Could not get lact systemd unit")?;
let unit = UnitProxy::builder(&conn)
.path(path)?
.build()
.await
.context("Could not connect to systemd unit interface")?;
Ok(unit)
}
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1"
)]
pub trait Manager {
#[zbus(allow_interactive_auth)]
fn get_unit(&self, name: &str) -> zbus::Result<OwnedObjectPath>;
#[zbus(allow_interactive_auth)]
fn enable_unit_files(
&self,
files: &[&str],
runtime: bool,
force: bool,
) -> zbus::Result<(bool, Vec<(String, String, String)>)>;
}
#[proxy(
interface = "org.freedesktop.systemd1.Unit",
default_service = "org.freedesktop.systemd1"
)]
pub trait Unit {
/// Restart method
#[zbus(allow_interactive_auth)]
fn restart(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
/// Start method
#[zbus(allow_interactive_auth)]
fn start(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
/// Stop method
#[zbus(allow_interactive_auth)]
fn stop(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
}
+1 -1
View File
@@ -69,7 +69,7 @@ pub fn bytes_to_mib(bytes: u64) -> f64 {
pub struct Pong;
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct SystemInfo {
pub version: String,
pub commit: Option<String>,