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 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<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
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

View File

@ -44,7 +44,7 @@ enum Commands {
#[derive(Debug, Subcommand)]
enum ServiceCommands {
Install,
Uninstall,
Remove,
Run,
}
@ -93,23 +93,36 @@ 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);
{
windows::run()?;
Ok(DaemonStatus::NotRunning)
}
#[cfg(not(windows))]
{
Ok(DaemonStatus::NotRunning)
}
}
},
Commands::Create(create_commands) => match create_commands {
CreateCommands::ApiKey {
@ -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<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 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<AppError>> {
pub fn uninstall_service() -> Result<(), Report<AppError>> {
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,23 +248,21 @@ 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<OsString>) {
if let Err(_e) = run_service() {
// Handle the error, by logging or something.
}
}
pub fn run() -> Result<(), Report<AppError>> {
// Generate the windows service boilerplate.
// The boilerplate contains the low-level service entry function (ffi_service_main) that parses
// incoming service arguments into Vec<OsString> and passes them to user defined service
// entry (my_service_main).
define_windows_service!(ffi_service_main, service_main);
fn run_service() -> Result<(), Report<AppError>> {
// 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.
let event_handler = move |control_event| -> ServiceControlHandlerResult {
@ -252,7 +273,7 @@ mod windows {
// Handle stop
ServiceControl::Stop => {
shutdown_tx.try_send(()).unwrap();
let _ = shutdown_tx.send(());
ServiceControlHandlerResult::NoError
}
@ -262,8 +283,7 @@ mod windows {
// 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)
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
.change_context(AppError)?;
// Tell the system that service is running
@ -279,6 +299,10 @@ mod windows {
})
.change_context(AppError)?;
// load config file
let config = Configuration::new(ROOT_PATH.with_file_name("config.toml"))?;
config.check()?;
// run service - blocking thread
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
@ -286,8 +310,28 @@ mod windows {
.unwrap();
rt.block_on(async move {
if let Err(e) = execute(shutdown_rx).await {
error!("{e:?}");
// 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!(),
}
});
@ -302,9 +346,23 @@ mod windows {
wait_hint: Duration::default(),
process_id: None,
})
.into_report()
.change_context(ServiceError::Starting)?;
.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<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 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<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();
// 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?;