#![warn(dead_code)]
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use windows_service::service::{
Service, ServiceAccess, ServiceAction as ScmFailureAction, ServiceActionType,
ServiceErrorControl, ServiceFailureActions, ServiceFailureResetPeriod, ServiceInfo,
ServiceStartType, ServiceState, ServiceType,
};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
use super::scm::{self, RawScmStatus};
use super::{
InstallSpec, SERVICE_NAME, Scope, ServiceBackend, ServiceError, ServiceStatus,
current_exe_canonical,
};
const STOP_POLL_INTERVAL: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Copy, Default)]
pub struct ScmBackend;
impl ScmBackend {
pub fn new() -> Self {
Self
}
}
pub(super) fn raw_code(err: &windows_service::Error) -> Option<i32> {
match err {
windows_service::Error::Winapi(io) => io.raw_os_error(),
_ => None,
}
}
pub(super) fn describe(err: &windows_service::Error) -> String {
match err {
windows_service::Error::Winapi(io) => io.to_string(),
other => other.to_string(),
}
}
fn map_err(verb: &str, err: &windows_service::Error) -> ServiceError {
scm::map_os_error(verb, raw_code(err), &describe(err))
}
fn is_missing_service(err: &windows_service::Error) -> bool {
raw_code(err) == Some(scm::error_code::SERVICE_DOES_NOT_EXIST)
}
fn open_manager(access: ServiceManagerAccess, verb: &str) -> Result<ServiceManager, ServiceError> {
ServiceManager::local_computer(None::<&OsStr>, access).map_err(|e| map_err(verb, &e))
}
fn open_service(
manager: &ServiceManager,
access: ServiceAccess,
verb: &str,
) -> Result<Option<Service>, ServiceError> {
match manager.open_service(SERVICE_NAME, access) {
Ok(service) => Ok(Some(service)),
Err(e) if is_missing_service(&e) => Ok(None),
Err(e) => Err(map_err(verb, &e)),
}
}
fn registered_command_line(service: &Service) -> Option<String> {
service
.query_config()
.ok()
.map(|cfg| cfg.executable_path.to_string_lossy().into_owned())
}
fn read_status(service: &Service, verb: &str) -> Result<ServiceStatus, ServiceError> {
let status = service.query_status().map_err(|e| map_err(verb, &e))?;
let start_type = service.query_config().ok().map(|c| c.start_type.to_raw());
Ok(scm::map_status(RawScmStatus {
current_state: status.current_state as u32,
process_id: status.process_id,
start_type,
}))
}
fn executable_to_register() -> Result<PathBuf, ServiceError> {
let exe = current_exe_canonical()?;
match exe.to_str() {
Some(s) => Ok(PathBuf::from(scm::strip_verbatim_prefix(s))),
None => Ok(exe),
}
}
fn build_service_info(exe: &Path, spec: &InstallSpec) -> ServiceInfo {
ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from(scm::SERVICE_DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: exe.to_path_buf(),
launch_arguments: scm::LAUNCH_ARGUMENTS.iter().map(OsString::from).collect(),
dependencies: vec![],
account_name: spec.service_user.clone().map(OsString::from),
account_password: None,
}
}
fn apply_extended_config(service: &Service, verb: &str) -> Result<(), ServiceError> {
service
.set_description(scm::SERVICE_DESCRIPTION)
.map_err(|e| map_err(verb, &e))?;
let delay = Duration::from_secs(scm::RESTART_DELAY_SECS);
let actions = vec![
ScmFailureAction {
action_type: ServiceActionType::Restart,
delay,
},
ScmFailureAction {
action_type: ServiceActionType::Restart,
delay,
},
ScmFailureAction {
action_type: ServiceActionType::Restart,
delay,
},
];
service
.update_failure_actions(ServiceFailureActions {
reset_period: ServiceFailureResetPeriod::After(Duration::from_secs(
scm::FAILURE_RESET_PERIOD_SECS,
)),
reboot_msg: None,
command: None,
actions: Some(actions),
})
.map_err(|e| map_err(verb, &e))?;
service
.set_failure_actions_on_non_crash_failures(true)
.map_err(|e| map_err(verb, &e))?;
Ok(())
}
fn prepare_program_data() {
let dir = super::scm_log::log_dir();
if let Err(e) = std::fs::create_dir_all(&dir) {
eprintln!(
"warning: could not create the log directory {}: {e}",
dir.display()
);
}
}
impl ServiceBackend for ScmBackend {
fn install(&self, spec: &InstallSpec) -> Result<(), ServiceError> {
if spec.scope == Scope::User {
return Err(scm::user_scope_unsupported());
}
if spec.service_user.is_some() {
eprintln!(
"warning: --service-user on Windows only works for accounts that log on without \
a password (NT AUTHORITY\\LocalService, NT AUTHORITY\\NetworkService, or a group \
managed service account). A normal account needs a password, which this \
subcommand cannot supply, and registration will fail."
);
}
let exe = executable_to_register()?;
let info = build_service_info(&exe, spec);
let manager = open_manager(
ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
"install",
)?;
let access = ServiceAccess::QUERY_CONFIG
| ServiceAccess::CHANGE_CONFIG
| ServiceAccess::QUERY_STATUS
| ServiceAccess::START
| ServiceAccess::STOP;
let service = match open_service(&manager, access, "install")? {
Some(existing) => {
if !spec.force {
let command_line = registered_command_line(&existing).unwrap_or_default();
if !scm::command_line_targets(&command_line, &exe) {
return Err(scm::binary_path_conflict(&command_line, &exe));
}
}
existing
.change_config(&info)
.map_err(|e| map_err("install", &e))?;
existing
}
None => manager
.create_service(&info, access)
.map_err(|e| map_err("install", &e))?,
};
apply_extended_config(&service, "install")?;
prepare_program_data();
if spec.start_now {
start_service(&service, "install")?;
}
Ok(())
}
fn uninstall(&self, scope: Scope) -> Result<(), ServiceError> {
self.remove(scope, false)
}
fn uninstall_forced(&self, scope: Scope) -> Result<(), ServiceError> {
self.remove(scope, true)
}
fn start(&self, scope: Scope) -> Result<(), ServiceError> {
let service = self.open_for(scope, ServiceAccess::START, "start")?;
start_service(&service, "start")
}
fn stop(&self, scope: Scope) -> Result<(), ServiceError> {
let service = self.open_for(
scope,
ServiceAccess::STOP | ServiceAccess::QUERY_STATUS,
"stop",
)?;
stop_service(&service, "stop")?;
await_stopped(&service, "stop")
}
fn restart(&self, scope: Scope) -> Result<(), ServiceError> {
let service = self.open_for(
scope,
ServiceAccess::START | ServiceAccess::STOP | ServiceAccess::QUERY_STATUS,
"restart",
)?;
stop_service(&service, "restart")?;
await_stopped(&service, "restart")?;
start_service(&service, "restart")
}
fn status(&self, scope: Scope) -> Result<ServiceStatus, ServiceError> {
if scope == Scope::User {
return Err(scm::user_scope_unsupported());
}
let manager = open_manager(ServiceManagerAccess::CONNECT, "status")?;
let access = ServiceAccess::QUERY_STATUS | ServiceAccess::QUERY_CONFIG;
match open_service(&manager, access, "status")? {
Some(service) => read_status(&service, "status"),
None => Ok(scm::not_installed_status()),
}
}
}
impl ScmBackend {
fn open_for(
&self,
scope: Scope,
access: ServiceAccess,
verb: &str,
) -> Result<Service, ServiceError> {
if scope == Scope::User {
return Err(scm::user_scope_unsupported());
}
let manager = open_manager(ServiceManagerAccess::CONNECT, verb)?;
open_service(&manager, access, verb)?.ok_or(ServiceError::NotInstalled)
}
fn remove(&self, scope: Scope, force: bool) -> Result<(), ServiceError> {
if scope == Scope::User {
return Err(scm::user_scope_unsupported());
}
let manager = open_manager(ServiceManagerAccess::CONNECT, "uninstall")?;
let access = ServiceAccess::QUERY_STATUS
| ServiceAccess::QUERY_CONFIG
| ServiceAccess::STOP
| ServiceAccess::DELETE;
let service =
open_service(&manager, access, "uninstall")?.ok_or(ServiceError::NotInstalled)?;
if !force {
let exe = executable_to_register()?;
let command_line = registered_command_line(&service).unwrap_or_default();
if !scm::command_line_targets(&command_line, &exe) {
return Err(scm::binary_path_conflict(&command_line, &exe));
}
}
stop_service(&service, "uninstall")?;
await_stopped(&service, "uninstall")?;
service.delete().map_err(|e| map_err("uninstall", &e))?;
Ok(())
}
}
fn start_service(service: &Service, verb: &str) -> Result<(), ServiceError> {
match service.start(&[] as &[&OsStr]) {
Ok(()) => Ok(()),
Err(e) if scm::is_benign_lifecycle_error(raw_code(&e)) => Ok(()),
Err(e) => Err(map_err(verb, &e)),
}
}
fn stop_service(service: &Service, verb: &str) -> Result<(), ServiceError> {
match service.stop() {
Ok(_) => Ok(()),
Err(e) if scm::is_benign_lifecycle_error(raw_code(&e)) => Ok(()),
Err(e) => Err(map_err(verb, &e)),
}
}
fn await_stopped(service: &Service, verb: &str) -> Result<(), ServiceError> {
let deadline = Instant::now() + Duration::from_secs(scm::STOP_TIMEOUT_SECS);
loop {
let status = service.query_status().map_err(|e| map_err(verb, &e))?;
if status.current_state == ServiceState::Stopped {
return Ok(());
}
if Instant::now() >= deadline {
return Err(scm::stop_timeout_error(verb));
}
std::thread::sleep(STOP_POLL_INTERVAL);
}
}
#[cfg(test)]
#[path = "scm_backend_tests.rs"]
mod tests;