mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-08-17 16:34:54 -05:00
feat: new service setup GUI (#1043)
* feat: WIP service setup GUI * fix: avoid crash when closing dialog with app already running * wip * feat: make it more usable * wip logs * feat: working logs display * feat: use new dialog for version mismatch * chore: cleanup * fix: tcp functionality * feat: handle setup errors * perf: avoid refetching system info * fix test
This commit is contained in:
Generated
+2
@@ -1847,6 +1847,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"cairo-rs",
|
||||
"divan",
|
||||
"futures",
|
||||
"gtk4",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
@@ -1870,6 +1871,7 @@ dependencies = [
|
||||
"thread-priority",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -40,6 +40,7 @@ i18n-embed = { version = "0.16.0", features = [
|
||||
] }
|
||||
i18n-embed-fl = "0.10.0"
|
||||
rust-embed = { version = "8.12.0", features = ["debug-embed"] }
|
||||
zbus = { version = "5.14.0", default-features = false, features = ["tokio"] }
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
|
||||
@@ -138,6 +138,8 @@ On most desktop configurations (such as the default setup on Arch-based, most
|
||||
Debian-based or Fedora systems) this includes the default user, so you do not
|
||||
need to configure this.
|
||||
|
||||
This is not needed if you are using the Flatpak, the flatpak service setup handles permissions.
|
||||
|
||||
However, some systems may have different user configuration. In particular, this
|
||||
has been reported to be a problem on OpenSUSE.
|
||||
|
||||
@@ -145,7 +147,7 @@ To fix socket permissions in such configurations, edit `/etc/lact/config.yaml`
|
||||
and under the `daemon` section either:
|
||||
|
||||
- Set `admin_user` to your username
|
||||
- Set `admin_group` to a group that your user is a part of Then restart the
|
||||
- Set `admin_group` to a group that your user is a part of, then restart the
|
||||
service (`sudo systemctl restart lactd`).
|
||||
|
||||
# Overclocking (AMD)
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::future::BoxFuture;
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::path::Path;
|
||||
use tokio::{io::BufReader, net::UnixStream};
|
||||
use tracing::info;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct UnixConnection {
|
||||
inner: BufReader<UnixStream>,
|
||||
@@ -12,7 +12,7 @@ pub struct UnixConnection {
|
||||
|
||||
impl UnixConnection {
|
||||
pub async fn connect(path: &Path) -> anyhow::Result<Box<Self>> {
|
||||
info!("connecting to service at {path:?}");
|
||||
debug!("connecting to service at {path:?}");
|
||||
let inner = UnixStream::connect(path).await?;
|
||||
Ok(Box::new(Self {
|
||||
inner: BufReader::new(inner),
|
||||
|
||||
+31
-5
@@ -4,7 +4,7 @@ mod macros;
|
||||
|
||||
pub use lact_schema as schema;
|
||||
use lact_schema::{
|
||||
DeviceApiInfo, DisplaysInfo, ProcessList, ProfileRule,
|
||||
DeviceApiInfo, DisplaysInfo, Pong, ProcessList, ProfileRule,
|
||||
config::{GpuConfig, Profile, ProfileHooks},
|
||||
};
|
||||
|
||||
@@ -19,7 +19,8 @@ use schema::{
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{
|
||||
future::Future, os::unix::net::UnixStream, path::PathBuf, pin::Pin, rc::Rc, time::Duration,
|
||||
fmt, future::Future, io, os::unix::net::UnixStream, path::PathBuf, pin::Pin, rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
net::ToSocketAddrs,
|
||||
@@ -34,18 +35,24 @@ const RECONNECT_INTERVAL_MS: u64 = 500;
|
||||
pub struct DaemonClient {
|
||||
stream: Rc<Mutex<Box<dyn DaemonConnection>>>,
|
||||
status_tx: broadcast::Sender<ConnectionStatusMsg>,
|
||||
reconnect: bool,
|
||||
pub embedded: bool,
|
||||
}
|
||||
|
||||
impl DaemonClient {
|
||||
pub async fn connect() -> anyhow::Result<Self> {
|
||||
let path =
|
||||
get_socket_path().context("Could not connect to daemon: socket file not found")?;
|
||||
Self::connect_with_reconnect(true).await
|
||||
}
|
||||
|
||||
pub async fn connect_with_reconnect(reconnect: bool) -> anyhow::Result<Self> {
|
||||
let path = get_socket_path()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "socket file not found"))?;
|
||||
let stream = UnixConnection::connect(&path).await?;
|
||||
|
||||
Ok(Self {
|
||||
stream: Rc::new(Mutex::new(stream)),
|
||||
embedded: false,
|
||||
reconnect,
|
||||
status_tx: broadcast::Sender::new(STATUS_MSG_CHANNEL_SIZE),
|
||||
})
|
||||
}
|
||||
@@ -56,6 +63,7 @@ impl DaemonClient {
|
||||
Ok(Self {
|
||||
stream: Rc::new(Mutex::new(stream)),
|
||||
embedded: false,
|
||||
reconnect: true,
|
||||
status_tx: broadcast::Sender::new(STATUS_MSG_CHANNEL_SIZE),
|
||||
})
|
||||
}
|
||||
@@ -65,6 +73,7 @@ impl DaemonClient {
|
||||
Ok(Self {
|
||||
stream: Rc::new(Mutex::new(Box::new(connection))),
|
||||
embedded,
|
||||
reconnect: false,
|
||||
status_tx: broadcast::Sender::new(STATUS_MSG_CHANNEL_SIZE),
|
||||
})
|
||||
}
|
||||
@@ -94,9 +103,14 @@ impl DaemonClient {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not make request: {err}, reconnecting to socket");
|
||||
let _ = self.status_tx.send(ConnectionStatusMsg::Disconnected);
|
||||
|
||||
if !self.reconnect {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
error!("Could not make request: {err}, reconnecting to socket");
|
||||
|
||||
loop {
|
||||
match stream.new_connection().await {
|
||||
Ok(new_connection) => {
|
||||
@@ -122,6 +136,10 @@ impl DaemonClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn ping(&self) -> anyhow::Result<Pong> {
|
||||
self.make_request(Request::Ping).await
|
||||
}
|
||||
|
||||
pub async fn list_devices(&self) -> anyhow::Result<Vec<DeviceListEntry>> {
|
||||
self.make_request(Request::ListDevices).await
|
||||
}
|
||||
@@ -237,6 +255,14 @@ impl DaemonClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DaemonClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("DaemonClient")
|
||||
.field("embedded", &self.embedded)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_socket_path() -> Option<PathBuf> {
|
||||
let root_path = PathBuf::from("/run/lactd.sock");
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ serde_norway = { workspace = true }
|
||||
nvml-wrapper = { workspace = true }
|
||||
bitflags = "2.13.0"
|
||||
pciid-parser = { version = "0.8", features = ["serde"] }
|
||||
zbus = { version = "5.17.0", default-features = false, features = ["tokio"] }
|
||||
zbus = { workspace = true }
|
||||
zbus_polkit = { version = "5.0.0", default-features = false, features = ["tokio"] }
|
||||
libdrm_amdgpu_sys = { version = "0.8.16", default-features = false, features = [
|
||||
"dynamic_loading",
|
||||
|
||||
@@ -2,7 +2,7 @@ pub mod power_profiles_daemon;
|
||||
|
||||
use anyhow::{Context, anyhow, bail, ensure};
|
||||
use lact_schema::{
|
||||
AmdgpuParamsConfigurator, BootArgConfigurator, GIT_COMMIT, InitramfsType, SystemInfo,
|
||||
AmdgpuParamsConfigurator, BootArgConfigurator, InitramfsType, SystemInfo, VersionInfo,
|
||||
};
|
||||
use nix::sys::{
|
||||
socket::{
|
||||
@@ -45,14 +45,6 @@ static MODULE_CONF_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
pub async fn info() -> anyhow::Result<SystemInfo> {
|
||||
let version = DAEMON_VERSION.to_owned();
|
||||
let profile = if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
let kernel_version = uname().map_or_else(
|
||||
|err| {
|
||||
error!("could not fetch kernel version: {err}");
|
||||
@@ -70,12 +62,10 @@ pub async fn info() -> anyhow::Result<SystemInfo> {
|
||||
let os_release = get_os_release().inspect_err(|err| error!("Could not detect distro: {err}"));
|
||||
|
||||
Ok(SystemInfo {
|
||||
version,
|
||||
profile,
|
||||
version: VersionInfo::current(),
|
||||
kernel_version,
|
||||
distro: os_release.as_ref().map(|release| release.name.clone()).ok(),
|
||||
amdgpu_overdrive_enabled,
|
||||
commit: Some(GIT_COMMIT.to_owned()),
|
||||
amdgpu_params_configurator: match os_release {
|
||||
Ok(release) => detect_amdgpu_configurator(&release).await.ok(),
|
||||
Err(_) => None,
|
||||
|
||||
@@ -37,6 +37,9 @@ i18n-embed = { workspace = true }
|
||||
i18n-embed-fl = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
|
||||
futures = { workspace = true }
|
||||
zbus = { workspace = true }
|
||||
|
||||
plotters = { version = "0.3.7", default-features = false, features = [
|
||||
"area_series",
|
||||
"line_series",
|
||||
|
||||
@@ -230,18 +230,29 @@ edit-graph-sensors = Edit Graph Sensors
|
||||
error-heading = Error
|
||||
daemon-info-heading = Daemon info
|
||||
|
||||
reconnecting-to-daemon = Daemon connection lost, reconnecting...
|
||||
reconnecting-to-daemon = Service connection lost, reconnecting...
|
||||
daemon-connection-lost = Connection Lost
|
||||
embedded-daemon-info =
|
||||
Could not connect to daemon, running in embedded mode.
|
||||
Please make sure the lactd service is running.
|
||||
Using embedded mode, you will not be able to change any settings.
|
||||
service-explanation =
|
||||
LACT requires a system service in order to apply GPU settings.
|
||||
It is possible to skip the setup use the application in standalone mode, if you wish to use it only for information and monitoring.
|
||||
service-connection-status = Connection Status:
|
||||
service-status = Service Status:
|
||||
service-not-running = Service is not running
|
||||
service-permission-denied =
|
||||
Permission denied, service is not configured to allow connections from your user.
|
||||
See <a href="https://github.com/ilya-zlobintsev/lact#configuration">GitHub</a> for more information
|
||||
service-connected = Connected
|
||||
service-version = Service Version
|
||||
service-version-mismatch = mismatched
|
||||
service-logs = Service Logs
|
||||
|
||||
service-start = Start
|
||||
service-stop = Stop
|
||||
service-restart = Restart
|
||||
|
||||
{$error_info}To enable the daemon, run the following command, then restart LACT:
|
||||
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 service.
|
||||
|
||||
plot-show-detailed-info = Show detailed info
|
||||
|
||||
|
||||
+75
-69
@@ -12,7 +12,7 @@ mod profiles;
|
||||
pub(crate) mod utils;
|
||||
|
||||
use crate::{
|
||||
APP_ID, CONFIG, GUI_VERSION, I18N,
|
||||
APP_ID, CONFIG, I18N,
|
||||
app::{
|
||||
about_dialog::{AboutDialog, AboutDialogMsg},
|
||||
components::loader,
|
||||
@@ -31,6 +31,10 @@ use crate::{
|
||||
utils::ext::RelmLaunchable as _,
|
||||
},
|
||||
config::WindowSize,
|
||||
service_setup::{
|
||||
ServiceSetupDialog, ServiceSetupDialogParams,
|
||||
systemd::{self, connect_unit_proxy},
|
||||
},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use anyhow::{Context, anyhow};
|
||||
@@ -42,7 +46,7 @@ use gtk::{
|
||||
use i18n_embed_fl::fl;
|
||||
use lact_client::{ConnectionStatusMsg, DaemonClient};
|
||||
use lact_schema::{
|
||||
DeviceApiInfo, DeviceFlag, DeviceListEntry, DeviceStats, DeviceType, GIT_COMMIT, SystemInfo,
|
||||
DeviceApiInfo, DeviceFlag, DeviceListEntry, DeviceStats, DeviceType, SystemInfo,
|
||||
args::GuiArgs,
|
||||
config::{GpuConfig, Profile},
|
||||
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||
@@ -64,7 +68,7 @@ use relm4::{
|
||||
css,
|
||||
loading_widgets::LoadingWidgets,
|
||||
new_action_group, new_stateless_action,
|
||||
prelude::{AsyncComponent, AsyncComponentParts},
|
||||
prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts},
|
||||
tokio::{self, time::sleep},
|
||||
view,
|
||||
};
|
||||
@@ -155,6 +159,7 @@ impl AsyncComponent for AppModel {
|
||||
&fl!(I18N, "dump-vbios") => DumpVBiosAction,
|
||||
},
|
||||
section! {
|
||||
"Service Setup" => ServiceSetupAction,
|
||||
&fl!(I18N, "preferences") => PreferencesAction,
|
||||
&fl!(I18N, "about") => AboutAction,
|
||||
},
|
||||
@@ -357,6 +362,10 @@ impl AsyncComponent for AppModel {
|
||||
// 5. build child components,
|
||||
// 6. load profiles and initial GPU data.
|
||||
|
||||
let application = root
|
||||
.application()
|
||||
.expect("Failed to get application from root window");
|
||||
|
||||
relm4::set_global_css_with_priority(
|
||||
styles::COMBINED_CSS,
|
||||
STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
@@ -367,23 +376,20 @@ impl AsyncComponent for AppModel {
|
||||
}
|
||||
CONFIG.read().color_scheme.apply();
|
||||
|
||||
let (daemon_client, conn_err) = match args.tcp_address {
|
||||
let daemon_client = match args.tcp_address {
|
||||
Some(remote_addr) => {
|
||||
info!("establishing connection to {remote_addr}");
|
||||
match DaemonClient::connect_tcp(&remote_addr).await {
|
||||
Ok(conn) => (conn, None),
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("TCP connection error: {err:#}");
|
||||
let (conn, _) = create_connection()
|
||||
.await
|
||||
.expect("Could not create fallback connection");
|
||||
(conn, Some(err))
|
||||
sender.input(AppMsg::Error(
|
||||
anyhow!("TCP connection failed, falling back to local: {err:#}").into(),
|
||||
));
|
||||
create_connection(&root, &sender).await
|
||||
}
|
||||
}
|
||||
}
|
||||
None => create_connection()
|
||||
.await
|
||||
.expect("Could not establish any daemon connection"),
|
||||
None => create_connection(&root, &sender).await,
|
||||
};
|
||||
|
||||
let mut conn_status_rx = daemon_client.status_receiver();
|
||||
@@ -412,22 +418,9 @@ impl AsyncComponent for AppModel {
|
||||
.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()
|
||||
});
|
||||
if !system_info.version.is_current() {
|
||||
sender.input(AppMsg::ShowServiceSetupDialog);
|
||||
}
|
||||
|
||||
let info_page = InformationPage::detach_default();
|
||||
|
||||
@@ -464,14 +457,12 @@ impl AsyncComponent for AppModel {
|
||||
// create action group and actions for app menu
|
||||
// action group and actions are declared at the bottom of the file
|
||||
let mut actions = RelmActionGroup::<AppActionGroup>::new();
|
||||
let application = root
|
||||
.application()
|
||||
.expect("Failed to get application from root window");
|
||||
setup_actions! {
|
||||
(actions, ProcessMonitorAction, APP_BROKER.send(AppMsg::ShowProcessMonitor)),
|
||||
(actions, HistoricalGraphsAction, APP_BROKER.send(AppMsg::ShowGraphsWindow)),
|
||||
(actions, GenerateDebugSnapshotAction, APP_BROKER.send(AppMsg::DebugSnapshot)),
|
||||
(actions, PreferencesAction, APP_BROKER.send(AppMsg::ShowPreferencesDialog)),
|
||||
(actions, ServiceSetupAction, APP_BROKER.send(AppMsg::ShowServiceSetupDialog)),
|
||||
(actions, AboutAction, APP_BROKER.send(AppMsg::ShowAboutDialog)),
|
||||
(actions, QuitAction, APP_BROKER.send(AppMsg::Quit)),
|
||||
}
|
||||
@@ -551,26 +542,6 @@ impl AsyncComponent for AppModel {
|
||||
.set_title(&page.title().unwrap_or_default());
|
||||
}
|
||||
|
||||
if let Some(err) = conn_err {
|
||||
model
|
||||
.info_dialog
|
||||
.emit(InfoDialogMsg::Show(Box::new(InfoDialogData {
|
||||
id: InfoDialogId::EmbeddedDaemonInfo,
|
||||
heading: fl!(I18N, "daemon-info-heading"),
|
||||
body: fl!(
|
||||
I18N,
|
||||
"embedded-daemon-info",
|
||||
error_info = format!("Error info: {err:#}\n\n")
|
||||
),
|
||||
selectable_text: Some("sudo systemctl enable --now lactd".to_string()),
|
||||
..Default::default()
|
||||
})));
|
||||
}
|
||||
|
||||
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
|
||||
@@ -691,6 +662,15 @@ impl AppModel {
|
||||
AppMsg::ShowOverdriveDialog => {
|
||||
self.overdrive_dialog.emit(OverdriveDialogMsg::Show);
|
||||
}
|
||||
AppMsg::ShowServiceSetupDialog => {
|
||||
let params = ServiceSetupDialogParams {
|
||||
parent: root.clone().upcast(),
|
||||
initial_client: Ok((self.daemon_client.clone(), self.system_info.clone())),
|
||||
unit_proxy: systemd::connect_unit_proxy().await?,
|
||||
};
|
||||
let mut controller = ServiceSetupDialog::builder().launch(params).detach();
|
||||
controller.detach_runtime();
|
||||
}
|
||||
AppMsg::SelectProfile {
|
||||
profile,
|
||||
auto_switch,
|
||||
@@ -1435,37 +1415,63 @@ fn start_stats_update_loop(
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_connection() -> anyhow::Result<(DaemonClient, Option<anyhow::Error>)> {
|
||||
async fn create_connection(
|
||||
root: &adw::ApplicationWindow,
|
||||
sender: &relm4::AsyncComponentSender<AppModel>,
|
||||
) -> DaemonClient {
|
||||
match DaemonClient::connect().await {
|
||||
Ok(connection) => {
|
||||
debug!("Established daemon connection");
|
||||
Ok((connection, None))
|
||||
}
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
info!("could not connect to socket: {err:#}");
|
||||
info!("using a local daemon");
|
||||
let configured_client = match connect_unit_proxy().await {
|
||||
Ok(unit_proxy) => {
|
||||
let params = ServiceSetupDialogParams {
|
||||
parent: root.clone().upcast(),
|
||||
initial_client: Err(err),
|
||||
unit_proxy,
|
||||
};
|
||||
let service_setup = ServiceSetupDialog::builder().launch(params).into_stream();
|
||||
|
||||
let (server_stream, client_stream) = UnixStream::pair()?;
|
||||
client_stream.set_nonblocking(true)?;
|
||||
server_stream.set_nonblocking(true)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = lact_daemon::run_embedded(server_stream) {
|
||||
error!("Builtin daemon error: {err}");
|
||||
service_setup
|
||||
.recv_one()
|
||||
.await
|
||||
.expect("Could not get client")
|
||||
}
|
||||
});
|
||||
Err(setup_err) => {
|
||||
sender.input(AppMsg::Error(anyhow!("Could not connect to daemon: {err:#}\nPlease make sure the daemon is set up and running.\nGuided setup not available: {setup_err}").into()));
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let client = DaemonClient::from_stream(client_stream, true)?;
|
||||
Ok((client, Some(err)))
|
||||
match configured_client {
|
||||
Some(client) => client,
|
||||
None => create_embedded_connection()
|
||||
.await
|
||||
.expect("Could not spawn embedded daemon"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_embedded_connection() -> anyhow::Result<DaemonClient> {
|
||||
let (server_stream, client_stream) = UnixStream::pair()?;
|
||||
client_stream.set_nonblocking(true)?;
|
||||
server_stream.set_nonblocking(true)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = lact_daemon::run_embedded(server_stream) {
|
||||
error!("Builtin daemon error: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
DaemonClient::from_stream(client_stream, true)
|
||||
}
|
||||
|
||||
new_action_group!(pub AppActionGroup, "app");
|
||||
new_stateless_action!(pub ProcessMonitorAction, AppActionGroup, "show-process-monitor");
|
||||
new_stateless_action!(pub HistoricalGraphsAction, AppActionGroup, "show-historical-graphs");
|
||||
new_stateless_action!(pub GenerateDebugSnapshotAction, AppActionGroup, "generate-debug-snapshot");
|
||||
new_stateless_action!(pub DumpVBiosAction, AppActionGroup, "dump-vbios");
|
||||
new_stateless_action!(pub ServiceSetupAction, AppActionGroup, "service-setup");
|
||||
new_stateless_action!(pub PreferencesAction, AppActionGroup, "preferences");
|
||||
new_stateless_action!(pub AboutAction, AppActionGroup, "about");
|
||||
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
|
||||
|
||||
@@ -16,9 +16,7 @@ pub enum InfoDialogId {
|
||||
#[default]
|
||||
Unknown,
|
||||
Error,
|
||||
EmbeddedDaemonInfo,
|
||||
ResetConfigConfirmation,
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -27,7 +25,6 @@ pub struct InfoDialogData {
|
||||
pub heading: String,
|
||||
pub body: String,
|
||||
pub stacktrace: Option<String>,
|
||||
pub selectable_text: Option<String>,
|
||||
pub confirmation: Option<InfoDialogConfirmation>,
|
||||
}
|
||||
|
||||
@@ -161,12 +158,6 @@ impl relm4::Component for InfoDialogEntry {
|
||||
set_xalign: 0.0,
|
||||
},
|
||||
|
||||
gtk::Entry {
|
||||
set_visible: model.data.selectable_text.is_some(),
|
||||
set_text: model.data.selectable_text.as_deref().unwrap_or_default(),
|
||||
set_editable: false,
|
||||
},
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_visible: cfg!(debug_assertions) && model.data.stacktrace.is_some(),
|
||||
set_min_content_width: 600,
|
||||
|
||||
@@ -29,6 +29,7 @@ pub enum AppMsg {
|
||||
ShowPreferencesDialog,
|
||||
ShowAboutDialog,
|
||||
ShowOverdriveDialog,
|
||||
ShowServiceSetupDialog,
|
||||
EnableOverdrive,
|
||||
DisableOverdrive,
|
||||
ResetConfig,
|
||||
|
||||
@@ -268,11 +268,14 @@ impl relm4::SimpleComponent for SoftwarePage {
|
||||
device_api_info: None,
|
||||
};
|
||||
|
||||
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
|
||||
let mut daemon_version = format!(
|
||||
"{}-{}",
|
||||
system_info.version.version, system_info.version.profile
|
||||
);
|
||||
if embedded {
|
||||
daemon_version.push_str("-embedded");
|
||||
}
|
||||
if let Some(commit) = &system_info.commit {
|
||||
if let Some(commit) = &system_info.version.commit {
|
||||
let daemon_commit_link = format!("{REPO_URL}/commit/{commit}");
|
||||
write!(
|
||||
daemon_version,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod service_setup;
|
||||
|
||||
use std::{
|
||||
panic,
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
pub mod systemd;
|
||||
|
||||
use crate::service_setup::systemd::{START_MODE_REPLACE, UnitProxy};
|
||||
use crate::{GUI_VERSION, I18N};
|
||||
use adw::prelude::*;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use i18n_embed_fl::fl;
|
||||
use lact_client::DaemonClient;
|
||||
use lact_schema::{GIT_COMMIT, SystemInfo, VersionInfo};
|
||||
use relm4::css::{self, WARNING};
|
||||
use relm4::{
|
||||
AsyncComponentSender, RelmWidgetExt,
|
||||
css::{ERROR, SUCCESS},
|
||||
prelude::{AsyncComponent, AsyncComponentParts},
|
||||
tokio,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct ServiceSetupDialog {
|
||||
connection_status: ConnectionStatus,
|
||||
unit_proxy: UnitProxy<'static>,
|
||||
service_logs: gtk::TextBuffer,
|
||||
service_state: String,
|
||||
setup_error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
pub struct ServiceSetupDialogParams {
|
||||
pub parent: gtk::ApplicationWindow,
|
||||
pub initial_client: anyhow::Result<(DaemonClient, SystemInfo)>,
|
||||
pub unit_proxy: UnitProxy<'static>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ServiceSetupDialogMsg {
|
||||
Reconnect,
|
||||
StartService,
|
||||
RestartService,
|
||||
StopService,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
impl AsyncComponent for ServiceSetupDialog {
|
||||
type Init = ServiceSetupDialogParams;
|
||||
type Input = ServiceSetupDialogMsg;
|
||||
type Output = Option<DaemonClient>;
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
adw::Dialog {
|
||||
set_content_width: 500,
|
||||
set_title: "Service Setup",
|
||||
|
||||
connect_closed => ServiceSetupDialogMsg::Close,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
set_margin_horizontal: 15,
|
||||
set_margin_vertical: 5,
|
||||
|
||||
gtk::Label {
|
||||
set_markup: &fl!(I18N, "service-explanation"),
|
||||
set_wrap: true,
|
||||
set_xalign: 0.0,
|
||||
set_margin_all: 10,
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
set_hexpand: true,
|
||||
set_margin_horizontal: 10,
|
||||
|
||||
gtk::Label {
|
||||
set_markup: &format!("<b>{}</b>", fl!(I18N, "service-connection-status")),
|
||||
set_size_group: &label_size_group,
|
||||
set_xalign: 0.0,
|
||||
set_yalign: 0.0,
|
||||
},
|
||||
|
||||
gtk::Label {
|
||||
#[watch]
|
||||
set_markup: &match &model.connection_status {
|
||||
ConnectionStatus::Connected {..} => fl!(I18N, "service-connected"),
|
||||
ConnectionStatus::Error(msg) => msg.clone(),
|
||||
},
|
||||
#[watch]
|
||||
set_css_classes: if model.connection_status.is_connected() { &[SUCCESS] } else { &[ERROR] },
|
||||
set_selectable: true,
|
||||
set_hexpand: true,
|
||||
set_halign: gtk::Align::End,
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
set_hexpand: true,
|
||||
set_margin_horizontal: 10,
|
||||
|
||||
gtk::Label {
|
||||
set_markup: &format!("<b>{}</b>", fl!(I18N, "service-status")),
|
||||
set_size_group: &label_size_group,
|
||||
set_xalign: 0.0,
|
||||
set_yalign: 0.0,
|
||||
},
|
||||
|
||||
gtk::Label {
|
||||
#[watch]
|
||||
set_markup: &format!("<tt>{}</tt>", model.service_state),
|
||||
set_wrap: true,
|
||||
set_hexpand: true,
|
||||
set_halign: gtk::Align::End,
|
||||
},
|
||||
|
||||
gtk::MenuButton {
|
||||
set_icon_name: "utilities-terminal-symbolic",
|
||||
add_css_class: css::FLAT,
|
||||
set_valign: gtk::Align::Center,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_popover = >k::Popover {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
set_margin_all: 10,
|
||||
|
||||
gtk::Label {
|
||||
set_label: &fl!(I18N, "service-logs"),
|
||||
},
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_min_content_width: 650,
|
||||
set_min_content_height: 250,
|
||||
|
||||
gtk::TextView {
|
||||
set_editable: false,
|
||||
set_buffer: Some(&model.service_logs),
|
||||
set_top_margin: 5,
|
||||
set_bottom_margin: 5,
|
||||
set_left_margin: 5,
|
||||
set_right_margin: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
set_hexpand: true,
|
||||
set_margin_horizontal: 10,
|
||||
|
||||
gtk::Label {
|
||||
set_markup: &format!("<b>{}</b>", fl!(I18N, "service-version")),
|
||||
set_size_group: &label_size_group,
|
||||
set_xalign: 0.0,
|
||||
set_yalign: 0.0,
|
||||
},
|
||||
|
||||
gtk::Label {
|
||||
#[watch]
|
||||
set_markup: &model.service_version_text().unwrap_or_else(|| fl!(I18N, "missing-stat")),
|
||||
#[watch]
|
||||
set_css_classes: match &model.connection_status {
|
||||
ConnectionStatus::Connected { version, .. } => {
|
||||
if version.is_current() {
|
||||
&[SUCCESS]
|
||||
} else {
|
||||
&[WARNING]
|
||||
}
|
||||
}
|
||||
ConnectionStatus::Error(_) => &[],
|
||||
},
|
||||
set_wrap: true,
|
||||
set_hexpand: true,
|
||||
set_halign: gtk::Align::End,
|
||||
},
|
||||
|
||||
gtk::MenuButton {
|
||||
set_icon_name: "dialog-warning-symbolic",
|
||||
add_css_class: css::FLAT,
|
||||
set_valign: gtk::Align::Center,
|
||||
#[watch]
|
||||
set_visible: model.daemon_version().is_some_and(|version| !version.is_current()),
|
||||
|
||||
#[wrap(Some)]
|
||||
set_popover = >k::Popover {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
set_margin_all: 5,
|
||||
|
||||
gtk::Label {
|
||||
#[watch]
|
||||
set_label: &fl!(
|
||||
I18N,
|
||||
"version-mismatch-description",
|
||||
gui_version = GUI_VERSION,
|
||||
gui_commit = GIT_COMMIT,
|
||||
daemon_version = model.daemon_version().map(|version| version.version.as_str()).unwrap_or_default(),
|
||||
daemon_commit = model.daemon_version().and_then(|version| version.commit.as_deref()).unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
gtk::Label {
|
||||
#[watch]
|
||||
set_visible: model.setup_error.is_some(),
|
||||
#[watch]
|
||||
set_text: &model.setup_error.as_ref().map(|err| format!("Setup error: {err}")).unwrap_or_default(),
|
||||
set_css_classes: &[ERROR],
|
||||
set_selectable: true,
|
||||
set_hexpand: true,
|
||||
},
|
||||
},
|
||||
|
||||
add_bottom_bar = >k::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
set_halign: gtk::Align::End,
|
||||
set_margin_horizontal: 25,
|
||||
set_margin_vertical: 20,
|
||||
|
||||
gtk::Button {
|
||||
set_label: &fl!(I18N, "service-start"),
|
||||
connect_clicked => ServiceSetupDialogMsg::StartService,
|
||||
add_css_class: "suggested-action",
|
||||
#[watch]
|
||||
set_visible: model.service_state != systemd::UNIT_STATE_ACTIVE,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: &fl!(I18N, "service-stop"),
|
||||
connect_clicked => ServiceSetupDialogMsg::StopService,
|
||||
#[watch]
|
||||
set_visible: model.service_state == systemd::UNIT_STATE_ACTIVE,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: &fl!(I18N, "service-restart"),
|
||||
connect_clicked => ServiceSetupDialogMsg::RestartService,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
params: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let input_sender = sender.input_sender().clone();
|
||||
relm4::spawn(async move {
|
||||
loop {
|
||||
if input_sender.send(ServiceSetupDialogMsg::Reconnect).is_err() {
|
||||
debug!("service setup dialog closed, exiting client watcher");
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
});
|
||||
|
||||
let (service_state, setup_error) = params
|
||||
.unit_proxy
|
||||
.active_state()
|
||||
.await
|
||||
.map(|state| (state, None))
|
||||
.unwrap_or_else(|err| {
|
||||
(
|
||||
"unknown".to_owned(),
|
||||
Some(anyhow!("Could not fetch service status: {err}")),
|
||||
)
|
||||
});
|
||||
|
||||
let service_logs_handle = tokio::spawn(service_logs_text());
|
||||
|
||||
let connection_status = match params.initial_client {
|
||||
Ok((client, info)) => ConnectionStatus::Connected {
|
||||
client: client.clone(),
|
||||
version: info.version,
|
||||
},
|
||||
Err(err) => ConnectionStatus::from_result(Err(err)).await,
|
||||
};
|
||||
|
||||
let model = Self {
|
||||
connection_status,
|
||||
unit_proxy: params.unit_proxy,
|
||||
service_logs: gtk::TextBuffer::builder()
|
||||
.text(service_logs_handle.await.unwrap())
|
||||
.build(),
|
||||
service_state,
|
||||
setup_error,
|
||||
};
|
||||
|
||||
let label_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
root.present(Some(¶ms.parent));
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
msg: Self::Input,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
if let Err(err) = self.handle_msg(msg, sender).await {
|
||||
self.setup_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceSetupDialog {
|
||||
async fn handle_msg(
|
||||
&mut self,
|
||||
msg: ServiceSetupDialogMsg,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> anyhow::Result<()> {
|
||||
match msg {
|
||||
ServiceSetupDialogMsg::Reconnect => (),
|
||||
ServiceSetupDialogMsg::StartService => {
|
||||
self.unit_proxy.start(START_MODE_REPLACE).await?;
|
||||
}
|
||||
ServiceSetupDialogMsg::RestartService => {
|
||||
self.unit_proxy.restart(START_MODE_REPLACE).await?;
|
||||
}
|
||||
ServiceSetupDialogMsg::StopService => {
|
||||
self.unit_proxy.stop(START_MODE_REPLACE).await?;
|
||||
}
|
||||
ServiceSetupDialogMsg::Close => {
|
||||
let client = match &self.connection_status {
|
||||
ConnectionStatus::Connected { client, .. } => Some(client.clone()),
|
||||
ConnectionStatus::Error(_) => None,
|
||||
};
|
||||
let _ = sender.output(client);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.reconnect().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reconnect(&mut self) -> anyhow::Result<()> {
|
||||
let logs_handle = tokio::spawn(service_logs_text());
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
let new_state = self
|
||||
.unit_proxy
|
||||
.active_state()
|
||||
.await
|
||||
.context("Could not update unit state")?;
|
||||
|
||||
if self.service_state != new_state {
|
||||
self.service_state = new_state;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
let client = DaemonClient::connect_with_reconnect(false).await;
|
||||
|
||||
let connection_status = ConnectionStatus::from_result(client).await;
|
||||
changed |= !self.connection_status.roughly_eq(&connection_status);
|
||||
self.connection_status = connection_status;
|
||||
|
||||
let logs = logs_handle.await.unwrap();
|
||||
|
||||
let current_text = self.service_logs.slice(
|
||||
&self.service_logs.start_iter(),
|
||||
&self.service_logs.end_iter(),
|
||||
true,
|
||||
);
|
||||
|
||||
if logs != current_text.as_str() {
|
||||
self.service_logs.set_text(&logs);
|
||||
}
|
||||
|
||||
if changed {
|
||||
self.setup_error = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn service_version_text(&self) -> Option<String> {
|
||||
match &self.connection_status {
|
||||
ConnectionStatus::Connected { version, .. } => {
|
||||
let mut text = format!("<tt>{}</tt>", format_version(version));
|
||||
|
||||
if !version.is_current() {
|
||||
write!(text, " ({})", fl!(I18N, "service-version-mismatch")).unwrap();
|
||||
}
|
||||
|
||||
Some(text)
|
||||
}
|
||||
ConnectionStatus::Error(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn daemon_version(&self) -> Option<&VersionInfo> {
|
||||
match &self.connection_status {
|
||||
ConnectionStatus::Connected { version, .. } => Some(version),
|
||||
ConnectionStatus::Error(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnectionStatus {
|
||||
Connected {
|
||||
client: DaemonClient,
|
||||
version: VersionInfo,
|
||||
},
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl ConnectionStatus {
|
||||
async fn from_result(result: anyhow::Result<DaemonClient>) -> Self {
|
||||
match result {
|
||||
Ok(client) => match client.get_system_info().await {
|
||||
Ok(info) => Self::Connected {
|
||||
client,
|
||||
version: info.version,
|
||||
},
|
||||
Err(err) => Self::from_err(err),
|
||||
},
|
||||
Err(err) => Self::from_err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_err(err: anyhow::Error) -> Self {
|
||||
let msg = if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
|
||||
match io_err.kind() {
|
||||
io::ErrorKind::NotFound => fl!(I18N, "service-not-running"),
|
||||
io::ErrorKind::PermissionDenied => fl!(I18N, "service-permission-denied"),
|
||||
_ => format!("{} (IO {io_err:#})", fl!(I18N, "error-heading")),
|
||||
}
|
||||
} else {
|
||||
format!("{} ({err:#})", fl!(I18N, "error-heading"))
|
||||
};
|
||||
Self::Error(msg)
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
match self {
|
||||
Self::Connected { .. } => true,
|
||||
Self::Error(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn roughly_eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
ConnectionStatus::Connected {
|
||||
version: version_l, ..
|
||||
},
|
||||
ConnectionStatus::Connected {
|
||||
version: version_r, ..
|
||||
},
|
||||
) => version_l == version_r,
|
||||
(ConnectionStatus::Error(err_l), ConnectionStatus::Error(err_r)) => err_l == err_r,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_version(version: &VersionInfo) -> String {
|
||||
format!("{}-{}", version.version, version.profile)
|
||||
}
|
||||
|
||||
async fn service_logs_text() -> String {
|
||||
systemd::fetch_logs()
|
||||
.await
|
||||
.unwrap_or_else(|err| format!("Could not fetch logs: {err:#}"))
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! # D-Bus interface proxy for: `org.freedesktop.systemd1.Manager`
|
||||
use anyhow::{Context, bail};
|
||||
use relm4::tokio::process::Command;
|
||||
use zbus::{proxy, zvariant::OwnedObjectPath};
|
||||
|
||||
const UNIT_NAME: &str = "lactd.service";
|
||||
|
||||
pub const UNIT_STATE_ACTIVE: &str = "active";
|
||||
|
||||
pub const START_MODE_REPLACE: &str = "replace";
|
||||
|
||||
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 {
|
||||
#[zbus(allow_interactive_auth)]
|
||||
fn restart(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
|
||||
|
||||
#[zbus(allow_interactive_auth)]
|
||||
fn start(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
|
||||
|
||||
#[zbus(allow_interactive_auth)]
|
||||
fn stop(&self, mode: &str) -> zbus::Result<OwnedObjectPath>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn active_state(&self) -> zbus::Result<String>;
|
||||
}
|
||||
|
||||
pub async fn fetch_logs() -> anyhow::Result<String> {
|
||||
let output = Command::new("journalctl")
|
||||
.args([
|
||||
"-I",
|
||||
"--no-hostname",
|
||||
"--no-pager",
|
||||
"-o",
|
||||
"short",
|
||||
"-u",
|
||||
UNIT_NAME,
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Could not run journalctl")?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("journalctl exited with status {}", output.status);
|
||||
}
|
||||
|
||||
String::from_utf8(output.stdout).context("Could not parse journalctl output")
|
||||
}
|
||||
+30
-3
@@ -73,15 +73,42 @@ pub struct Pong;
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SystemInfo {
|
||||
pub version: String,
|
||||
pub commit: Option<String>,
|
||||
pub profile: String,
|
||||
#[serde(flatten)]
|
||||
pub version: VersionInfo,
|
||||
pub distro: Option<String>,
|
||||
pub kernel_version: String,
|
||||
pub amdgpu_overdrive_enabled: Option<bool>,
|
||||
pub amdgpu_params_configurator: Option<AmdgpuParamsConfigurator>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VersionInfo {
|
||||
pub version: String,
|
||||
pub commit: Option<String>,
|
||||
pub profile: String,
|
||||
}
|
||||
|
||||
impl VersionInfo {
|
||||
pub fn current() -> Self {
|
||||
let profile = if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
}
|
||||
.to_owned();
|
||||
Self {
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
commit: Some(GIT_COMMIT.to_owned()),
|
||||
profile,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_current(&self) -> bool {
|
||||
*self == Self::current()
|
||||
}
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceListEntry {
|
||||
|
||||
@@ -17,7 +17,7 @@ fn ping_requset() {
|
||||
fn pong_response() {
|
||||
let expected_response = json!({
|
||||
"status": "ok",
|
||||
"data": null
|
||||
"data": null,
|
||||
});
|
||||
let response = Response::Ok(Pong);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user