windows service
This commit is contained in:
parent
47af1e62b7
commit
96f7fead42
@ -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
|
||||||
|
134
src/cli.rs
134
src/cli.rs
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
52
src/main.rs
52
src/main.rs
@ -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?;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user