urma-node 0.6.0

Optional URMA archive node: Litecoin follower, record archive, libp2p overlay and loopback service
Documentation
use crate::{
    config::SERVICE_NAME,
    install::{
        Error, Init, Registration, Runner, Status, System,
        windows_acl::{administered, reclaim, trusted_binary},
    },
    platform::ensure_defaults,
};
use std::{
    ffi::{OsStr, OsString},
    fs,
    io::Write,
    thread,
    time::Duration,
};
use windows_service::{
    Error as ServiceError,
    service::{
        Service, ServiceAccess, ServiceAction, ServiceActionType, ServiceErrorControl,
        ServiceFailureActions, ServiceFailureResetPeriod, ServiceInfo, ServiceStartType,
        ServiceState, ServiceType,
    },
    service_manager::{ServiceManager, ServiceManagerAccess},
};

pub struct Scm;

impl Scm {
    const NAME: &'static str = SERVICE_NAME;
    const DISPLAY: &'static str = "URMA Node";
    const DOES_NOT_EXIST: i32 = 1060;
    const STOP_POLLS: u32 = 60;
    const POLL: Duration = Duration::from_millis(500);
    const RESTART_DELAY: Duration = Duration::from_secs(10);
    const RESET_PERIOD: Duration = Duration::from_secs(86_400);

    fn access() -> ServiceAccess {
        ServiceAccess::QUERY_STATUS
            | ServiceAccess::QUERY_CONFIG
            | ServiceAccess::CHANGE_CONFIG
            | ServiceAccess::START
            | ServiceAccess::STOP
            | ServiceAccess::DELETE
    }

    fn manager() -> Result<ServiceManager, Error> {
        Ok(ServiceManager::local_computer(
            None::<&str>,
            ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
        )
        .map_err(scm("OpenSCManager"))?)
    }

    fn open(manager: &ServiceManager) -> Result<Vec<Service>, Error> {
        match manager.open_service(Self::NAME, Self::access()) {
            Ok(service) => Ok(vec![service]),
            Err(ServiceError::Winapi(error)) => match error.raw_os_error() {
                Some(code) if code == Self::DOES_NOT_EXIST => Ok(Vec::new()),
                Some(..) | None => Err(scm("OpenService")(ServiceError::Winapi(error))),
            },
            Err(error) => Err(scm("OpenService")(error)),
        }
    }

    fn info(binary: &std::path::Path) -> ServiceInfo {
        ServiceInfo {
            name: OsString::from(Self::NAME),
            display_name: OsString::from(Self::DISPLAY),
            service_type: ServiceType::OWN_PROCESS,
            start_type: ServiceStartType::AutoStart,
            error_control: ServiceErrorControl::Normal,
            executable_path: binary.to_path_buf(),
            launch_arguments: vec![OsString::from("service"), OsString::from("run")],
            dependencies: vec![],
            account_name: Some(OsString::from(format!(r"NT SERVICE\{}", Self::NAME))),
            account_password: None,
        }
    }

    fn recover(service: &Service) -> Result<(), Error> {
        let restart = ServiceAction {
            action_type: ServiceActionType::Restart,
            delay: Self::RESTART_DELAY,
        };
        service
            .update_failure_actions(ServiceFailureActions {
                reset_period: ServiceFailureResetPeriod::After(Self::RESET_PERIOD),
                reboot_msg: None,
                command: None,
                actions: Some(vec![restart.clone(), restart.clone(), restart]),
            })
            .map_err(scm("ChangeServiceConfig2 failure actions"))?;
        service
            .set_failure_actions_on_non_crash_failures(true)
            .map_err(scm("ChangeServiceConfig2 failure flag"))?;
        Ok(())
    }

    fn start(service: &Service) -> Result<(), Error> {
        service.start::<&OsStr>(&[]).map_err(scm("StartService"))?;
        Ok(())
    }

    fn stop(service: &Service) -> Result<(), Error> {
        if service
            .query_status()
            .map_err(scm("QueryServiceStatus"))?
            .current_state
            == ServiceState::Stopped
        {
            return Ok(());
        }
        service.stop().map_err(scm("ControlService stop"))?;
        for _ in 0..Self::STOP_POLLS {
            if service
                .query_status()
                .map_err(scm("QueryServiceStatus"))?
                .current_state
                == ServiceState::Stopped
            {
                return Ok(());
            }
            thread::sleep(Self::POLL);
        }
        Err(Error::Failed(format!("{} did not stop", Self::NAME)))
    }
}

fn scm(call: &'static str) -> impl FnOnce(ServiceError) -> Error {
    move |error| Error::Windows { call, error }
}

fn native(init: Init) -> Result<(), Error> {
    match init {
        Init::Windows => Ok(()),
        Init::Systemd | Init::Openrc | Init::Runit => Err(Error::Failed(format!(
            "{init:?} is not available on Windows"
        ))),
    }
}

impl<R: Runner> System<R> {
    pub fn registration(&self) -> Result<Registration, Error> {
        match Scm::open(&Scm::manager()?)?.is_empty() {
            true => Ok(Registration::Absent),
            false => Ok(Registration::Registered(Init::Windows)),
        }
    }

    pub fn install(&mut self, init: Init, out: &mut impl Write) -> Result<(), Error> {
        native(init)?;
        trusted_binary(&self.host.binary)?;
        let manager = Scm::manager()?;
        let info = Scm::info(&self.host.binary);
        let existing = Scm::open(&manager)?;
        for service in &existing {
            Scm::stop(service)?;
        }
        let paths = self.paths();
        reclaim(&paths)?;
        let service = match existing.into_iter().next() {
            Some(service) => {
                service
                    .change_config(&info)
                    .map_err(scm("ChangeServiceConfig"))?;
                service
            }
            None => manager
                .create_service(&info, Scm::access())
                .map_err(scm("CreateService"))?,
        };
        Scm::recover(&service)?;
        ensure_defaults(&paths)?;
        administered(&paths.data)?;
        Scm::start(&service)?;
        writeln!(out, "{} is installed and running.", Scm::DISPLAY)?;
        Ok(())
    }

    pub fn update(&mut self, init: Init) -> Result<(), Error> {
        native(init)?;
        trusted_binary(&self.host.binary)?;
        let manager = Scm::manager()?;
        let Some(service) = Scm::open(&manager)?.into_iter().next() else {
            return Err(Error::NotInstalled);
        };
        service
            .change_config(&Scm::info(&self.host.binary))
            .map_err(scm("ChangeServiceConfig"))?;
        Scm::recover(&service)?;
        Scm::stop(&service)?;
        Scm::start(&service)
    }

    pub fn uninstall(&mut self, purge: bool) -> Result<(), Error> {
        let manager = Scm::manager()?;
        for service in Scm::open(&manager)? {
            Scm::stop(&service)?;
            service.delete().map_err(scm("DeleteService"))?;
        }
        let paths = self.paths();
        if purge && paths.state_dir.is_dir() {
            fs::remove_dir_all(&paths.state_dir)?;
        }
        Ok(())
    }

    pub fn state(&mut self, init: Init) -> Result<Status, Error> {
        let manager = Scm::manager()?;
        let mut status = Status {
            init,
            registered: false,
            enabled: false,
            running: false,
            listening: false,
        };
        for service in Scm::open(&manager)? {
            status.registered = true;
            status.enabled = service
                .query_config()
                .map_err(scm("QueryServiceConfig"))?
                .start_type
                == ServiceStartType::AutoStart;
            status.running = service
                .query_status()
                .map_err(scm("QueryServiceStatus"))?
                .current_state
                == ServiceState::Running;
        }
        Ok(status)
    }
}