From aa500b3ad822e4000cf943ffa4a34492ae15816a Mon Sep 17 00:00:00 2001 From: Revertron Date: Thu, 8 Jun 2023 00:07:15 +0200 Subject: [PATCH] Added Windows service mode! --- Cargo.lock | 82 +++++++++++++++----- Cargo.toml | 3 +- src/main.rs | 188 +++++++++++++++++++++++++++++++++------------ src/win_service.rs | 164 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 72 deletions(-) create mode 100644 src/win_service.rs diff --git a/Cargo.lock b/Cargo.lock index 997f441..803c143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "alfis" -version = "0.8.4" +version = "0.8.5" dependencies = [ "base64 0.21.0", "bincode", @@ -125,6 +125,7 @@ dependencies = [ "uuid", "web-view", "winapi", + "windows-service", "winres", "x25519-dalek", ] @@ -1738,6 +1739,12 @@ dependencies = [ "webkit2gtk-sys", ] +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" @@ -1769,6 +1776,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.45.0", +] + [[package]] name = "windows-sys" version = "0.36.1" @@ -1789,19 +1807,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" @@ -1811,9 +1853,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" @@ -1823,9 +1865,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" @@ -1835,9 +1877,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" @@ -1847,15 +1889,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" @@ -1865,9 +1907,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index e1a04d6..da4d00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alfis" -version = "0.8.4" +version = "0.8.5" authors = ["Revertron "] edition = "2021" build = "build.rs" @@ -52,6 +52,7 @@ open = { version = "3.0.3", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["impl-default", "wincon", "shellscalingapi"] } +windows-service = "0.6.0" thread-priority = "0.10.0" [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] diff --git a/src/main.rs b/src/main.rs index aacf7ee..da0c3e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::Duration; -use std::{env, thread}; +use std::{env, fs, thread}; use getopts::{Matches, Options}; #[allow(unused_imports)] @@ -20,17 +20,23 @@ use std::fs::{File, OpenOptions}; use std::io::{Seek, SeekFrom, Write}; use std::process::exit; use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::JoinHandle; use alfis::event::Event; use alfis::eventbus::{post, register}; use alfis::keystore::create_key; use alfis::{dns_utils, Block, Bytes, Chain, Context, Keystore, Miner, Network, Settings, Transaction, ALFIS_DEBUG, ALFIS_TRACE, DB_NAME, ORIGIN_DIFFICULTY}; +#[cfg(windows)] +use crate::win_service::start_service; #[cfg(feature = "webgui")] mod web_ui; +#[cfg(windows)] +mod win_service; const SETTINGS_FILENAME: &str = "alfis.toml"; const LOG_TARGET_MAIN: &str = "alfis::Main"; +const CONFIG: &str = include_str!("../alfis.toml"); fn main() { #[allow(unused_assignments, unused_mut)] @@ -55,6 +61,12 @@ fn main() { opts.optflag("t", "trace", "Show trace messages, more than debug"); opts.optflag("b", "blocks", "List blocks from DB and exit"); opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console."); + #[cfg(windows)] + { + opts.optflag("", "install", "Install self as Windows service."); + opts.optflag("", "uninstall", "Uninstall self as Windows service."); + opts.optflag("", "service", "Run as Windows service."); + } opts.optopt("k", "gen-key", "Generate new keys and save them to file.", "FILE"); opts.optopt("l", "log", "Write log to file", "FILE"); opts.optopt("s", "status", "Write status to file", "FILE"); @@ -79,7 +91,40 @@ fn main() { } if opt_matches.opt_present("g") { - println!("{}", include_str!("../alfis.toml")); + println!("{}", CONFIG); + exit(0); + } + + #[cfg(windows)] + if opt_matches.opt_present("install") { + let progdata = env::var("PROGRAMDATA").expect("Failed to get APPDATA directory"); + + // Create a new directory inside the AppData directory + let new_directory = format!("{}\\ALFIS", progdata); + fs::create_dir_all(&new_directory).expect("Failed to create directory"); + + // Change the current directory to the new directory + env::set_current_dir(&new_directory).expect("Failed to change directory"); + let mut file = File::create("alfis.toml").expect("Failed to create alfis.toml in AppData\\ALFIS"); + file.write_all(CONFIG.as_bytes()).expect("Failed to write alfis.toml"); + + use crate::win_service::*; + install_service(SERVICE_NAME, &program); + // Without explicitly detaching the console cmd won't redraw it's prompt. + unsafe { + FreeConsole(); + } + exit(0); + } + + #[cfg(windows)] + if opt_matches.opt_present("uninstall") { + use crate::win_service::*; + uninstall_service(SERVICE_NAME); + // Without explicitly detaching the console cmd won't redraw it's prompt. + unsafe { + FreeConsole(); + } exit(0); } @@ -97,10 +142,22 @@ fn main() { }; #[cfg(feature = "webgui")] - let no_gui = opt_matches.opt_present("n"); + let mut no_gui = opt_matches.opt_present("n"); #[cfg(not(feature = "webgui"))] let no_gui = true; + if opt_matches.opt_present("service") { + let appdata = env::var("PROGRAMDATA").expect("Failed to get APPDATA directory"); + + // Create a new directory inside the AppData directory + let new_directory = format!("{}\\ALFIS", appdata); + fs::create_dir_all(&new_directory).expect("Failed to create directory"); + + // Change the current directory to the new directory + env::set_current_dir(&new_directory).expect("Failed to change directory"); + no_gui = true; + } + if let Some(path) = opt_matches.opt_str("w") { env::set_current_dir(Path::new(&path)).unwrap_or_else(|_| panic!("Unable to change working directory to '{}'", &path)); } @@ -131,33 +188,22 @@ fn main() { let settings = Settings::load(&config_name).unwrap_or_else(|| panic!("Cannot load settings from {}!", &config_name)); debug!(target: LOG_TARGET_MAIN, "Loaded settings: {:?}", &settings); - let chain: Chain = Chain::new(&settings, DB_NAME); - if opt_matches.opt_present("b") { - for i in 1..(chain.get_height() + 1) { - if let Some(block) = chain.get_block(i) { - info!(target: LOG_TARGET_MAIN, "{:?}", &block); - } - } - return; - } - info!("Blocks count: {}, domains count: {}, users count: {}", chain.get_height(), chain.get_domains_count(), chain.get_users_count()); let settings_copy = settings.clone(); - let mut keys = Vec::new(); - if !settings.key_files.is_empty() { - for name in &settings.key_files { - match Keystore::from_file(name, "") { - None => { - warn!("Error loading keyfile from {}", name); - } - Some(keystore) => { - info!("Successfully loaded keyfile {}", name); - keys.push(keystore); - } - } + + let context = match create_context(opt_matches.opt_present("b"), settings) { + Some(value) => value, + None => return, + }; + + #[cfg(windows)] + if opt_matches.opt_present("service") { + if let Err(e) = start_service(settings_copy, context) { + error!("Unable to start service: {}", e); + exit(1); } + info!("Service is stopped."); + return; } - let context = Context::new(env!("CARGO_PKG_VERSION").to_owned(), settings, keys, chain); - let context: Arc> = Arc::new(Mutex::new(context)); // If we just need to generate keys if let Some(filename) = opt_matches.opt_str("k") { @@ -192,8 +238,68 @@ fn main() { exit(0); } + let (dns_server_ok, miner, network) = start_services(&settings_copy, &context); + + create_genesis_if_needed(&context, &miner); + if no_gui { + print_my_domains(&context); + let _ = network.join(); + } else { + if !dns_server_ok { + thread::spawn(|| { + thread::sleep(Duration::from_millis(500)); + post(Event::Error { text: String::from("Error starting DNS-server. Please, check that it’s port is not busy.") }); + }); + } + #[cfg(feature = "webgui")] + web_ui::run_interface(Arc::clone(&context), miner); + } + + // Without explicitly detaching the console cmd won't redraw it's prompt. + #[cfg(windows)] + unsafe { + FreeConsole(); + } +} + +fn create_context(only_print_blocks: bool, settings: Settings) -> Option>> { + let chain: Chain = Chain::new(&settings, DB_NAME); + if only_print_blocks { + for i in 1..(chain.get_height() + 1) { + if let Some(block) = chain.get_block(i) { + info!(target: LOG_TARGET_MAIN, "{:?}", &block); + } + } + return None; + } + info!("Blocks count: {}, domains count: {}, users count: {}", chain.get_height(), chain.get_domains_count(), chain.get_users_count()); + let keys = load_keys(&settings); + let context = Context::new(env!("CARGO_PKG_VERSION").to_owned(), settings, keys, chain); + let context: Arc> = Arc::new(Mutex::new(context)); + Some(context) +} + +fn load_keys(settings: &Settings) -> Vec { + let mut keys = Vec::new(); + if !settings.key_files.is_empty() { + for name in &settings.key_files { + match Keystore::from_file(name, "") { + None => { + warn!("Error loading keyfile from {}", name); + } + Some(keystore) => { + info!("Successfully loaded keyfile {}", name); + keys.push(keystore); + } + } + } + } + keys +} + +pub fn start_services(settings: &Settings, context: &Arc>) -> (bool, Arc>, JoinHandle<()>) { if let Ok(mut context) = context.lock() { - context.chain.check_chain(settings_copy.check_blocks); + context.chain.check_chain(settings.check_blocks); match context.chain.get_block(1) { None => { info!(target: LOG_TARGET_MAIN, "No blocks found in DB"); @@ -204,8 +310,8 @@ fn main() { } } - let dns_server_ok = if settings_copy.dns.threads > 0 { - dns_utils::start_dns_server(&context, &settings_copy) + let dns_server_ok = if settings.dns.threads > 0 { + dns_utils::start_dns_server(&context, &settings) } else { true }; @@ -220,27 +326,7 @@ fn main() { thread::sleep(Duration::from_millis(1000)); network.start(); }).expect("Could not start network thread!"); - - create_genesis_if_needed(&context, &miner); - if no_gui { - print_my_domains(&context); - let _ = network.join(); - } else { - if !dns_server_ok { - thread::spawn(|| { - thread::sleep(Duration::from_millis(500)); - post(Event::Error { text: String::from("Error starting DNS-server. Please, check that it’s port is not busy.") }); - }); - } - #[cfg(feature = "webgui")] - web_ui::run_interface(Arc::clone(&context), miner); - } - - // Without explicitly detaching the console cmd won't redraw it's prompt. - #[cfg(windows)] - unsafe { - FreeConsole(); - } + (dns_server_ok, miner, network) } /// Sets up logger in accordance with command line options diff --git a/src/win_service.rs b/src/win_service.rs new file mode 100644 index 0000000..78c85e7 --- /dev/null +++ b/src/win_service.rs @@ -0,0 +1,164 @@ +use std::ffi::{OsStr, OsString}; +use std::path::PathBuf; +use std::sync::{Arc, mpsc, Mutex}; +use std::thread; +use std::time::Duration; +use lazy_static::lazy_static; +use log::{error, info}; + +use windows_service::{define_windows_service, service::{ + ServiceControl, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, +}, service_control_handler::ServiceControlHandlerResult, service_dispatcher, Result, service_control_handler}; +use windows_service::service::ServiceControlAccept; +use alfis::{Context, Settings}; +use crate::start_services; + +// Define the service entry point and its behavior +define_windows_service!(ffi_service_main, alfis_service_main); + +pub const SERVICE_NAME: &str = "ALFIS"; +pub const SERVICE_DESCRIPTION: &str = "Alternative Free Identity System, DNS on a smallest blockchain."; + +lazy_static! { + // Sending parameters through static variables. Don't do this! + static ref SETTINGS: Mutex<(Option, Option>>)> = Mutex::new((None, None)); +} + +pub fn start_service(settings: Settings, context: Arc>) -> Result<()> { + if let Ok(mut option) = SETTINGS.lock() { + let _ = option.0.insert(settings); + let _ = option.1.insert(context); + } + // Register the service entry point and control handler + service_dispatcher::start(SERVICE_NAME, ffi_service_main) +} + +fn alfis_service_main(_arguments: Vec) { + if let Err(e) = run_service_logic() { + error!("Error while starting service: {}", e); + } +} + +fn run_service_logic() -> Result<()> { + let (shutdown_tx, shutdown_rx) = mpsc::channel(); + let event_handler = move |control_event| -> ServiceControlHandlerResult { + info!("Event: {:?}", &control_event); + match control_event { + ServiceControl::Stop => { + // Handle stop event and return control back to the system. + shutdown_tx.send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + // All services must accept Interrogate even if it's a no-op. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: ServiceType::OWN_PROCESS, + // The new state + current_state: ServiceState::Running, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + // Unused for setting status + process_id: None, + }; + + // Tell the system that the service is running now + status_handle.set_service_status(next_status)?; + + let (settings, context) = { + let mut lock = SETTINGS.lock().unwrap(); + (lock.0.take().unwrap(), lock.1.take().unwrap()) + }; + let (_dns_server_ok, _miner, _network) = start_services(&settings, &context); + + loop { + thread::sleep(Duration::from_secs(1)); + // 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) => break, + + // Continue work if no events were received within the timeout + Err(mpsc::RecvTimeoutError::Timeout) => (), + }; + } + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: ServiceType::OWN_PROCESS, + // The new state + current_state: ServiceState::Stopped, + // Accept stop events when running + controls_accepted: ServiceControlAccept::empty(), + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + // Unused for setting status + process_id: None, + }; + status_handle.set_service_status(next_status)?; + Ok(()) +} + +// Function to install a Windows service +pub fn install_service(service_name: &str, bin_path: &str) { + use windows_service::service_manager::*; + use windows_service::service::*; + let error = "Error creating service. Try to start with admin rights"; + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let manager = ServiceManager::local_computer(None::<&str>, manager_access).expect(error); + + let my_service_info = ServiceInfo { + name: OsString::from(service_name), + display_name: OsString::from(service_name), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: PathBuf::from(bin_path), + launch_arguments: vec![OsString::from("--service"), OsString::from("-l"), OsString::from("alfis_log.txt")], + dependencies: vec![], + account_name: None, // run as System + account_password: None, + }; + + let my_service = manager.create_service(&my_service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START).expect(error); + let _ = my_service.set_description(&OsStr::new(SERVICE_DESCRIPTION)); + thread::sleep(Duration::from_secs(1)); + match my_service.start(&[OsStr::new("--service")]) { + Ok(_) => println!("Service successfully installed and started"), + Err(e) => println!("Error starting service: {}", e) + } +} + +// Function to uninstall a Windows service +pub fn uninstall_service(service_name: &str) { + use windows_service::service_manager::*; + use windows_service::service::*; + let error = "Error creating service. Try to start with admin rights"; + let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).expect(error); + let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; + match manager.open_service(&OsStr::new(service_name), service_access) { + Ok(service) => { + let _ = service.stop(); + thread::sleep(Duration::from_secs(2)); + let _ = service.delete(); + } + Err(e) => println!("Error opening service. Try running with admin rights: {}", e) + } +} \ No newline at end of file