distant 0.20.0

Operate on a remote computer through file and process manipulation
Documentation
use std::ffi::{OsStr, OsString};
use std::path::Path;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use anyhow::Context;
use derive_more::From;
use log::*;
use windows_service::service::{
    ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType,
};
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::{define_windows_service, service_dispatcher};

use super::Cli;

const SERVICE_NAME: &str = "distant_manager";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;

#[derive(serde::Serialize, serde::Deserialize)]
struct Config {
    pub args: Vec<std::ffi::OsString>,
}

impl Config {
    pub fn save(&self) -> anyhow::Result<()> {
        let mut bytes = Vec::new();
        serde_json::to_writer(&mut bytes, self).context("Could not convert config into json")?;
        std::fs::write(Self::config_file(), bytes).context("Could not write config to file")
    }

    pub fn load() -> anyhow::Result<Self> {
        let bytes = std::fs::read(Self::config_file()).context("Could not read config file")?;
        serde_json::from_slice(&bytes).context("Could not convert json into config")
    }

    pub fn delete() -> anyhow::Result<()> {
        std::fs::remove_file(Self::config_file()).context("Could not delete config file")
    }

    /// Stored next to the service exe
    fn config_file() -> std::path::PathBuf {
        let mut path = std::env::current_exe().unwrap();
        path.set_extension("exe.config");
        path
    }
}

#[derive(From)]
pub enum ServiceError {
    /// Any other error type
    Anyhow(anyhow::Error),

    /// Represents a service-specific error that we use to known that we are not running as a
    /// service
    Service(windows_service::Error),
}

pub fn run() -> Result<(), ServiceError> {
    // Save our CLI arguments to pass on to the service
    let config = Config {
        args: std::env::args_os().collect(),
    };
    config.save()?;

    // Attempt to run as a service, deleting our config when completed
    // regardless of success
    let result = service_dispatcher::start(SERVICE_NAME, ffi_service_main);
    let config_result = Config::delete();

    // Swallow the config error if we have a service error, otherwise display
    // the config error
    match (result, config_result) {
        (Ok(_), Ok(_)) => Ok(()),
        (Err(x), _) => Err(ServiceError::Service(x)),
        (_, Err(x)) => Err(ServiceError::Anyhow(x)),
    }
}

/// Returns true if running as a windows service
pub fn is_windows_service() -> bool {
    use sysinfo::{Pid, PidExt, Process, ProcessExt, System, SystemExt};

    let mut system = System::new();

    // Get our own process pid
    let pid = Pid::from_u32(std::process::id());

    // Update our system's knowledge about our process
    system.refresh_process(pid);

    // Get our parent process' pid and update sustem's knowledge about parent process
    let maybe_parent_pid = system.process(pid).and_then(Process::parent);
    if let Some(pid) = maybe_parent_pid {
        system.refresh_process(pid);
    }

    // Check modeled after https://github.com/dotnet/extensions/blob/9069ee83c6ff1e4471cfbc07215c715c5ce157e1/src/Hosting/WindowsServices/src/WindowsServiceHelpers.cs#L31
    maybe_parent_pid
        .and_then(|pid| system.process(pid))
        .map(Process::exe)
        .and_then(Path::file_name)
        .map(OsStr::to_string_lossy)
        .map(|s| s.eq_ignore_ascii_case("services"))
        .unwrap_or_default()
}

define_windows_service!(ffi_service_main, service_main);

fn service_main(_arguments: Vec<OsString>) {
    if let Err(_e) = run_service() {
        // Handle the error, by logging or something.
    }
}

fn run_service() -> windows_service::Result<()> {
    debug!("Starting windows service for {SERVICE_NAME}");

    // Create a channel to be able to poll a stop event from the service worker loop.
    let (shutdown_tx, shutdown_rx) = std::sync::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(true).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.
    debug!("Registering service control handler for {SERVICE_NAME}");
    let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;

    // Tell the system that service is running
    debug!("Setting service status as running for {SERVICE_NAME}");
    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,
    })?;

    // Kick off thread to run our cli
    debug!("Spawning CLI thread for {SERVICE_NAME}");
    let handle = thread::spawn({
        move || {
            debug!("Loading CLI using args from disk for {SERVICE_NAME}");
            let config = Config::load().expect("Failed to load config");

            debug!("Parsing CLI args from disk for {SERVICE_NAME}");
            let cli = Cli::initialize_from(config.args).expect("Failed to initialize CLI");

            debug!("Running CLI for {SERVICE_NAME}");
            cli.run().expect("CLI failed during execution")
        }
    });

    // Continually check for a shutdown trigger, catching completion of the thread
    // running our CLI as well and reporting errors if they occurred
    let success = loop {
        if handle.is_finished() {
            match handle.join() {
                Ok(_) => break true,
                Err(x) => {
                    error!("{x:?}");
                    break false;
                }
            }
        }

        match shutdown_rx.try_recv() {
            // Break the loop either upon stop or channel disconnect as a success
            Ok(_) | Err(mpsc::TryRecvError::Disconnected) => break true,

            // Continue work if no events were received within the timeout
            Err(mpsc::TryRecvError::Empty) => thread::sleep(Duration::from_millis(100)),
        }
    };

    // Tell the system that service has stopped.
    debug!("Setting service status as stopped for {SERVICE_NAME}");
    status_handle.set_service_status(ServiceStatus {
        service_type: SERVICE_TYPE,
        current_state: ServiceState::Stopped,
        controls_accepted: ServiceControlAccept::empty(),
        exit_code: if success {
            ServiceExitCode::NO_ERROR
        } else {
            ServiceExitCode::ServiceSpecific(1u32)
        },
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    })?;

    Ok(())
}