Finalized watcher

This commit is contained in:
Dennis Nemec
2025-09-29 17:25:59 +02:00
parent 3f57ddefb7
commit 58446d3ae6
9 changed files with 558 additions and 63 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
*.log

117
Cargo.lock generated
View File

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

View File

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

6
config.toml Normal file
View File

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

84
register.ps1 Normal file
View File

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

View File

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
pub fn watch_files(
config: Config,
shutdown_rx: Receiver<()>,
) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
}
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));
}
Ok(())
}
pub fn run(shutdown_rx: Receiver<()>) -> Result<(), Box<dyn std::error::Error>> {
// 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(())
}

View File

@ -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<Config, ConfigErr> {
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::<Config>(&toml_contents);

View File

@ -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<dyn std::error::Error>> {
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");
ping_service::run()
}
#[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,
};
ConfigErr::WriteError(e) => {
error!("Failed to write config file.");
error!("Message: {e}");
return Ok(());
},
const SERVICE_NAME: &str = "SV: SXP Service Watcher";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
ConfigErr::ParsingError(e) => {
error!("Failed to parse config file");
error!("Message: {e}");
return Ok(());
}
}
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)
}
let config = config_res.unwrap();
// 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, my_service_main);
watch_files(config)?;
// 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<OsString>) {
if let Err(e) = run_service() {
// Handle the error, by logging or something.
error!("Service failed: {}", e)
}
}
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();
// 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,
// 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(())
}
}

View File

@ -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<DWORD, DWORD> {
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);
}
}