qsu 0.10.1

Service subsystem utilities and runtime wrapper.
Documentation
use std::{cell::RefCell, ffi::OsString, thread, time::Duration};

use windows_service::{
  service::{
    ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo,
    ServiceStartType, ServiceState, ServiceType
  },
  service_manager::{ServiceManager, ServiceManagerAccess}
};

use crate::rt::winsvc::{
  create_service_params, get_service_params_subkey, write_service_subkey
};

use super::Error;


/// Register a service in the system service's subsystem.
///
/// # Errors
/// Problems encountered in the Windows service subsystem will be reported as
/// [`Error::SubSystem`].
// ToDo: Make notes about Windows-specific semantics:
// - Uses registry
// - Installer
// - Windows Event Log
pub fn install(ctx: super::RegSvc) -> Result<(), Error> {
  let svcname = &ctx.svcname;

  // Create a reference cell used to keep track of whether to keep system
  // modification (or not) when leaving function.
  let status = RefCell::new(false);

  // Register an event source named by the service name.
  fltevtlog::register(svcname)?;

  // The event source registration was successful and is a persistent change.
  // If this function returns early due to an error we want to roll back the
  // changes it made up to that point.  This scope guard is used to deregister
  // the event source of the function returns early.
  let _status = scopeguard::guard(&status, |st| {
    if !*st.borrow() && fltevtlog::deregister(svcname).is_err() {
      eprintln!("!!> Unable to deregister event source");
    }
  });

  let manager_access =
    ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
  let service_manager =
    ServiceManager::local_computer(None::<&str>, manager_access)?;

  let service_binary_path = ::std::env::current_exe()?;

  // Default display name to service name
  let display_name = ctx
    .display_name
    .as_ref()
    .map_or(svcname, |display_name| display_name);

  //
  // Generate service launch arguments
  //
  let launch_args: Vec<OsString> =
    ctx.args.iter().map(OsString::from).collect();

  let autostart = if ctx.autostart {
    ServiceStartType::AutoStart
  } else {
    ServiceStartType::OnDemand
  };

  let mut deps = vec![];
  for dep in &ctx.deps {
    match dep {
      super::Depend::Network => {
        deps.push(OsString::from("Tcpip"));
      }
      super::Depend::Custom(lst) => {
        for d in lst {
          deps.push(OsString::from(d));
        }
      }
    }
  }

  let dependencies: Vec<ServiceDependency> =
    deps.into_iter().map(ServiceDependency::Service).collect();

  // If None, then run as System
  let account_name = ctx.runas.user.clone().map(OsString::from);

  let service_info = ServiceInfo {
    name: OsString::from(svcname),
    display_name: OsString::from(display_name),
    service_type: ServiceType::OWN_PROCESS,
    start_type: autostart,
    error_control: ServiceErrorControl::Normal,
    executable_path: service_binary_path,
    launch_arguments: launch_args,
    dependencies,
    account_name,
    account_password: None
  };
  //println!("==> Registering service '{}' ..", svcname);
  let service = service_manager
    .create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?;
  let _status = scopeguard::guard(&status, |st| {
    if !(*st.borrow()) {
      let service_access = ServiceAccess::DELETE;
      let res = service_manager.open_service(svcname, service_access);
      if let Ok(service) = res {
        if service.delete().is_err() {
          eprintln!("!!> Unable to delete service");
        }
      } else {
        eprintln!("!!> Unable to open registered service");
      }
    }
  });

  if let Some(ref desc) = ctx.description {
    service.set_description(desc)?;
  }

  if ctx.have_envs() {
    let key = write_service_subkey(svcname)?;
    let envs: Vec<String> =
      ctx.envs.iter().map(|(k, v)| format!("{k}={v}")).collect();
    let elst: Vec<&str> = envs.iter().map(String::as_str).collect();
    key.set_multi_string("Environment", &elst)?;
  }

  //println!("==> Service installation successful");

  let mut params = create_service_params(svcname)?;

  // Just so the uninstaller will accept this service
  params.set_string("Installer", "qsu")?;

  if let Some(wd) = ctx.workdir {
    params.set_string("WorkDir", &wd)?;
  }

  if let Some(ll) = ctx.log_level {
    params.set_string("LogLevel", ll.to_string())?;
  }
  if let Some(lf) = ctx.log_filter {
    params.set_string("LogFilter", &lf)?;
  }

  if let Some(ll) = ctx.trace_filter {
    params.set_string("TraceFilter", &ll)?;
  }
  if let Some(fname) = ctx.trace_file {
    params.set_string("TraceFile", &fname)?;
  }


  // Give application the opportunity to create registry keys.
  if let Some(cb) = ctx.regconf {
    cb(svcname, &mut params)?;
  }

  // Mark status as success so the scopeguards won't attempt to reverse the
  // changes.
  *status.borrow_mut() = true;

  Ok(())
}


/// Deregister a system service.
///
/// # Constraints
/// Uninstalling the wrong service can have spectacularly bad side-effects, so
/// qsu goes to some lengths to ensure that only services it installed itself
/// can be uninstalled.
///
/// Before attempting an actual uninstallation, this function will verify that
/// under the service's `Parameters` subkey there is an `Installer` key with
/// the value `qsu`.
///
/// # Errors
/// Problems encountered in the Windows service subsystem will be reported as
/// [`Error::SubSystem`].
pub fn uninstall(svcname: &str) -> Result<(), Error> {
  // Only allow uninstallation of services that have an Installer=qsu key in
  // its Parameters subkey.
  if let Ok(params) = get_service_params_subkey(svcname) {
    if let Ok(val) = params.get_string("Installer") {
      if val != "qsu" {
        Err(Error::missing(
          "Refusing to uninstall service that doesn't appear to be installed \
           by qsu"
        ))?;
      }
    } else {
      Err(Error::missing(
        "Service Parameters does not have a Installer key."
      ))?;
    }
  } else {
    Err(Error::missing("Service does not have a Parameters subkey."))?;
  }


  let manager_access = ServiceManagerAccess::CONNECT;
  let service_manager =
    ServiceManager::local_computer(None::<&str>, manager_access)?;
  let service_access =
    ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
  let service = service_manager.open_service(svcname, service_access)?;

  // Make sure service is stopped before trying to delete it
  loop {
    let service_status = service.query_status()?;
    if service_status.current_state == ServiceState::Stopped {
      break;
    }
    println!("==> Requesting service '{svcname}' to stop ..");
    service.stop()?;
    thread::sleep(Duration::from_secs(2));
  }


  //println!("==> Removing service '{}' ..", service_name);
  service.delete()?;

  //println!("==> Deregistering event log source '{}' ..", service_name);
  // ToDo: error-handling
  fltevtlog::deregister(svcname)
    .map_err(|e| Error::SubSystem(e.to_string()))?;

  //println!("==> Service uninstallation successful");

  Ok(())
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :