Refactor ROGCC to use dbus to communicate with self instead of pipe file

This commit is contained in:
Luke D. Jones
2024-12-24 12:59:19 +13:00
parent 0f2d89858e
commit ab7a4bbad3
8 changed files with 250 additions and 253 deletions

View File

@@ -18,8 +18,6 @@ tokio-debug = ["console-subscriber"]
[dependencies]
console-subscriber = { version = "^0.4", optional = true }
nix = { version = "^0.29.0", features = ["fs"] }
tempfile = "3.3.0"
ksni = { version = "0.3", default-features = false, features = ["async-io"] }
image = "0.25.5"

View File

@@ -5,7 +5,6 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Nix(nix::Error),
ConfigLoadFail,
ConfigLockFail,
XdgVars,
@@ -18,7 +17,6 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(err) => write!(f, "Failed to open: {}", err),
Error::Nix(err) => write!(f, "Error: {}", err),
Error::ConfigLoadFail => write!(f, "Failed to load user config"),
Error::ConfigLockFail => write!(f, "Failed to lock user config"),
Error::XdgVars => write!(f, "XDG environment vars appear unset"),
@@ -36,12 +34,6 @@ impl From<std::io::Error> for Error {
}
}
impl From<nix::Error> for Error {
fn from(err: nix::Error) -> Self {
Error::Nix(err)
}
}
impl From<zbus::Error> for Error {
fn from(err: zbus::Error) -> Self {
Error::Zbus(err)

View File

@@ -2,14 +2,8 @@
#![allow(clippy::redundant_clone, clippy::cmp_owned)]
slint::include_modules!();
// Intentionally reexport slint so that GUI consumers don't need to add to
// `Cargo.toml`
use std::fs::{remove_dir_all, File, OpenOptions};
use std::io::{Read, Write};
use std::process::exit;
use std::thread::sleep;
use std::time::Duration;
/// Intentionally reexport slint so that GUI consumers don't need to add to
/// `Cargo.toml`
pub use slint;
pub mod cli_options;
@@ -21,11 +15,7 @@ pub mod notify;
pub mod tray;
pub mod types;
pub mod ui;
use nix::sys::stat;
use nix::unistd;
use tempfile::TempDir;
// use log::{error, info, warn};
pub mod zbus;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const APP_ICON_PATH: &str = "/usr/share/icons/hicolor/512x512/apps/rog-control-center.png";
@@ -42,10 +32,6 @@ pub fn print_versions() {
println!("rog-platform v{}", rog_platform::VERSION);
}
pub const SHOWING_GUI: u8 = 1;
pub const SHOW_GUI: u8 = 2;
pub const QUIT_APP: u8 = 3;
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum Page {
AppSettings,
@@ -54,59 +40,3 @@ pub enum Page {
AnimeMatrix,
FanCurves,
}
/// Either exit the process, or return with a refreshed tmp-dir
pub fn on_tmp_dir_exists() -> Result<TempDir, std::io::Error> {
let mut buf = [0u8; 2];
let path = std::env::temp_dir().join("rog-gui");
if path.read_dir()?.next().is_none() {
std::fs::remove_dir_all(path)?;
return tempfile::Builder::new()
.prefix("rog-gui")
.rand_bytes(0)
.tempdir();
}
let mut ipc_file = OpenOptions::new()
.read(true)
.write(true)
.create(false)
.open(path.join("ipc.pipe"))?;
// If the app is running this ends up stacked on top of SHOWING_GUI
ipc_file.write_all(&[SHOW_GUI, 0])?;
// tiny sleep to give the app a chance to respond
sleep(Duration::from_millis(10));
ipc_file.read_exact(&mut buf).ok();
// First entry is the actual state
if buf[0] == SHOWING_GUI {
ipc_file.write_all(&[SHOWING_GUI, 0])?; // Store state again as we drained the fifo
// Early exit is not an error and we don't want to pass back a dir
#[allow(clippy::exit)]
exit(0);
} else if buf[0] == SHOW_GUI {
remove_dir_all(&path)?;
return tempfile::Builder::new()
.prefix("rog-gui")
.rand_bytes(0)
.tempdir();
}
panic!("Invalid exit or app state");
}
pub fn get_ipc_file() -> Result<File, crate::error::Error> {
let tmp_dir = std::env::temp_dir().join("rog-gui");
let fifo_path = tmp_dir.join("ipc.pipe");
if let Err(e) = unistd::mkfifo(&fifo_path, stat::Mode::S_IRWXU) {
if !matches!(e, nix::errno::Errno::EEXIST) {
Err(e)?
}
}
Ok(OpenOptions::new()
.read(true)
.write(true)
// .truncate(true)
.open(&fifo_path)?)
}

View File

@@ -1,6 +1,5 @@
use std::borrow::BorrowMut;
use std::env::args;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::exit;
use std::sync::{Arc, Mutex};
@@ -10,7 +9,7 @@ use std::time::Duration;
use config_traits::{StdConfig, StdConfigLoad1};
use dmi_id::DMIID;
use gumdrop::Options;
use log::{info, LevelFilter};
use log::{info, warn, LevelFilter};
use rog_control_center::cli_options::CliStart;
use rog_control_center::config::Config;
use rog_control_center::error::Result;
@@ -18,41 +17,71 @@ use rog_control_center::notify::start_notifications;
use rog_control_center::slint::ComponentHandle;
use rog_control_center::tray::init_tray;
use rog_control_center::ui::setup_window;
use rog_control_center::{
get_ipc_file, on_tmp_dir_exists, print_versions, MainWindow, QUIT_APP, SHOWING_GUI, SHOW_GUI,
use rog_control_center::zbus::{
AppState, ROGCCZbus, ROGCCZbusProxyBlocking, ZBUS_IFACE, ZBUS_PATH,
};
use rog_control_center::{print_versions, MainWindow};
use tokio::runtime::Runtime;
#[tokio::main]
async fn main() -> Result<()> {
#[cfg(feature = "tokio-debug")]
console_subscriber::init();
let mut logger = env_logger::Builder::new();
logger
.filter_level(LevelFilter::Warn)
.parse_default_env()
.target(env_logger::Target::Stdout)
.format_timestamp(None)
.init();
// Try to open a proxy and check for app state first
{
let user_con = zbus::blocking::Connection::session()?;
if let Ok(proxy) = ROGCCZbusProxyBlocking::new(&user_con) {
if let Ok(state) = proxy.state() {
info!("App is already running: {state:?}, opening the window");
// if there is a proxy connection assume the app is already running
proxy.set_state(AppState::MainWindowShouldOpen)?;
std::process::exit(0);
}
}
}
// version checks
let self_version = env!("CARGO_PKG_VERSION");
let conn = zbus::blocking::Connection::system()?;
let proxy = rog_dbus::zbus_platform::PlatformProxyBlocking::new(&conn)?;
let asusd_version = proxy.version().unwrap();
let zbus_con = zbus::blocking::Connection::system()?;
let platform_proxy = rog_dbus::zbus_platform::PlatformProxyBlocking::new(&zbus_con)?;
let asusd_version = platform_proxy.version().unwrap();
if asusd_version != self_version {
println!("Version mismatch: asusctl = {self_version}, asusd = {asusd_version}");
return Ok(());
}
// start tokio
let rt = Runtime::new().expect("Unable to create Runtime");
// Enter the runtime so that `tokio::spawn` is available immediately.
let _enter = rt.enter();
#[cfg(feature = "tokio-debug")]
console_subscriber::init();
let state_zbus = ROGCCZbus::new();
let app_state = state_zbus.clone_state();
let _conn = zbus::connection::Builder::session()?
.name(ZBUS_IFACE)?
.serve_at(ZBUS_PATH, state_zbus)?
.build()
.await
.map_err(|err| {
warn!("{}: add_to_server {}", ZBUS_PATH, err);
err
})?;
let dmi = DMIID::new().unwrap_or_default();
let board_name = dmi.board_name;
let prod_family = dmi.product_family;
info!("Running on {board_name}, product: {prod_family}");
let is_rog_ally = prod_family == "RC71L";
// tmp-dir must live to the end of program life
let _tmp_dir = match tempfile::Builder::new()
.prefix("rog-gui")
.rand_bytes(0)
.tempdir()
{
Ok(tmp) => tmp,
Err(_) => on_tmp_dir_exists().unwrap(),
};
let args: Vec<String> = args().skip(1).collect();
let cli_parsed = match CliStart::parse_args_default(&args) {
@@ -66,15 +95,7 @@ async fn main() -> Result<()> {
return Ok(());
}
let mut logger = env_logger::Builder::new();
logger
.filter_level(LevelFilter::Warn)
.parse_default_env()
.target(env_logger::Target::Stdout)
.format_timestamp(None)
.init();
let supported_properties = proxy.supported_properties().unwrap_or_default();
let supported_properties = platform_proxy.supported_properties().unwrap_or_default();
// Startup
let mut config = Config::new().load();
@@ -99,8 +120,6 @@ async fn main() -> Result<()> {
if config.startup_in_background {
config.run_in_background = true;
} else {
get_ipc_file().unwrap().write_all(&[SHOW_GUI, 0]).unwrap();
}
config.write();
@@ -108,10 +127,6 @@ async fn main() -> Result<()> {
let startup_in_background = config.startup_in_background;
let config = Arc::new(Mutex::new(config));
// start tokio
let rt = Runtime::new().expect("Unable to create Runtime");
// Enter the runtime so that `tokio::spawn` is available immediately.
let _enter = rt.enter();
start_notifications(config.clone(), &rt)?;
if enable_tray_icon {
@@ -121,7 +136,11 @@ async fn main() -> Result<()> {
thread_local! { pub static UI: std::cell::RefCell<Option<MainWindow>> = Default::default()};
// i_slint_backend_selector::with_platform(|_| Ok(())).unwrap();
let mut do_once = !startup_in_background;
if !startup_in_background {
if let Ok(mut lock) = app_state.lock() {
*lock = AppState::MainWindowShouldOpen;
}
}
if std::env::var("RUST_TRANSLATIONS").is_ok() {
// don't care about content
@@ -133,42 +152,40 @@ async fn main() -> Result<()> {
}
thread::spawn(move || {
let mut buf = [0u8; 2];
// blocks until it is read, typically the read will happen after a second
// process writes to the IPC (so there is data to actually read)
let mut state = AppState::StartingUp;
loop {
if do_once {
buf[0] = SHOW_GUI;
do_once = false;
} else {
get_ipc_file().unwrap().read_exact(&mut buf).unwrap();
// save as a var, don't hold the lock the entire time or deadlocks happen
if let Ok(lock) = app_state.lock() {
state = *lock;
}
if buf[0] == SHOW_GUI {
// There's a balancing act with read/write timing of IPC, there needs to be a
// small sleep after this to give any other process a chance to
// read the IPC before looping
get_ipc_file()
.unwrap()
.write_all(&[SHOWING_GUI, 0])
.unwrap();
if state == AppState::MainWindowShouldOpen {
if let Ok(mut lock) = app_state.lock() {
*lock = AppState::MainWindowOpen;
}
sleep(Duration::from_millis(50));
let config_copy = config.clone();
let app_state_copy = app_state.clone();
slint::invoke_from_event_loop(move || {
UI.with(|ui| {
let app_state_copy = app_state_copy.clone();
let mut ui = ui.borrow_mut();
if let Some(ui) = ui.as_mut() {
ui.window().show().unwrap();
ui.window().on_close_requested(|| {
get_ipc_file().unwrap().write_all(&[0, 0]).unwrap();
ui.window().on_close_requested(move || {
if let Ok(mut lock) = app_state_copy.lock() {
*lock = AppState::MainWindowClosed;
}
slint::CloseRequestResponse::HideWindow
});
} else {
let newui = setup_window(config_copy);
newui.window().show().unwrap();
newui.window().on_close_requested(|| {
get_ipc_file().unwrap().write_all(&[0, 0]).unwrap();
newui.window().on_close_requested(move || {
if let Ok(mut lock) = app_state_copy.lock() {
*lock = AppState::MainWindowClosed;
}
slint::CloseRequestResponse::HideWindow
});
ui.replace(newui);
@@ -176,29 +193,26 @@ async fn main() -> Result<()> {
});
})
.unwrap();
} else {
if buf[1] == QUIT_APP {
slint::quit_event_loop().unwrap();
exit(0);
}
if buf[0] != SHOWING_GUI {
if let Ok(lock) = config.lock() {
if !lock.run_in_background {
slint::quit_event_loop().unwrap();
exit(0);
}
} else if state == AppState::QuitApp {
slint::quit_event_loop().unwrap();
exit(0);
} else if state != AppState::MainWindowOpen {
if let Ok(lock) = config.lock() {
if !lock.run_in_background {
slint::quit_event_loop().unwrap();
exit(0);
}
slint::invoke_from_event_loop(move || {
UI.with(|ui| {
let mut ui = ui.take();
if let Some(ui) = ui.borrow_mut() {
ui.window().hide().unwrap();
}
});
})
.unwrap();
}
slint::invoke_from_event_loop(move || {
UI.with(|ui| {
let mut ui = ui.take();
if let Some(ui) = ui.borrow_mut() {
ui.window().hide().unwrap();
}
});
})
.unwrap();
}
}
});

View File

@@ -1,22 +1,20 @@
//! A seld-contained tray icon with menus. The control of app<->tray is done via
//! commands over an MPSC channel.
//! A self-contained tray icon with menus.
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use ksni::{Handle, Icon, TrayMethods};
// use betrayer::{Icon, Menu, MenuItem, TrayEvent, TrayIcon, TrayIconBuilder};
use log::{error, info, warn};
use log::{info, warn};
use rog_platform::platform::Properties;
use supergfxctl::pci_device::{Device, GfxMode, GfxPower};
use supergfxctl::zbus_proxy::DaemonProxy as GfxProxy;
use versions::Versioning;
use crate::config::Config;
use crate::{get_ipc_file, QUIT_APP, SHOW_GUI};
use crate::zbus::{AppState, ROGCCZbusProxyBlocking};
const TRAY_LABEL: &str = "ROG Control Center";
const TRAY_ICON_PATH: &str = "/usr/share/icons/hicolor/512x512/apps/";
@@ -31,15 +29,6 @@ struct Icons {
static ICONS: OnceLock<Icons> = OnceLock::new();
fn toggle_app(open: bool) {
if let Ok(mut ipc) = get_ipc_file().map_err(|e| {
error!("ROGTray: get_ipc_file: {}", e);
}) {
let action = if open { SHOW_GUI } else { QUIT_APP };
ipc.write_all(&[action, 0]).ok();
}
}
fn read_icon(file: &Path) -> Icon {
let mut path = PathBuf::from(TRAY_ICON_PATH);
path.push(file);
@@ -66,6 +55,7 @@ fn read_icon(file: &Path) -> Icon {
struct AsusTray {
current_title: String,
current_icon: Icon,
proxy: ROGCCZbusProxyBlocking<'static>,
}
impl ksni::Tray for AsusTray {
@@ -91,7 +81,9 @@ impl ksni::Tray for AsusTray {
StandardItem {
label: "Open ROGCC".into(),
icon_name: "rog-control-center".into(),
activate: Box::new(|_| toggle_app(true)),
activate: Box::new(move |s: &mut AsusTray| {
s.proxy.set_state(AppState::MainWindowShouldOpen).ok();
}),
..Default::default()
}
.into(),
@@ -163,11 +155,15 @@ fn find_dgpu() -> Option<Device> {
/// The tray is controlled somewhat by `Arc<Mutex<SystemState>>`
pub fn init_tray(_supported_properties: Vec<Properties>, config: Arc<Mutex<Config>>) {
tokio::spawn(async move {
let user_con = zbus::blocking::Connection::session().unwrap();
let proxy = ROGCCZbusProxyBlocking::new(&user_con).unwrap();
let rog_red = read_icon(&PathBuf::from("asus_notif_red.png"));
let tray = AsusTray {
current_title: TRAY_LABEL.to_string(),
current_icon: rog_red.clone(),
proxy,
};
let mut tray = tray

View File

@@ -0,0 +1,68 @@
use std::sync::{Arc, Mutex};
use zbus::zvariant::{OwnedValue, Type, Value};
use zbus::{interface, proxy};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Type, Value, OwnedValue)]
#[zvariant(signature = "u")]
pub enum AppState {
MainWindowOpen = 0,
/// If the app is running, open the main window
MainWindowShouldOpen = 1,
MainWindowClosed = 2,
StartingUp = 3,
QuitApp = 4,
LockFailed = 5,
}
pub struct ROGCCZbus {
state: Arc<Mutex<AppState>>,
}
impl ROGCCZbus {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(AppState::StartingUp)),
}
}
pub fn clone_state(&self) -> Arc<Mutex<AppState>> {
self.state.clone()
}
}
pub const ZBUS_PATH: &str = "/xyz/ljones/rogcc";
pub const ZBUS_IFACE: &str = "xyz.ljones.rogcc";
#[interface(name = "xyz.ljones.rogcc")]
impl ROGCCZbus {
/// Return the device type for this Aura keyboard
#[zbus(property)]
async fn state(&self) -> AppState {
if let Ok(lock) = self.state.try_lock() {
return *lock;
}
AppState::LockFailed
}
#[zbus(property)]
async fn set_state(&self, state: AppState) {
if let Ok(mut lock) = self.state.try_lock() {
*lock = state;
}
}
}
#[proxy(
interface = "xyz.ljones.rogcc",
default_service = "xyz.ljones.rogcc",
default_path = "/xyz/ljones/rogcc"
)]
pub trait ROGCCZbus {
/// EnableDisplay property
#[zbus(property)]
fn state(&self) -> zbus::Result<AppState>;
#[zbus(property)]
fn set_state(&self, state: AppState) -> zbus::Result<()>;
}