mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-05 23:16:23 -06:00
271 lines
8.8 KiB
Rust
271 lines
8.8 KiB
Rust
use std::{ffi::OsString};
|
|
|
|
use log::{info, debug};
|
|
use log4rs::Handle;
|
|
use openrgb::OpenRGB;
|
|
use windows_service::{
|
|
service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType},
|
|
service_control_handler::{self, ServiceControlHandlerResult},
|
|
service_manager::{ServiceManager, ServiceManagerAccess},
|
|
};
|
|
|
|
use std::time::Duration;
|
|
use tokio::{net::TcpStream, runtime::Runtime, sync::mpsc};
|
|
|
|
use tokio::sync::mpsc::UnboundedReceiver;
|
|
use windows_service::{define_windows_service, service_dispatcher};
|
|
|
|
const SERVICE_NAME: &str = "RGBXevion";
|
|
const SERVICE_DESCRIPTION: &str = "Custom service to toggle RGB lights based on lock/sleep events";
|
|
|
|
const PROFILE_ENABLE_NAME: &str = "On";
|
|
const PROFILE_DISABLE_NAME: &str = "Off";
|
|
|
|
define_windows_service!(ffi_service_main, service_main);
|
|
|
|
pub async fn try_load_profile(client: &OpenRGB<TcpStream>, profile_name: &str) -> anyhow::Result<()> {
|
|
let profiles = client.get_profiles().await?;
|
|
|
|
let profile_available: bool = profiles.iter().any(|profile| profile == profile_name);
|
|
if !profile_available {
|
|
info!("Profile not found: {}", profile_name);
|
|
return Ok(());
|
|
}
|
|
|
|
client.load_profile(profile_name).await?;
|
|
info!("Profile set to: {}", profile_name);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn profile_applier(
|
|
profile_recv: &mut UnboundedReceiver<bool>,
|
|
shutdown_recv: &mut UnboundedReceiver<()>,
|
|
) -> anyhow::Result<()> {
|
|
|
|
let client = OpenRGB::connect().await?;
|
|
client.set_name(format!("{} v{}", SERVICE_NAME, env!("CARGO_PKG_VERSION"))).await?;
|
|
|
|
loop {
|
|
tokio::select! {
|
|
enable = profile_recv.recv() => {
|
|
debug!("Received profile command: {:?}", enable);
|
|
if enable.is_none() {
|
|
continue;
|
|
}
|
|
|
|
try_load_profile(&client, if enable.unwrap() { PROFILE_ENABLE_NAME } else { PROFILE_DISABLE_NAME }).await?;
|
|
}
|
|
_ = shutdown_recv.recv() => {
|
|
info!("Service shutting down");
|
|
return Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn service_main(_: Vec<OsString>) {
|
|
use windows_service::service::{
|
|
PowerEventParam, ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, SessionChangeReason
|
|
};
|
|
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel();
|
|
let (profile_send, mut profile_recv) = mpsc::unbounded_channel::<bool>();
|
|
|
|
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
|
match control_event {
|
|
ServiceControl::PowerEvent(event) =>
|
|
{
|
|
debug!("Power event: {:?}", event);
|
|
match event {
|
|
PowerEventParam::QuerySuspend => {
|
|
// Send false to disable RGB
|
|
profile_send.send(false).unwrap();
|
|
}
|
|
PowerEventParam::ResumeSuspend | PowerEventParam::QuerySuspendFailed => {
|
|
// Send true to enable RGB
|
|
profile_send.send(true).unwrap();
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
ServiceControlHandlerResult::NoError
|
|
}
|
|
ServiceControl::SessionChange(change) => {
|
|
debug!("Session change: {:?}", change);
|
|
|
|
match change.reason {
|
|
SessionChangeReason::SessionLock => {
|
|
// Send false to disable RGB
|
|
profile_send.send(false).unwrap();
|
|
}
|
|
SessionChangeReason::SessionUnlock => {
|
|
// Send true to enable RGB
|
|
profile_send.send(true).unwrap();
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
ServiceControlHandlerResult::NoError
|
|
}
|
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
|
ServiceControl::Stop => {
|
|
shutdown_send.send(()).unwrap();
|
|
ServiceControlHandlerResult::NoError
|
|
}
|
|
_ => ServiceControlHandlerResult::NotImplemented,
|
|
}
|
|
};
|
|
|
|
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler).unwrap();
|
|
status_handle
|
|
.set_service_status(ServiceStatus {
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
current_state: ServiceState::Running,
|
|
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SESSION_CHANGE | ServiceControlAccept::POWER_EVENT,
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})
|
|
.unwrap();
|
|
|
|
let args = std::env::args().collect::<Vec<_>>();
|
|
let rules_path = args.get(2).map(|s| s.as_str()).unwrap_or("rules.json");
|
|
|
|
let error_code = if rt
|
|
.block_on(profile_applier(&mut profile_recv, &mut shutdown_recv))
|
|
.is_err()
|
|
{
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
|
|
status_handle
|
|
.set_service_status(ServiceStatus {
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
current_state: ServiceState::Stopped,
|
|
controls_accepted: ServiceControlAccept::empty(),
|
|
exit_code: ServiceExitCode::Win32(error_code),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
fn init_logger() -> Handle {
|
|
use log::LevelFilter;
|
|
use log4rs::{
|
|
append::{console::ConsoleAppender, file::FileAppender},
|
|
config::{Appender, Root},
|
|
encode::pattern::PatternEncoder,
|
|
Config,
|
|
};
|
|
|
|
let stdout_appender = ConsoleAppender::builder().build();
|
|
|
|
let log_file_path = std::env::current_exe()
|
|
.unwrap()
|
|
.with_file_name("service.log");
|
|
|
|
let log_file_appender = FileAppender::builder()
|
|
.encoder(Box::new(PatternEncoder::new(
|
|
"{date} {level} {target} - {message}{n}",
|
|
)))
|
|
.build(log_file_path)
|
|
.unwrap();
|
|
|
|
let config = Config::builder()
|
|
.appender(Appender::builder().build("stdout", Box::new(stdout_appender)))
|
|
.appender(Appender::builder().build("logfile", Box::new(log_file_appender)))
|
|
.build(
|
|
Root::builder()
|
|
.appender("stdout")
|
|
.appender("logfile")
|
|
.build(LevelFilter::Trace),
|
|
)
|
|
.unwrap();
|
|
|
|
log4rs::init_config(config).unwrap()
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn main() -> anyhow::Result<(), windows_service::Error> {
|
|
let _ = init_logger();
|
|
|
|
let args = std::env::args().collect::<Vec<_>>();
|
|
let command = args.get(1);
|
|
|
|
debug!("Service control executed with args: {:?}", args);
|
|
|
|
if let Some(command) = command {
|
|
match command.as_str() {
|
|
"install" => {
|
|
install_service()?;
|
|
info!("Service installed");
|
|
return Ok(());
|
|
}
|
|
"uninstall" => {
|
|
uninstall_service()?;
|
|
info!("Service uninstalled");
|
|
return Ok(());
|
|
}
|
|
"run" => {
|
|
info!("Running service (nil)");
|
|
}
|
|
_ => {
|
|
info!("Unknown command");
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("Starting service");
|
|
match service_dispatcher::start(SERVICE_NAME, ffi_service_main) {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
info!("Error starting service: {:?}", e);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn install_service() -> windows_service::Result<()> {
|
|
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
|
|
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
|
|
let service_binary_path = ::std::env::current_exe().unwrap();
|
|
|
|
let service_info = ServiceInfo {
|
|
name: SERVICE_NAME.into(),
|
|
display_name: SERVICE_NAME.into(),
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
start_type: ServiceStartType::AutoStart,
|
|
error_control: ServiceErrorControl::Normal,
|
|
executable_path: service_binary_path,
|
|
launch_arguments: vec!["run".into()],
|
|
dependencies: vec![],
|
|
account_name: None, // run as System
|
|
account_password: None,
|
|
};
|
|
|
|
let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?;
|
|
service.set_description(SERVICE_DESCRIPTION)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn uninstall_service() -> windows_service::Result<()> {
|
|
let manager_access = ServiceManagerAccess::CONNECT;
|
|
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
|
|
let service = service_manager.open_service(SERVICE_NAME, ServiceAccess::DELETE)?;
|
|
service.delete()?;
|
|
Ok(())
|
|
}
|