windows service

This commit is contained in:
Larsiiii 2025-01-08 19:15:51 +01:00
parent 47af1e62b7
commit 96f7fead42
3 changed files with 198 additions and 132 deletions

View File

@ -8,12 +8,10 @@ use routes::api_keys::models::ApiKey;
use sqlx::PgPool; use sqlx::PgPool;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{info, level_filters::LevelFilter, Level}; use tracing::info;
use tracing_rolling_file::{RollingConditionBase, RollingFileAppenderBase};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
use crate::APP_NAME; use crate::APP_NAME;
use crate::{config::Configuration, errors::AppError, ROOT_PATH}; use crate::{config::Configuration, errors::AppError};
pub mod backend; pub mod backend;
mod description; mod description;
@ -23,44 +21,8 @@ pub async fn start(
config: &Configuration, config: &Configuration,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<(), Report<AppError>> { ) -> Result<(), Report<AppError>> {
// 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 // Initialize database pool
let pool = PgPool::connect(&config.database_url) let pool = PgPool::connect_lazy(&config.database_url).change_context(AppError)?;
.await
.change_context(AppError)?;
// Get API Keys from database // Get API Keys from database
let api_keys = ApiKey::get_all_with_secret_attached(&pool, config) let api_keys = ApiKey::get_all_with_secret_attached(&pool, config)
.await .await

View File

@ -44,7 +44,7 @@ enum Commands {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
enum ServiceCommands { enum ServiceCommands {
Install, Install,
Uninstall, Remove,
Run, Run,
} }
@ -93,23 +93,36 @@ impl Cli {
} }
Commands::Service(service_commands) => match service_commands { Commands::Service(service_commands) => match service_commands {
ServiceCommands::Install => { ServiceCommands::Install => {
// TODO do things #[cfg(windows)]
windows::install_service()?;
// Print success message
println!("Succssfully installed service {APP_NAME}");
Ok(DaemonStatus::NotRunning) Ok(DaemonStatus::NotRunning)
} }
ServiceCommands::Uninstall => { ServiceCommands::Remove => {
// TODO do things #[cfg(windows)]
windows::uninstall_service()?;
// Print success message
println!("Succssfully removed service {APP_NAME}");
Ok(DaemonStatus::NotRunning) Ok(DaemonStatus::NotRunning)
} }
ServiceCommands::Run => { 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)] #[cfg(windows)]
let _ = service_dispatcher::start(env!("CARGO_PKG_NAME"), ffi_service_main); {
windows::run()?;
Ok(DaemonStatus::NotRunning) Ok(DaemonStatus::NotRunning)
} }
#[cfg(not(windows))]
{
Ok(DaemonStatus::NotRunning)
}
}
}, },
Commands::Create(create_commands) => match create_commands { Commands::Create(create_commands) => match create_commands {
CreateCommands::ApiKey { CreateCommands::ApiKey {
@ -160,18 +173,30 @@ async fn start_service(
mod windows { mod windows {
use error_stack::{Report, ResultExt}; use error_stack::{Report, ResultExt};
use std::{ffi::OsString, thread, time::Duration}; use std::{ffi::OsString, thread, time::Duration};
use tokio_util::task::TaskTracker;
use tracing::{error, info};
use windows_service::{ use windows_service::{
define_windows_service,
service::{ service::{
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl,
ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus,
ServiceType, ServiceType,
}, },
service_control_handler::{self, ServiceControlHandlerResult},
service_dispatcher,
service_manager::{ServiceManager, ServiceManagerAccess}, service_manager::{ServiceManager, ServiceManagerAccess},
}; };
use crate::errors::AppError; use crate::{
use crate::APP_NAME; cli::{start_service, DaemonStatus},
APP_NAME, ROOT_PATH,
};
use crate::{config::Configuration, errors::AppError};
pub fn install() -> Result<(), Report<AppError>> { const SERVICE_NAME: &str = "GenericApiService";
const SERVICE_DISPLAY_NAME: &str = APP_NAME;
pub fn install_service() -> Result<(), Report<AppError>> {
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.change_context(AppError)?; .change_context(AppError)?;
@ -184,30 +209,28 @@ mod windows {
.with_file_name(format!("{}.exe", env!("CARGO_PKG_NAME"))); .with_file_name(format!("{}.exe", env!("CARGO_PKG_NAME")));
let service_info = ServiceInfo { let service_info = ServiceInfo {
name: OsString::from(env!("CARGO_PKG_NAME")), name: SERVICE_NAME.into(),
display_name: OsString::from(&format!("{APP_NAME}")), display_name: SERVICE_DISPLAY_NAME.into(),
service_type: ServiceType::OWN_PROCESS, service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart, start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal, error_control: ServiceErrorControl::Normal,
executable_path: service_binary_path, executable_path: service_binary_path,
launch_arguments: vec!["service run".into()], launch_arguments: vec!["service".into(), "run".into()],
dependencies: vec![], dependencies: vec![],
account_name: None, // run as System account_name: None, // run as System
account_password: None, account_password: None,
}; };
let service = service_manager let service = service_manager
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG) .create_service(&service_info, ServiceAccess::CHANGE_CONFIG)
.into_report()
.change_context(AppError)?; .change_context(AppError)?;
service service
.set_description(format!("{APP_NAME}")) .set_description(format!("{APP_NAME}"))
.into_report()
.change_context(AppError)?; .change_context(AppError)?;
Ok(()) Ok(())
} }
pub fn remove() -> Result<(), Report<AppError>> { pub fn uninstall_service() -> Result<(), Report<AppError>> {
let manager_access = ServiceManagerAccess::CONNECT; let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.change_context(AppError)?; .change_context(AppError)?;
@ -215,7 +238,7 @@ mod windows {
let service_access = let service_access =
ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = service_manager let service = service_manager
.open_service(env!("CARGO_PKG_NAME"), service_access) .open_service(SERVICE_NAME, service_access)
.change_context(AppError)?; .change_context(AppError)?;
let service_status = service.query_status().change_context(AppError)?; let service_status = service.query_status().change_context(AppError)?;
@ -225,23 +248,21 @@ mod windows {
thread::sleep(Duration::from_secs(1)); thread::sleep(Duration::from_secs(1));
} }
service.delete().into_report().change_context(AppError)?; service.delete().change_context(AppError)?;
Ok(()) Ok(())
} }
// Service entry function which is called on background thread by the system with service pub fn run() -> Result<(), Report<AppError>> {
// parameters. There is no stdout or stderr at this point so make sure to configure the log // Generate the windows service boilerplate.
// output to file if needed. // The boilerplate contains the low-level service entry function (ffi_service_main) that parses
pub fn service_main(_arguments: Vec<OsString>) { // incoming service arguments into Vec<OsString> and passes them to user defined service
if let Err(_e) = run_service() { // entry (my_service_main).
// Handle the error, by logging or something. define_windows_service!(ffi_service_main, service_main);
}
}
fn run_service() -> Result<(), Report<AppError>> { fn run_service() -> Result<(), Report<AppError>> {
// Create a channel to be able to poll a stop event from the service worker loop. // 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); let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::unbounded_channel();
// Define system service event handler that will be receiving service events. // Define system service event handler that will be receiving service events.
let event_handler = move |control_event| -> ServiceControlHandlerResult { let event_handler = move |control_event| -> ServiceControlHandlerResult {
@ -252,7 +273,7 @@ mod windows {
// Handle stop // Handle stop
ServiceControl::Stop => { ServiceControl::Stop => {
shutdown_tx.try_send(()).unwrap(); let _ = shutdown_tx.send(());
ServiceControlHandlerResult::NoError ServiceControlHandlerResult::NoError
} }
@ -262,8 +283,7 @@ mod windows {
// Register system service event handler. // Register system service event handler.
// The returned status handle should be used to report service status changes to the system. // The returned status handle should be used to report service status changes to the system.
let status_handle = let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
service_control_handler::register(env!("CARGO_PKG_NAME"), event_handler)
.change_context(AppError)?; .change_context(AppError)?;
// Tell the system that service is running // Tell the system that service is running
@ -279,6 +299,10 @@ mod windows {
}) })
.change_context(AppError)?; .change_context(AppError)?;
// load config file
let config = Configuration::new(ROOT_PATH.with_file_name("config.toml"))?;
config.check()?;
// run service - blocking thread // run service - blocking thread
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
@ -286,8 +310,28 @@ mod windows {
.unwrap(); .unwrap();
rt.block_on(async move { rt.block_on(async move {
if let Err(e) = execute(shutdown_rx).await { // create task tracker
error!("{e:?}"); 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!(),
} }
}); });
@ -302,9 +346,23 @@ mod windows {
wait_hint: Duration::default(), wait_hint: Duration::default(),
process_id: None, process_id: None,
}) })
.into_report() .change_context(AppError)?;
.change_context(ServiceError::Starting)?;
Ok(()) 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<OsString>) {
if let Err(e) = run_service() {
// Handle the error, by logging or something.
error!("Error executing windows service: {e}")
}
}
// 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)
}
} }

View File

@ -1,12 +1,14 @@
use std::{path::PathBuf, time::Duration}; use std::{path::PathBuf, time::Duration};
use clap::Parser; use clap::Parser;
use error_stack::Report; use error_stack::{Report, ResultExt};
use errors::AppError; use errors::AppError;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tokio::signal; use tokio::signal;
use tokio_util::task::TaskTracker; 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; pub mod api;
mod cli; mod cli;
@ -29,17 +31,61 @@ pub const APP_NAME: &str = "Generic API Service";
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Report<AppError>> { async fn main() -> Result<(), Report<AppError>> {
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<AppError>> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
// Set Report Colour Mode to NONE
Report::set_color_mode(error_stack::fmt::ColorMode::None);
// prepare CLI // prepare CLI
let cli = cli::Cli::parse(); let cli = cli::Cli::parse();
// load config file // load config file
let mut config = config::Configuration::new(cli.config())?; let mut config = config::Configuration::new(cli.config())?;
config.check()?; config.check()?;
println!("{config:?}"); 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 // handle CLI input
let daemon = cli.handle(&mut config).await?; let daemon = cli.handle(&mut config).await?;