2026-05-24 17:56:57 +00:00
|
|
|
#[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)]
|
2026-06-01 14:29:11 +00:00
|
|
|
use crate::wallets::structures::Wallet;
|
|
|
|
|
#[cfg(windows)]
|
2026-05-24 17:56:57 +00:00
|
|
|
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<dyn Error>> {
|
|
|
|
|
// 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<dyn Error>> {
|
|
|
|
|
// 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<bool, Box<dyn Error>> {
|
|
|
|
|
// 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<bool, Box<dyn Error>> {
|
|
|
|
|
let args: Vec<String> = 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<String, Box<dyn Error>> {
|
|
|
|
|
// 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<String, Box<dyn Error>> {
|
|
|
|
|
// 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<String, Box<dyn Error>> {
|
|
|
|
|
// 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<String, Box<dyn Error>> {
|
|
|
|
|
// 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<bool, Box<dyn Error>> {
|
|
|
|
|
// 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<OsString>) {
|
|
|
|
|
// 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();
|
2026-06-01 15:19:47 +00:00
|
|
|
let service_state_for_loop = service_state.clone();
|
2026-05-24 17:56:57 +00:00
|
|
|
let shutdown_for_pipe = shutdown.clone();
|
2026-06-01 14:29:11 +00:00
|
|
|
let (unlock_tx, mut unlock_rx) = mpsc::unbounded_channel::<Arc<Wallet>>();
|
2026-05-24 17:56:57 +00:00
|
|
|
let unlocked_node_task: Arc<StdMutex<Option<JoinHandle<()>>>> = 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! {
|
2026-06-01 14:29:11 +00:00
|
|
|
maybe_wallet = unlock_rx.recv() => {
|
|
|
|
|
if let Some(wallet) = maybe_wallet {
|
2026-05-24 17:56:57 +00:00
|
|
|
// Once the wallet key is accepted, the shared unlocked-node
|
|
|
|
|
// runtime is launched inside the service process itself.
|
2026-06-01 15:19:47 +00:00
|
|
|
let service_state_for_node = service_state_for_loop.clone();
|
|
|
|
|
let shutdown_for_node = shutdown_for_service.clone();
|
2026-05-24 17:56:57 +00:00
|
|
|
let handle = tokio::spawn(async move {
|
2026-06-01 14:29:11 +00:00
|
|
|
if let Err(err) = run_unlocked_node(wallet, false).await {
|
2026-05-24 17:56:57 +00:00
|
|
|
error!("Unlocked Windows service node failed during startup: {err}");
|
|
|
|
|
}
|
2026-06-01 15:19:47 +00:00
|
|
|
|
|
|
|
|
if !shutdown_for_node.load(AtomicOrdering::SeqCst) {
|
|
|
|
|
let mut state = service_state_for_node.write().await;
|
|
|
|
|
*state = ServiceWaitState::WaitingForUnlock;
|
|
|
|
|
}
|
2026-05-24 17:56:57 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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<bool, Box<dyn std::error::Error>> {
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
|
pub fn handle_windows_service_command() -> Result<bool, Box<dyn std::error::Error>> {
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|