From 97d3c2c04b89ee5475b5a0f23896dcff4f997592 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Fri, 5 Jun 2026 08:18:49 +0200 Subject: [PATCH] feat(272): support running plugins after service stop --- src/plugins.rs | 16 +++++++++++++ src/ui/app.rs | 44 +++++++++++++++++++++++++++++++++++- src/ui/job_worker/mod.rs | 5 ++++ src/ui/preferences_window.rs | 11 +++++++-- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/plugins.rs b/src/plugins.rs index 963b9c2..a405046 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -2,6 +2,13 @@ use freedesktop_desktop_entry::DesktopEntry; use std::path::Path; use tracing::warn; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum XRPluginExecutionTime { + #[default] + AfterStart, + AfterStop, +} + #[derive(Debug, Clone)] pub struct XRPlugin { pub name: String, @@ -11,6 +18,7 @@ pub struct XRPlugin { pub exec: String, /// the path to the .desktop file pub path: String, + pub execution_time: XRPluginExecutionTime, } impl XRPlugin { @@ -44,6 +52,14 @@ impl XRPlugin { icon: entry.icon().map(String::from), description: entry.comment(&[] as &[&str]).map(String::from), path: p.to_string_lossy().to_string(), + execution_time: dg + .entry("X-XR-Plugin-Execution-Time") + .map(|s| match s { + "after-start" => XRPluginExecutionTime::AfterStart, + "after-stop" => XRPluginExecutionTime::AfterStop, + _ => XRPluginExecutionTime::default(), + }) + .unwrap_or_default(), }) } } diff --git a/src/ui/app.rs b/src/ui/app.rs index c47b7ba..d25af70 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -35,6 +35,7 @@ use crate::{ linux_distro::LinuxDistro, openxr_prober::is_openxr_ready, paths::get_data_dir, + plugins::XRPluginExecutionTime, profile::{OvrCompatibilityModuleType, Profile, XRServiceType}, profiles::system_profile::system_profile, stateless_action, @@ -142,6 +143,7 @@ pub enum Msg { LaunchMonadoGui, OnMonadoGuiExit, ProfilesEnabledChanged(bool), + RunAfterStopPlugins, NoOp, } @@ -289,7 +291,13 @@ impl App { .config .get_xr_plugins() .into_iter() - .map(|plugin| plugin.exec) + .filter_map(|plugin| { + if plugin.execution_time == XRPluginExecutionTime::AfterStart { + Some(plugin.exec) + } else { + None + } + }) .collect::>() .join(" & "); if !plugins_cmd.is_empty() { @@ -461,6 +469,40 @@ impl AsyncComponent for App { if self.restart_xrservice { self.restart_xrservice = false; self.start_xrservice(sender, false); + } else { + // run AfterStop plugins + sender.input(Msg::RunAfterStopPlugins); + } + } + Msg::RunAfterStopPlugins => { + let prof = self.get_selected_profile(); + let plugins_cmd = self + .config + .get_xr_plugins() + .into_iter() + .filter_map(|plugin| { + if plugin.execution_time == XRPluginExecutionTime::AfterStop { + Some(plugin.exec) + } else { + None + } + }) + .collect::>() + .join(" & "); + if !plugins_cmd.is_empty() { + let mut jobs = VecDeque::new(); + jobs.push_back(WorkerJob::new_cmd( + Some(prof.environment.clone()), + "sh".into(), + Some(vec!["-c".into(), plugins_cmd]), + )); + let plugins_worker = + JobWorker::new(jobs, sender.input_sender(), |msg| match msg { + JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows), + JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code), + }); + plugins_worker.start(); + plugins_worker.detach(); } } Msg::OnPluginsExit(_) => self.plugins_worker = None, diff --git a/src/ui/job_worker/mod.rs b/src/ui/job_worker/mod.rs index d0a648e..d613d64 100644 --- a/src/ui/job_worker/mod.rs +++ b/src/ui/job_worker/mod.rs @@ -125,6 +125,11 @@ impl JobWorker { } } + /// drop the reference but continue the execution + pub fn detach(mut self) { + self.worker.detach_runtime(); + } + pub fn exit_code(&self) -> Option { if let Ok(state) = self.state.lock() { return state.exit_status; diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index c00e043..1dca065 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -2,7 +2,7 @@ use crate::{ config::Config, - plugins::XRPlugin, + plugins::{XRPlugin, XRPluginExecutionTime}, profile::OvrCompatibilityModuleType, ui::{ SENDER_IO_ERR_MSG, @@ -165,9 +165,16 @@ impl AsyncComponent for PreferencesWindow { Self::Input::PopulatePlugins => { if let Some(grp) = self.plugins_grp.as_ref() { for plugin in XRPlugin::find_plugins() { + let mut desc = plugin.description.clone().unwrap_or_default(); + if plugin.execution_time == XRPluginExecutionTime::AfterStop { + if !desc.is_empty() { + desc.push('\n'); + } + desc.push_str("Runs on service stop"); + } let row = switch_row( &plugin.name, - plugin.description.as_deref(), + if desc.is_empty() { None } else { Some(&desc) }, self.config.borrow().xr_plugins.contains(&plugin.path), clone!( #[strong(rename_to=config)] -- 2.51.2