diff --git a/.gitignore b/.gitignore index ea8c4bf..4b1d890 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 019a10c..4b54e46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "deranged" version = "0.5.4" @@ -90,18 +96,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rust" -version = "0.1.0" -dependencies = [ - "log", - "serde", - "simplelog", - "toml", - "widestring", - "winapi", -] - [[package]] name = "serde" version = "1.0.227" @@ -152,6 +146,19 @@ dependencies = [ "time", ] +[[package]] +name = "sxp-service-watcher" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "simplelog", + "toml", + "widestring", + "winapi", + "windows-service", +] + [[package]] name = "syn" version = "2.0.106" @@ -278,7 +285,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.1", ] [[package]] @@ -293,6 +300,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-service" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.1" @@ -302,6 +329,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.13" diff --git a/Cargo.toml b/Cargo.toml index 69b3ac5..eaf0b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rust" +name = "sxp-service-watcher" version = "0.1.0" edition = "2024" @@ -9,4 +9,5 @@ serde = { version = "1.0.227", features = ["derive"] } simplelog = "0.12.2" toml = "0.9.7" widestring = "1.2.0" -winapi = "0.3.9" +winapi = {version = "0.3.9", features = ["winsvc", "winerror", "winnt", "handleapi", "errhandlingapi", "winbase"]} +windows-service = "0.8.0" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..cfd2d47 --- /dev/null +++ b/config.toml @@ -0,0 +1,6 @@ +period = 30 +watch_path = "C:\\Users\\dn\\Desktop\\test" +watch_file_prefix = "sv_" +dir = "C:\\Users\\dn\\Desktop\\errorlocation" +watch_file_size = 0 +service_name = "gitea" \ No newline at end of file diff --git a/register.ps1 b/register.ps1 new file mode 100644 index 0000000..398f392 --- /dev/null +++ b/register.ps1 @@ -0,0 +1,84 @@ +# Register SV SXP Service Watcher +# Run this script as Administrator + +# Check if running as Administrator +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + +if (-not $isAdmin) { + Write-Error "This script must be run as Administrator!" + Write-Host "Please right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow + exit 1 +} + +# Service configuration +$serviceName = "SV SXP Service watcher" +$binaryPath = "C:\Users\dn\Desktop\SXP-Service-watcher\sxp-service-watcher.exe" +$displayName = "SV SXP Service watcher" +$description = "SV SXP Service watcher" +$startupType = "Automatic" + +# Check if service already exists +$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + +if ($existingService) { + Write-Host "Service '$serviceName' already exists." -ForegroundColor Yellow + $response = Read-Host "Do you want to remove and recreate it? (y/n)" + + if ($response -eq 'y') { + Write-Host "Stopping service..." -ForegroundColor Cyan + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + + Write-Host "Removing service..." -ForegroundColor Cyan + # Use sc.exe for compatibility with Windows PowerShell 5.1 + $result = sc.exe delete $serviceName + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to remove service: $result" + exit 1 + } + + Start-Sleep -Seconds 2 + Write-Host "Service removed successfully." -ForegroundColor Green + } else { + Write-Host "Registration cancelled." -ForegroundColor Yellow + exit 0 + } +} + +# Check if binary exists +if (-not (Test-Path $binaryPath)) { + Write-Error "Binary not found at: $binaryPath" + exit 1 +} + +# Register the service +try { + Write-Host "Registering service '$serviceName'..." -ForegroundColor Cyan + + New-Service -Name $serviceName ` + -BinaryPathName $binaryPath ` + -DisplayName $displayName ` + -Description $description ` + -StartupType $startupType ` + -ErrorAction Stop + + Write-Host "Service registered successfully!" -ForegroundColor Green + + # Ask if user wants to start the service + $startService = Read-Host "Do you want to start the service now? (y/n)" + + if ($startService -eq 'y') { + Write-Host "Starting service..." -ForegroundColor Cyan + Start-Service -Name $serviceName + + $status = Get-Service -Name $serviceName + Write-Host "Service status: $($status.Status)" -ForegroundColor Green + } + +} catch { + Write-Error "Failed to register service: $_" + exit 1 +} + +Write-Host "`nService registration complete!" -ForegroundColor Green \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index ca9763d..4e0adad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,19 @@ -use std::fs; +use crate::config::Config; +use crate::util::restart_service; use std::path::Path; +use std::process::exit; +use std::sync::mpsc; use std::thread::sleep; use std::time::Duration; -use crate::config::Config; +use std::{fs, sync::mpsc::Receiver}; -pub fn move_file_to_dir(watch_path_string: &String, filename: &String, move_path_string: &String) -> Result<(), Box> { +use crate::config::{CONFIG_PATH, ConfigErr, create_default_config, load_config}; + +pub fn move_file_to_dir( + watch_path_string: &String, + filename: &String, + move_path_string: &String, +) -> Result<(), Box> { let move_path = Path::new(move_path_string); let watch_path = Path::new(watch_path_string); let file_path = watch_path.join(Path::new(filename)); @@ -27,17 +36,35 @@ pub fn move_file_to_dir(watch_path_string: &String, filename: &String, move_path Ok(()) } -pub fn restart_server() { - +pub fn restart_server(service_name: String) { + restart_service(service_name); } -pub fn watch_files(config: Config) -> Result<(), Box> { +pub fn watch_files( + config: Config, + shutdown_rx: Receiver<()>, +) -> Result<(), Box> { loop { + // Poll shutdown event. + match shutdown_rx.recv_timeout(Duration::from_secs(1)) { + // Break the loop either upon stop or channel disconnect + Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => { + info!("Received shutdown event. Shutting down..."); + break + }, + + // Continue work if no events were received within the timeout + Err(mpsc::RecvTimeoutError::Timeout) => (), + }; + let mut has_found = false; for file in fs::read_dir(Path::new(&config.watch_path))? { if file.is_ok() { - let filename = file.unwrap().file_name().into_string().unwrap(); - if filename.starts_with(&config.watch_file_prefix) { + let file_obj = file.unwrap(); + let filename = file_obj.file_name().into_string().unwrap(); + if filename.starts_with(&config.watch_file_prefix) + && file_obj.metadata().unwrap().len() == config.watch_file_size + { info!("Found file: {}. Moving file.", filename); move_file_to_dir(&config.watch_path, &filename, &config.dir)?; has_found = true; @@ -46,10 +73,53 @@ pub fn watch_files(config: Config) -> Result<(), Box> { } if has_found { - info!("Restarting server"); - restart_server(); + info!("Restarting target service"); + restart_server(config.service_name.clone()); } sleep(Duration::from_secs(config.period as u64)); } -} \ No newline at end of file + + Ok(()) +} + +pub fn run(shutdown_rx: Receiver<()>) -> Result<(), Box> { + // Step 1: Get or create config file + let config_res = load_config(CONFIG_PATH); + + if let Err(err) = &config_res { + match err { + ConfigErr::NotExist => { + error!("Failed to find existing config file. Create one."); + match create_default_config(CONFIG_PATH) { + Ok(_) => (), + Err(_) => error!("Failed to create config file."), + } + + exit(0); + } + + ConfigErr::WriteError(e) => { + error!("Failed to write config file."); + error!("Message: {e}"); + return Ok(()); + } + + ConfigErr::ParsingError(e) => { + error!("Failed to parse config file"); + error!("Message: {e}"); + return Ok(()); + } + + ConfigErr::Unknown(e) => { + error!("Unknown error"); + error!("Message: {e}"); + return Ok(()); + } + } + } + + watch_files(config_res.unwrap(), shutdown_rx)?; + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 3f947c7..2ebac1c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::env; use std::fs::{read_to_string, File}; use std::io::Write; use serde::*; @@ -30,10 +31,23 @@ pub enum ConfigErr { NotExist, ParsingError(String), WriteError(String), + Unknown(String) } pub fn load_config(config_file: &str) -> Result { - let toml_contents = read_to_string(config_file); + let exe_path_res = env::current_exe(); + + if let Err(e) = &exe_path_res { + error!("Failed to get current working directory."); + return Err(ConfigErr::Unknown(String::from(format!("{}", e)))); + } + + + //let exe_path = exe_path_res.unwrap(); + let binding = exe_path_res.unwrap(); + let exe_dir = binding.parent().unwrap(); + let config_path = exe_dir.join(config_file); + let toml_contents = read_to_string(config_path); if let Ok(toml_contents) = toml_contents { let config = toml::from_str::(&toml_contents); diff --git a/src/main.rs b/src/main.rs index 490d63f..09a87ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,52 +1,150 @@ -#[macro_use] extern crate log; +#[macro_use] +extern crate log; -use std::fs::File; -use simplelog::*; - -use crate::app::watch_files; -use crate::config::{create_default_config, load_config, ConfigErr, CONFIG_PATH}; - -mod config; mod app; +mod config; mod util; +use simplelog::*; +use std::fs::File; + const LOG_FILE: &str = "sv_file_watcher.log"; -fn main() -> Result<(), Box> { - CombinedLogger::init( - vec![ - TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed, ColorChoice::Auto), - WriteLogger::new(LevelFilter::Info, Config::default(), File::create(LOG_FILE).unwrap()), - ] - ).unwrap(); +#[cfg(windows)] +fn main() -> windows_service::Result<()> { + let current_exe = std::env::current_exe().expect("Failed to get current working directory"); + let parent = current_exe.parent(); + let cwd = parent.expect("Failed to get parent directory"); - // Step 1: Get or create config file - let mut config_res = load_config(CONFIG_PATH); + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Info, + simplelog::Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + ), + WriteLogger::new( + LevelFilter::Info, + simplelog::Config::default(), + File::create(cwd.join(LOG_FILE)).unwrap(), + ), + ]) + .unwrap(); - if let Err(err) = &config_res { - match err { - ConfigErr::NotExist => { - error!("Failed to find existing config file. Create one."); - config_res = create_default_config(CONFIG_PATH); - }, + info!("Initialized logger"); - ConfigErr::WriteError(e) => { - error!("Failed to write config file."); - error!("Message: {e}"); - return Ok(()); - }, + ping_service::run() +} - ConfigErr::ParsingError(e) => { - error!("Failed to parse config file"); - error!("Message: {e}"); - return Ok(()); - } +#[cfg(not(windows))] +fn main() { + panic!("This program is only intended to run on Windows."); +} + +#[cfg(windows)] +mod ping_service { + use crate::app::run as run_watch; + + use std::{ + ffi::OsString, + sync::mpsc, + time::Duration, + }; + use windows_service::{ + Result, define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, + }; + + const SERVICE_NAME: &str = "SV: SXP Service Watcher"; + const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + + pub fn run() -> Result<()> { + // 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) + } + + // 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, my_service_main); + + // 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 my_service_main(_arguments: Vec) { + if let Err(e) = run_service() { + // Handle the error, by logging or something. + error!("Service failed: {}", e) } } - let config = config_res.unwrap(); + pub fn run_service() -> Result<()> { + // Create a channel to be able to poll a stop event from the service worker loop. + let (shutdown_tx, shutdown_rx) = mpsc::channel(); - watch_files(config)?; + // 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, - Ok(()) + // Handle stop + ServiceControl::Stop => { + shutdown_tx.send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + + // treat the UserEvent as a stop request + ServiceControl::UserEvent(code) => { + if code.to_raw() == 130 { + shutdown_tx.send(()).unwrap(); + } + ServiceControlHandlerResult::NoError + } + + _ => 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(SERVICE_NAME, event_handler)?; + + // Tell the system that service is running + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + if let Err(e) = run_watch(shutdown_rx) { + error!("Service crashed. Error: {}", e); + + } + + // Tell the system that service has stopped. + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) + } } diff --git a/src/util.rs b/src/util.rs index e69de29..2ed0061 100644 --- a/src/util.rs +++ b/src/util.rs @@ -0,0 +1,130 @@ +use std::{ptr::null_mut, time::Duration}; +use winapi::{ + shared::minwindef::DWORD, + um::{ + errhandlingapi::GetLastError, + winsvc::{ + CloseServiceHandle, ControlService, OpenSCManagerW, OpenServiceW, QueryServiceStatus, StartServiceW, SC_HANDLE__, SC_MANAGER_CONNECT, SERVICE_CONTROL_STOP, SERVICE_QUERY_STATUS, SERVICE_START, SERVICE_STOP + }, + }, +}; +use widestring::U16CString; + +const SERVICE_STATUS_RUNNING: u32 = 0x00000004; +const SERVICE_STATUS_STOPPED: u32 = 0x00000001; + +fn open_sc_manager(desired_access: DWORD) -> Result<*mut SC_HANDLE__, DWORD> { + let sc_manager_handle = unsafe { OpenSCManagerW(null_mut(), null_mut(), desired_access) }; + if sc_manager_handle.is_null() { + Err(unsafe { GetLastError() }) + } else { + Ok(sc_manager_handle) + } +} + + +fn open_service( + sc_manager_handle: *mut SC_HANDLE__, + service_name: &str, + desired_access: DWORD, +) -> Result<*mut SC_HANDLE__, DWORD> { + let service_name_wstr = U16CString::from_str(service_name).unwrap(); + let service_handle = unsafe { + OpenServiceW( + sc_manager_handle, + service_name_wstr.as_ptr(), + desired_access, + ) + }; + if service_handle.is_null() { + Err(unsafe { GetLastError() }) + } else { + Ok(service_handle) + } +} + +fn start_service(service_handle: *mut SC_HANDLE__) -> Result<(), DWORD> { + let result = unsafe { StartServiceW(service_handle, 0, null_mut()) }; + if result == 0 { + Err(unsafe { GetLastError() }) + } else { + Ok(()) + } +} + +fn stop_service(service_handle: *mut SC_HANDLE__) -> Result<(), DWORD> { + let mut service_status = unsafe { std::mem::zeroed() }; + let result = unsafe { + ControlService( + service_handle, + SERVICE_CONTROL_STOP, + &mut service_status, + ) + }; + if result == 0 { + Err(unsafe { GetLastError() }) + } else { + Ok(()) + } +} + +fn query_service_status(service_handle: *mut SC_HANDLE__) -> Result { + let mut service_status = unsafe { std::mem::zeroed() }; + let result = unsafe { QueryServiceStatus(service_handle, &mut service_status) }; + if result == 0 { + Err(unsafe { GetLastError() }) + } else { + Ok(service_status.dwCurrentState) + } +} + +pub fn restart_service(service_name: String) { + let sc_manager_handle = open_sc_manager(SC_MANAGER_CONNECT).unwrap(); + + let service_handle = open_service( + sc_manager_handle, + service_name.as_str(), + SERVICE_QUERY_STATUS | SERVICE_START | SERVICE_STOP, + ) + .unwrap(); + + let service_status_res = query_service_status(service_handle); + + if let Err(e) = service_status_res { + error!("Failed to query service status. Error code: {e}"); + return; + } + + let service_status = service_status_res.unwrap(); + + if service_status == SERVICE_STATUS_RUNNING { + // Stop the service + match stop_service(service_handle) { + Ok(_) => info!("Service stopped successfully."), + Err(error) => info!("Failed to stop service. Error: {}", error), + } + + info!("Waiting for service being stopped successfully"); + std::thread::sleep(Duration::from_secs(10)); + + // Start the service + match start_service(service_handle) { + Ok(_) => info!("Service started successfully."), + Err(error) => info!("Failed to start service. Error: {}", error), + } + } + + if service_status == SERVICE_STATUS_STOPPED { + // Start the service + match start_service(service_handle) { + Ok(_) => info!("Service started successfully."), + Err(error) => info!("Failed to start service. Error: {}", error), + } + } + + // Close the service and SCM handles + unsafe { + CloseServiceHandle(service_handle); + CloseServiceHandle(sc_manager_handle); + } +}