#[cfg(windows)] use crate::common::network_paths_and_settings::block_extension_and_paths; #[cfg(windows)] use crate::env; #[cfg(windows)] use crate::log::{error, info, warn}; #[cfg(windows)] use crate::startup::initialize_startup::prepare_pre_wallet_startup; #[cfg(windows)] use crate::startup::node_runtime::{ initialize_node_logging, install_panic_cleanup, run_unlocked_node, }; #[cfg(windows)] use crate::startup::unlock_pipe::{pipe_name, run_unlock_pipe_server}; #[cfg(windows)] use crate::startup::unlock_structs::ServiceWaitState; #[cfg(windows)] use crate::Arc; #[cfg(windows)] use crate::Duration; #[cfg(windows)] use crate::Error; #[cfg(windows)] use crate::Runtime; #[cfg(windows)] use crate::RwLock; #[cfg(windows)] use crate::{AtomicBool, AtomicOrdering}; #[cfg(windows)] use std::ffi::OsString; #[cfg(windows)] use std::path::{Path, PathBuf}; #[cfg(windows)] use std::sync::Mutex as StdMutex; #[cfg(windows)] use std::thread::sleep as thread_sleep; #[cfg(windows)] use std::time::{Duration as StdDuration, Instant}; #[cfg(windows)] use tokio::sync::mpsc; #[cfg(windows)] use tokio::task::JoinHandle; #[cfg(windows)] use windows_service::service::{ ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType, }; #[cfg(windows)] use windows_service::service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, }; #[cfg(windows)] use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; #[cfg(windows)] use windows_service::service_dispatcher; #[cfg(windows)] use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; #[cfg(windows)] use windows_sys::Win32::Foundation::{ERROR_SERVICE_ALREADY_RUNNING, ERROR_SERVICE_DOES_NOT_EXIST}; #[cfg(windows)] use winreg::enums::HKEY_LOCAL_MACHINE; #[cfg(windows)] use winreg::RegKey; #[cfg(windows)] const WINDOWS_SERVICE_CONTROLLER_CONNECT_ERROR: i32 = 1063; #[cfg(windows)] windows_service::define_windows_service!(ffi_service_main, contractless_service_main); #[cfg(windows)] fn service_name() -> &'static str { #[cfg(feature = "mainnet")] { "ContractlessMainnet" } #[cfg(feature = "testnet")] { "ContractlessTestnet" } } #[cfg(windows)] fn service_display_name() -> &'static str { #[cfg(feature = "mainnet")] { "Contractless Mainnet" } #[cfg(feature = "testnet")] { "Contractless Testnet" } } #[cfg(windows)] fn uninstall_registry_key_name() -> String { // The uninstall entry shares the service identity so Windows can present the // running node as a normal removable application in Add/Remove Programs. format!( r"Software\Microsoft\Windows\CurrentVersion\Uninstall\{}", service_name() ) } #[cfg(windows)] fn submit_key_binary_path(service_binary_path: &Path) -> PathBuf { // The helper binary is expected next to the service executable. service_binary_path .parent() .unwrap_or_else(|| Path::new(".")) .join("contractless-submit-key.exe") } #[cfg(windows)] fn quoted_command(path: &Path, args: &[&str]) -> String { // Windows registry uninstall commands need quoted paths because Program Files contains spaces. let mut command = format!("\"{}\"", path.display()); for arg in args { command.push(' '); command.push_str(arg); } command } #[cfg(windows)] fn register_add_remove_entry(service_binary_path: &Path) -> Result<(), Box> { // The uninstall registry entry points back to the same binary so the install, // service management, and uninstall logic all stay in one maintained path. let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let uninstall_path = uninstall_registry_key_name(); let (key, _) = hklm.create_subkey(&uninstall_path)?; let install_location = service_binary_path .parent() .unwrap_or_else(|| Path::new(".")) .to_string_lossy() .to_string(); let uninstall_command = quoted_command(service_binary_path, &["--uninstall-service"]); let submit_key_path = submit_key_binary_path(service_binary_path); key.set_value( "DisplayName", &format!("{} Service", service_display_name()), )?; key.set_value("DisplayVersion", &env!("CARGO_PKG_VERSION"))?; key.set_value("Publisher", &"Contractless")?; key.set_value("InstallLocation", &install_location)?; key.set_value( "DisplayIcon", &service_binary_path.to_string_lossy().to_string(), )?; key.set_value("UninstallString", &uninstall_command)?; key.set_value("QuietUninstallString", &uninstall_command)?; key.set_value( "Comments", &format!( "{} background node service. Use contractless-submit-key.exe to unlock the wallet after the service starts.", service_display_name() ), )?; key.set_value("NoModify", &1u32)?; key.set_value("NoRepair", &1u32)?; if submit_key_path.exists() { key.set_value( "HelpLink", &format!( "Submit wallet key with {} after starting the service.", submit_key_path.display() ), )?; } Ok(()) } #[cfg(windows)] fn unregister_add_remove_entry() -> Result<(), Box> { // Removing an already-missing uninstall entry should not make service uninstall fail. let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let uninstall_path = uninstall_registry_key_name(); match hklm.delete_subkey_all(&uninstall_path) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err.into()), } } #[cfg(windows)] pub fn try_run_as_windows_service() -> Result> { // Console launches fall through to the normal startup path. Only SCM launches // should enter the dedicated Windows service runtime. match service_dispatcher::start(service_name(), ffi_service_main) { Ok(()) => Ok(true), Err(windows_service::Error::Winapi(err)) if err.raw_os_error() == Some(WINDOWS_SERVICE_CONTROLLER_CONNECT_ERROR) => { Ok(false) } Err(err) => Err(err.into()), } } #[cfg(windows)] pub fn handle_windows_service_command() -> Result> { let args: Vec = env::args().skip(1).collect(); // Service install and lifecycle commands are handled before any wallet prompt // or node startup so administrative setup never mixes with normal runtime flow. if args.iter().any(|arg| arg == "--install-service") { println!("{}", install_windows_service()?); return Ok(true); } if args.iter().any(|arg| arg == "--uninstall-service") { println!("{}", uninstall_windows_service()?); return Ok(true); } if args.iter().any(|arg| arg == "--start-service") { println!("{}", start_windows_service()?); return Ok(true); } if args.iter().any(|arg| arg == "--stop-service") { println!("{}", stop_windows_service()?); return Ok(true); } Ok(false) } #[cfg(windows)] fn install_windows_service() -> Result> { // Installing the service also registers the uninstall entry so Windows has a // standard removal path in addition to the direct CLI commands. let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; let service_binary_path = env::current_exe()?; let service_info = ServiceInfo { name: OsString::from(service_name()), display_name: OsString::from(service_display_name()), service_type: ServiceType::OWN_PROCESS, start_type: ServiceStartType::OnDemand, error_control: ServiceErrorControl::Normal, executable_path: service_binary_path.clone(), launch_arguments: vec![], dependencies: vec![], account_name: None, account_password: None, }; let service_access = ServiceAccess::QUERY_CONFIG | ServiceAccess::CHANGE_CONFIG | ServiceAccess::START | ServiceAccess::DELETE; let service = match service_manager.create_service(&service_info, service_access) { Ok(service) => { let _ = service.set_description("Contractless blockchain node service"); register_add_remove_entry(&service_binary_path)?; return Ok(format!("Windows service {} installed.", service_name())); } Err(_) => service_manager.open_service(service_name(), service_access)?, }; let _ = service.set_description("Contractless blockchain node service"); register_add_remove_entry(&service_binary_path)?; Ok(format!( "Windows service {} is already installed.", service_name() )) } #[cfg(windows)] fn uninstall_windows_service() -> Result> { // Uninstall first stops the service if needed, then removes both the SCM entry // and the Add/Remove Programs registration. let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?; let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; let service = match service_manager.open_service(service_name(), service_access) { Ok(service) => service, Err(windows_service::Error::Winapi(err)) if err.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) => { unregister_add_remove_entry()?; return Ok(format!( "Windows service {} is not installed.", service_name() )); } Err(err) => return Err(err.into()), }; stop_service_if_running(&service)?; service.delete()?; drop(service); let start = Instant::now(); let timeout = StdDuration::from_secs(5); while start.elapsed() < timeout { match service_manager.open_service(service_name(), ServiceAccess::QUERY_STATUS) { Err(windows_service::Error::Winapi(err)) if err.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) => { unregister_add_remove_entry()?; return Ok(format!("Windows service {} uninstalled.", service_name())); } Ok(service) => { drop(service); } Err(err) => return Err(err.into()), } thread_sleep(StdDuration::from_millis(250)); } unregister_add_remove_entry()?; Ok(format!( "Windows service {} is marked for deletion and will finish uninstalling once Windows releases the open handles.", service_name() )) } #[cfg(windows)] fn start_windows_service() -> Result> { // Start is a management command; the service itself still waits for a wallet key over the pipe. let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?; let service = service_manager.open_service( service_name(), ServiceAccess::START | ServiceAccess::QUERY_STATUS, )?; match service.start(&[] as &[OsString]) { Ok(()) => Ok(format!("Windows service {} started.", service_name())), Err(windows_service::Error::Winapi(err)) if err.raw_os_error() == Some(ERROR_SERVICE_ALREADY_RUNNING as i32) => { Ok(format!( "Windows service {} is already running.", service_name() )) } Err(err) => Err(err.into()), } } #[cfg(windows)] fn stop_windows_service() -> Result> { // Stop requests go through SCM so service shutdown follows the normal control handler path. let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?; let service = match service_manager.open_service( service_name(), ServiceAccess::QUERY_STATUS | ServiceAccess::STOP, ) { Ok(service) => service, Err(windows_service::Error::Winapi(err)) if err.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) => { return Ok(format!( "Windows service {} is not installed.", service_name() )); } Err(err) => return Err(err.into()), }; if stop_service_if_running(&service)? { Ok(format!( "Windows service {} stop requested.", service_name() )) } else { Ok(format!( "Windows service {} is already stopped.", service_name() )) } } #[cfg(windows)] fn stop_service_if_running( service: &windows_service::service::Service, ) -> Result> { // Service deletion and rebuilds both depend on the process actually exiting, so // the stop path waits until Windows reports the service as fully stopped. let status = service.query_status()?; if status.current_state == ServiceState::Stopped { return Ok(false); } if status.current_state != ServiceState::StopPending { let _ = service.stop()?; } let start = Instant::now(); let timeout = StdDuration::from_secs(15); while start.elapsed() < timeout { let status = service.query_status()?; if status.current_state == ServiceState::Stopped { return Ok(true); } thread_sleep(StdDuration::from_millis(250)); } Err(format!( "Timed out waiting for Windows service {} to stop.", service_name() ) .into()) } #[cfg(windows)] fn contractless_service_main(_arguments: Vec) { // SCM enters here instead of the normal console main path. if let Err(err) = run_service() { eprintln!("Windows service error: {err}"); } } #[cfg(windows)] fn run_service() -> windows_service::Result<()> { // The Windows service starts in a locked state, completes only the pre-wallet // startup work, and then waits for the helper tool to submit the wallet key. let shutdown = Arc::new(AtomicBool::new(false)); let shutdown_handler = shutdown.clone(); let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { ServiceControl::Stop => { shutdown_handler.store(true, AtomicOrdering::SeqCst); ServiceControlHandlerResult::NoError } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, } }; // Register the SCM control handler before reporting StartPending. let status_handle = service_control_handler::register(service_name(), event_handler)?; // Tell SCM that initialization has begun but the service is not ready yet. status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StartPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 1, wait_hint: Duration::from_secs(10), process_id: None, })?; // The service owns its own Tokio runtime because SCM entry is synchronous. let runtime = Runtime::new().expect("Failed to create Windows service runtime"); let _log_handle = runtime.block_on(async { match initialize_node_logging().await { Ok(handle) => Some(handle), Err(err) => { eprintln!("Failed to initialize logging: {err}"); None } } }); install_panic_cleanup(); let service_state = Arc::new(RwLock::new(ServiceWaitState::WaitingForUnlock)); let service_state_for_pipe = service_state.clone(); let shutdown_for_pipe = shutdown.clone(); let (unlock_tx, mut unlock_rx) = mpsc::unbounded_channel::(); let unlocked_node_task: Arc>>> = Arc::new(StdMutex::new(None)); let unlocked_node_task_for_loop = unlocked_node_task.clone(); runtime.block_on(async { // Run only the wallet-independent startup work before accepting unlock requests. prepare_pre_wallet_startup().await; // The named pipe stays alive while the service waits for a valid wallet key. tokio::spawn(async move { run_unlock_pipe_server(service_state_for_pipe, shutdown_for_pipe, unlock_tx).await; }); }); info!( "Windows service {} for {} is running and waiting for wallet unlock on {}.", service_name(), block_extension_and_paths().0, pipe_name() ); // At this point the service is healthy, but still locked until the helper submits a key. status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, })?; let shutdown_for_service = shutdown.clone(); runtime.block_on(async move { loop { tokio::select! { maybe_wallet_key = unlock_rx.recv() => { if let Some(wallet_key) = maybe_wallet_key { // Once the wallet key is accepted, the shared unlocked-node // runtime is launched inside the service process itself. let handle = tokio::spawn(async move { if let Err(err) = run_unlocked_node(wallet_key, false).await { error!("Unlocked Windows service node failed during startup: {err}"); } }); if let Ok(mut task_slot) = unlocked_node_task_for_loop.lock() { // Keep the handle so service shutdown can abort the node task cleanly. *task_slot = Some(handle); } } } _ = crate::sleep(Duration::from_millis(500)) => { if shutdown_for_service.load(AtomicOrdering::SeqCst) { break; } } } } }); runtime.block_on(async { // Publish the stopping state so pipe status checks see shutdown in progress. let mut state = service_state.write().await; *state = ServiceWaitState::Stopping; }); warn!("Windows service received stop request."); status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StopPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 1, wait_hint: Duration::from_secs(10), process_id: None, })?; // Stop the unlocked node task before the runtime is torn down. if let Ok(mut task_slot) = unlocked_node_task.lock() { if let Some(handle) = task_slot.take() { handle.abort(); } } // Shutting down the runtime releases the remaining async work so the service // process can exit cleanly and the binary can be rebuilt or uninstalled. runtime.shutdown_timeout(StdDuration::from_secs(2)); info!("Windows service {} stopped.", service_name()); status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, })?; Ok(()) } #[cfg(not(windows))] pub fn try_run_as_windows_service() -> Result> { Ok(false) } #[cfg(not(windows))] pub fn handle_windows_service_command() -> Result> { Ok(false) }