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 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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										134
									
								
								src/cli.rs
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								src/cli.rs
									
									
									
									
									
								
							@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								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<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?;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user