From 96f7fead42d2bc08e703487cd104a46040354d8b Mon Sep 17 00:00:00 2001 From: Larsiiii Date: Wed, 8 Jan 2025 19:15:51 +0100 Subject: [PATCH] windows service --- src/api/mod.rs | 44 +--------- src/cli.rs | 234 ++++++++++++++++++++++++++++++------------------- src/main.rs | 52 ++++++++++- 3 files changed, 198 insertions(+), 132 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 128d597..eda5690 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -8,12 +8,10 @@ use routes::api_keys::models::ApiKey; use sqlx::PgPool; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; -use tracing::{info, level_filters::LevelFilter, Level}; -use tracing_rolling_file::{RollingConditionBase, RollingFileAppenderBase}; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; +use tracing::info; use crate::APP_NAME; -use crate::{config::Configuration, errors::AppError, ROOT_PATH}; +use crate::{config::Configuration, errors::AppError}; pub mod backend; mod description; @@ -23,44 +21,8 @@ pub async fn start( config: &Configuration, cancellation_token: CancellationToken, ) -> Result<(), Report> { - // Set Report Colour Mode to NONE - Report::set_color_mode(error_stack::fmt::ColorMode::None); - - let level_filter = LevelFilter::from_level(match config.debug { - true => Level::DEBUG, - false => Level::INFO, - }); - - // Prepare logging to file - let file_appender = RollingFileAppenderBase::new( - ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))), - RollingConditionBase::new().max_size(1024 * 1024 * 2), - 5, - ) - .change_context(AppError)?; - let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); - - // prepare initialization of logging - let log_layers = tracing_subscriber::registry().with(level_filter).with( - fmt::Layer::default() - .with_target(false) - .with_ansi(false) - .with_writer(non_blocking), - ); - - // also log to console in debug mode - #[cfg(debug_assertions)] - let stdout_log = tracing_subscriber::fmt::layer().pretty(); - #[cfg(debug_assertions)] - let log_layers = log_layers.with(stdout_log); - - // Initialize logging - log_layers.init(); - // Initialize database pool - let pool = PgPool::connect(&config.database_url) - .await - .change_context(AppError)?; + let pool = PgPool::connect_lazy(&config.database_url).change_context(AppError)?; // Get API Keys from database let api_keys = ApiKey::get_all_with_secret_attached(&pool, config) .await diff --git a/src/cli.rs b/src/cli.rs index 3a77cab..1e290cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -44,7 +44,7 @@ enum Commands { #[derive(Debug, Subcommand)] enum ServiceCommands { Install, - Uninstall, + Remove, Run, } @@ -93,22 +93,35 @@ impl Cli { } Commands::Service(service_commands) => match service_commands { ServiceCommands::Install => { - // TODO do things + #[cfg(windows)] + windows::install_service()?; + + // Print success message + println!("Succssfully installed service {APP_NAME}"); + Ok(DaemonStatus::NotRunning) } - ServiceCommands::Uninstall => { - // TODO do things + ServiceCommands::Remove => { + #[cfg(windows)] + windows::uninstall_service()?; + + // Print success message + println!("Succssfully removed service {APP_NAME}"); + Ok(DaemonStatus::NotRunning) } ServiceCommands::Run => { - // TODO do things - // create shutdown signals - // let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel(); - // Register generated `ffi_service_main` with the system and start the service, blocking - // this thread until the service is stopped. #[cfg(windows)] - let _ = service_dispatcher::start(env!("CARGO_PKG_NAME"), ffi_service_main); - Ok(DaemonStatus::NotRunning) + { + windows::run()?; + + Ok(DaemonStatus::NotRunning) + } + + #[cfg(not(windows))] + { + Ok(DaemonStatus::NotRunning) + } } }, Commands::Create(create_commands) => match create_commands { @@ -160,18 +173,30 @@ async fn start_service( mod windows { use error_stack::{Report, ResultExt}; use std::{ffi::OsString, thread, time::Duration}; + use tokio_util::task::TaskTracker; + use tracing::{error, info}; use windows_service::{ + define_windows_service, service::{ - ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, + ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess}, }; - use crate::errors::AppError; - use crate::APP_NAME; + use crate::{ + cli::{start_service, DaemonStatus}, + APP_NAME, ROOT_PATH, + }; + use crate::{config::Configuration, errors::AppError}; - pub fn install() -> Result<(), Report> { + const SERVICE_NAME: &str = "GenericApiService"; + const SERVICE_DISPLAY_NAME: &str = APP_NAME; + + pub fn install_service() -> Result<(), Report> { let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) .change_context(AppError)?; @@ -184,30 +209,28 @@ mod windows { .with_file_name(format!("{}.exe", env!("CARGO_PKG_NAME"))); let service_info = ServiceInfo { - name: OsString::from(env!("CARGO_PKG_NAME")), - display_name: OsString::from(&format!("{APP_NAME}")), + name: SERVICE_NAME.into(), + display_name: SERVICE_DISPLAY_NAME.into(), service_type: ServiceType::OWN_PROCESS, start_type: ServiceStartType::AutoStart, error_control: ServiceErrorControl::Normal, executable_path: service_binary_path, - launch_arguments: vec!["service run".into()], + launch_arguments: vec!["service".into(), "run".into()], dependencies: vec![], account_name: None, // run as System account_password: None, }; let service = service_manager .create_service(&service_info, ServiceAccess::CHANGE_CONFIG) - .into_report() .change_context(AppError)?; service .set_description(format!("{APP_NAME}")) - .into_report() .change_context(AppError)?; Ok(()) } - pub fn remove() -> Result<(), Report> { + pub fn uninstall_service() -> Result<(), Report> { let manager_access = ServiceManagerAccess::CONNECT; let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) .change_context(AppError)?; @@ -215,7 +238,7 @@ mod windows { let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; let service = service_manager - .open_service(env!("CARGO_PKG_NAME"), service_access) + .open_service(SERVICE_NAME, service_access) .change_context(AppError)?; let service_status = service.query_status().change_context(AppError)?; @@ -225,86 +248,121 @@ mod windows { thread::sleep(Duration::from_secs(1)); } - service.delete().into_report().change_context(AppError)?; + service.delete().change_context(AppError)?; Ok(()) } - // Service entry function which is called on background thread by the system with service - // parameters. There is no stdout or stderr at this point so make sure to configure the log - // output to file if needed. - pub fn service_main(_arguments: Vec) { - if let Err(_e) = run_service() { - // Handle the error, by logging or something. - } - } + pub fn run() -> Result<(), Report> { + // Generate the windows service boilerplate. + // The boilerplate contains the low-level service entry function (ffi_service_main) that parses + // incoming service arguments into Vec and passes them to user defined service + // entry (my_service_main). + define_windows_service!(ffi_service_main, service_main); - fn run_service() -> Result<(), Report> { - // Create a channel to be able to poll a stop event from the service worker loop. - let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel(1); + fn run_service() -> Result<(), Report> { + // Create a channel to be able to poll a stop event from the service worker loop. + let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::unbounded_channel(); - // Define system service event handler that will be receiving service events. - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - // Notifies a service to report its current status information to the service - // control manager. Always return NoError even if not implemented. - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + // Define system service event handler that will be receiving service events. + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NoError even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - // Handle stop - ServiceControl::Stop => { - shutdown_tx.try_send(()).unwrap(); - ServiceControlHandlerResult::NoError + // Handle stop + ServiceControl::Stop => { + let _ = shutdown_tx.send(()); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, } + }; - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - - // Register system service event handler. - // The returned status handle should be used to report service status changes to the system. - let status_handle = - service_control_handler::register(env!("CARGO_PKG_NAME"), event_handler) + // Register system service event handler. + // The returned status handle should be used to report service status changes to the system. + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) .change_context(AppError)?; - // Tell the system that service is running - status_handle - .set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }) - .change_context(AppError)?; + // Tell the system that service is running + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .change_context(AppError)?; - // run service - blocking thread - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); + // load config file + let config = Configuration::new(ROOT_PATH.with_file_name("config.toml"))?; + config.check()?; - rt.block_on(async move { - if let Err(e) = execute(shutdown_rx).await { - error!("{e:?}"); + // run service - blocking thread + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(async move { + // create task tracker + let tracker = TaskTracker::new(); + + match start_service(&config, Some(shutdown_rx)).await { + Ok(DaemonStatus::Running((cancellation_token, Some(mut shutdown_rx)))) => { + // wait for shutdown signal + shutdown_rx.recv().await; + + // send shutdown signal to application and wait + cancellation_token.cancel(); + + // close task tracker + tracker.close(); + + // Wait for everything to finish. + tracker.wait().await; + tokio::time::sleep(Duration::from_millis(200)).await; + + info!("{APP_NAME} gracefully shut down."); + } + Err(e) => error!("{e:?}"), + _ => unreachable!(), + } + }); + + // Tell the system that service has stopped. + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .change_context(AppError)?; + + Ok(()) + } + + // Service entry function which is called on background thread by the system with service + // parameters. There is no stdout or stderr at this point so make sure to configure the log + // output to file if needed. + pub fn service_main(_arguments: Vec) { + if let Err(e) = run_service() { + // Handle the error, by logging or something. + error!("Error executing windows service: {e}") } - }); + } - // Tell the system that service has stopped. - status_handle - .set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }) - .into_report() - .change_context(ServiceError::Starting)?; - - Ok(()) + // Register generated `ffi_service_main` with the system and start the service, blocking + // this thread until the service is stopped. + service_dispatcher::start(SERVICE_NAME, ffi_service_main).change_context(AppError) } } diff --git a/src/main.rs b/src/main.rs index bc6bb7d..2ef8aa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ use std::{path::PathBuf, time::Duration}; use clap::Parser; -use error_stack::Report; +use error_stack::{Report, ResultExt}; use errors::AppError; use once_cell::sync::Lazy; use tokio::signal; use tokio_util::task::TaskTracker; -use tracing::{error, info}; +use tracing::{error, info, level_filters::LevelFilter, Level}; +use tracing_rolling_file::{RollingConditionBase, RollingFileAppenderBase}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; pub mod api; mod cli; @@ -29,17 +31,61 @@ pub const APP_NAME: &str = "Generic API Service"; #[tokio::main] async fn main() -> Result<(), Report> { + let run_exit_code = run().await; + + if let Err(e) = run_exit_code.as_ref() { + error!("{e}"); + tokio::time::sleep(Duration::from_secs(1)).await; + } + + run_exit_code +} + +async fn run() -> Result<(), Report> { dotenv::dotenv().ok(); + // Set Report Colour Mode to NONE + Report::set_color_mode(error_stack::fmt::ColorMode::None); + // prepare CLI let cli = cli::Cli::parse(); // load config file let mut config = config::Configuration::new(cli.config())?; config.check()?; - println!("{config:?}"); + let level_filter = LevelFilter::from_level(match config.debug { + true => Level::DEBUG, + false => Level::INFO, + }); + + // Prepare logging to file + let file_appender = RollingFileAppenderBase::new( + ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))), + RollingConditionBase::new().max_size(1024 * 1024 * 2), + 5, + ) + .change_context(AppError)?; + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + // prepare initialization of logging + let log_layers = tracing_subscriber::registry().with(level_filter).with( + fmt::Layer::default() + .with_target(false) + .with_ansi(false) + .with_writer(non_blocking), + ); + + // also log to console in debug mode + #[cfg(debug_assertions)] + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + #[cfg(debug_assertions)] + let log_layers = log_layers.with(stdout_log); + + // Initialize logging + log_layers.init(); + // handle CLI input let daemon = cli.handle(&mut config).await?;