use cyberbrain_core::{Error, Result};
use std::path::{Path, PathBuf};
pub const SERVICE_NAME: &str = "CyberbrainHub";
pub const DISPLAY_NAME: &str = "Cyberbrain Hub";
#[cfg_attr(not(windows), allow(dead_code))]
pub const DESCRIPTION: &str = concat!(
"Collects the audit rows of Cyberbrain clients on this network. ",
"Holds no notes: only records of what happened, in a chain that cannot be edited."
);
pub fn default_data_dir() -> PathBuf {
std::env::var_os("PROGRAMDATA")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
.join("Cyberbrain")
}
pub fn licence_drop_path(data_dir: &Path) -> PathBuf {
data_dir.join(LICENCE_NAMES[0])
}
pub const LICENCE_NAMES: [&str; 4] = [
"licence.txt",
"licence.txt.txt",
"license.txt",
"license.txt.txt",
];
pub fn find_licence_file(data_dir: &Path) -> Option<PathBuf> {
LICENCE_NAMES
.iter()
.map(|n| data_dir.join(n))
.find(|p| p.is_file())
}
pub fn where_it_looked(data_dir: &Path) -> String {
format!(
"no licence file in {}. Put the one you were sent there as {} and restart this \
service.",
data_dir.display(),
LICENCE_NAMES[0]
)
}
#[derive(Debug, PartialEq)]
pub enum Dropped {
None,
Installed(String),
Unchanged,
Problem(String),
}
pub fn adopt_dropped_licence(hub: &super::HubStore, data_dir: &Path) -> Dropped {
let Some(path) = find_licence_file(data_dir) else {
return Dropped::None;
};
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
return Dropped::Problem(format!(
"{} cannot be read: {e}. If it was saved from an editor, save it again as \
UTF-8 or plain text, not Unicode/UTF-16.",
path.display()
));
}
};
if hub.licence_text().ok().flatten().as_deref() == Some(text.as_str()) {
return Dropped::Unchanged;
}
match super::licence::parse(&text) {
Ok(signed) => match hub.set_licence(&text) {
Ok(()) => Dropped::Installed(format!(
"licence picked up from {}: {}, {} seat(s), until {}",
path.display(),
signed.licence().customer,
signed.licence().seats,
signed.licence().valid_until
)),
Err(e) => Dropped::Problem(format!(
"could not store the licence from {}: {e}",
path.display()
)),
},
Err(e) => Dropped::Problem(format!(
"the licence at {} is not usable: {e}",
path.display()
)),
}
}
pub type Serve = Box<dyn Fn(std::sync::mpsc::Receiver<()>) -> Result<()> + Send + Sync>;
static LOG_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static SERVE: std::sync::OnceLock<Serve> = std::sync::OnceLock::new();
pub fn set_serve(f: Serve) {
let _ = SERVE.set(f);
}
pub fn run_serve(stop: std::sync::mpsc::Receiver<()>) -> Result<()> {
match SERVE.get() {
Some(f) => f(stop),
None => Err(Error::Config("nothing to serve was set up".into())),
}
}
pub fn set_log_path(data_db: &Path) {
let dir = data_db.parent().unwrap_or(Path::new("."));
let _ = LOG_PATH.set(dir.join("hub-service.log"));
}
pub fn log(msg: &str) {
let line = format!("{} {msg}\n", jiff::Timestamp::now());
match LOG_PATH.get() {
Some(p) => {
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(p)
{
let _ = f.write_all(line.as_bytes());
}
}
None => eprint!("{line}"),
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub fn describe_os_error(io: &std::io::Error) -> String {
match io.raw_os_error() {
Some(code) => format!("{io} (Windows error {code})"),
None => io.to_string(),
}
}
#[cfg(not(windows))]
mod platform {
use super::*;
pub fn install(
_exe: &Path,
_data: &Path,
_addr: &str,
_tls: Option<(&Path, &Path)>,
) -> Result<()> {
Err(unsupported())
}
pub fn uninstall() -> Result<()> {
Err(unsupported())
}
pub fn set_state(_start: bool) -> Result<()> {
Err(unsupported())
}
pub fn status() -> Result<String> {
Err(unsupported())
}
pub fn try_dispatch() -> Result<bool> {
Ok(false)
}
fn unsupported() -> Error {
Error::Config(
"Windows services exist only on Windows. On Linux use a systemd unit; \
docs/HUB.md has one."
.into(),
)
}
}
#[cfg(windows)]
mod platform {
use super::*;
use std::ffi::OsString;
use windows_service::service::{
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState,
ServiceType,
};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
fn why(e: windows_service::Error) -> String {
match &e {
windows_service::Error::Winapi(io) => describe_os_error(io),
other => other.to_string(),
}
}
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_SERVICE_MARKED_FOR_DELETE: i32 = 1072;
const ERROR_SERVICE_DOES_NOT_EXIST: i32 = 1060;
fn os_code(e: &windows_service::Error) -> Option<i32> {
match e {
windows_service::Error::Winapi(io) => io.raw_os_error(),
_ => None,
}
}
fn manager(access: ServiceManagerAccess) -> Result<ServiceManager> {
ServiceManager::local_computer(None::<&str>, access).map_err(|e| {
let hint = if os_code(&e) == Some(ERROR_ACCESS_DENIED) {
" This needs administrator rights: right-click, Run as administrator."
} else {
""
};
Error::Config(format!(
"cannot reach the service control manager: {}.{hint}",
why(e)
))
})
}
pub fn install(exe: &Path, data: &Path, addr: &str, tls: Option<(&Path, &Path)>) -> Result<()> {
let m = manager(ServiceManagerAccess::CREATE_SERVICE | ServiceManagerAccess::CONNECT)?;
let mut launch_arguments = vec![
OsString::from("hub"),
OsString::from("serve"),
OsString::from("--data"),
OsString::from(data),
OsString::from("--addr"),
OsString::from(addr),
];
if let Some((cert, key)) = tls {
launch_arguments.push(OsString::from("--tls-cert"));
launch_arguments.push(OsString::from(cert));
launch_arguments.push(OsString::from("--tls-key"));
launch_arguments.push(OsString::from(key));
}
let info = ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from(DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: exe.to_path_buf(),
launch_arguments,
dependencies: vec![],
account_name: None, account_password: None,
};
let existing = m.open_service(
SERVICE_NAME,
ServiceAccess::CHANGE_CONFIG | ServiceAccess::START | ServiceAccess::QUERY_STATUS,
);
let service = match existing {
Ok(service) => {
service
.change_config(&info)
.map_err(|e| Error::Config(format!("cannot update the service: {}", why(e))))?;
service
}
Err(e) if os_code(&e) == Some(ERROR_SERVICE_MARKED_FOR_DELETE) => {
return Err(Error::Config(
concat!(
"the service is being removed and Windows will not finish until ",
"everything holding it lets go. Close services.msc and the Services ",
"tab in Task Manager, then try again; a restart always clears it."
)
.into(),
));
}
Err(e) if os_code(&e) != Some(ERROR_SERVICE_DOES_NOT_EXIST) => {
let hint = if os_code(&e) == Some(ERROR_ACCESS_DENIED) {
" This needs administrator rights."
} else {
""
};
return Err(Error::Config(format!(
"cannot open the existing service: {}.{hint}",
why(e)
)));
}
Err(_) => m
.create_service(&info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
.map_err(|e| {
let hint = if os_code(&e) == Some(ERROR_ACCESS_DENIED) {
" This needs administrator rights."
} else {
""
};
Error::Config(format!("cannot register the service: {}.{hint}", why(e)))
})?,
};
let _ = service.set_description(DESCRIPTION);
match service.start::<OsString>(&[]) {
Ok(()) => Ok(()),
Err(_) if service_is_running(&service) => Ok(()),
Err(e) => Err(Error::Config(format!(
"it is registered, but did not start: {}. \
The log beside the record says why.",
why(e)
))),
}
}
fn service_is_running(service: &windows_service::service::Service) -> bool {
service
.query_status()
.is_ok_and(|s| s.current_state == ServiceState::Running)
}
pub fn uninstall() -> Result<()> {
let m = manager(ServiceManagerAccess::CONNECT)?;
let service = m
.open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::DELETE)
.map_err(|e| Error::Config(format!("cannot open the service: {}", why(e))))?;
let _ = service.stop();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while std::time::Instant::now() < deadline {
match service.query_status() {
Ok(status) if status.current_state == ServiceState::Stopped => break,
Err(_) => break,
_ => std::thread::sleep(std::time::Duration::from_millis(200)),
}
}
service
.delete()
.map_err(|e| Error::Config(format!("cannot remove the service: {}", why(e))))?;
Ok(())
}
pub fn set_state(start: bool) -> Result<()> {
let m = manager(ServiceManagerAccess::CONNECT)?;
let access = if start {
ServiceAccess::START
} else {
ServiceAccess::STOP
};
let service = m
.open_service(SERVICE_NAME, access)
.map_err(|e| Error::Config(format!("cannot open the service: {}", why(e))))?;
if start {
service
.start::<OsString>(&[])
.map_err(|e| Error::Config(format!("cannot start it: {}", why(e))))?;
} else {
service
.stop()
.map_err(|e| Error::Config(format!("cannot stop it: {}", why(e))))?;
}
Ok(())
}
pub fn status() -> Result<String> {
let m = manager(ServiceManagerAccess::CONNECT)?;
let service = m
.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS)
.map_err(|e| {
Error::Config(format!(
"the service is not registered ({}). Install it with \
`cyberbrain hub service install`, or tick the box in the installer.",
why(e)
))
})?;
let s = service
.query_status()
.map_err(|e| Error::Config(format!("cannot read the status: {}", why(e))))?;
Ok(match s.current_state {
ServiceState::Running => "running".into(),
ServiceState::Stopped => "stopped".into(),
ServiceState::StartPending => "starting".into(),
ServiceState::StopPending => "stopping".into(),
other => format!("{other:?}"),
})
}
use windows_service::service::{
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceStatus,
};
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::service_dispatcher;
const NOT_A_SERVICE: i32 = 1063;
windows_service::define_windows_service!(ffi_service_main, service_main);
fn service_main(_args: Vec<OsString>) {
if let Err(e) = run_service() {
log(&format!("the service stopped: {e}"));
}
}
fn run_service() -> Result<()> {
let (tx, rx) = std::sync::mpsc::channel::<()>();
let handle =
service_control_handler::register(SERVICE_NAME, move |control| match control {
ServiceControl::Stop | ServiceControl::Shutdown => {
let _ = tx.send(());
ServiceControlHandlerResult::NoError
}
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
_ => ServiceControlHandlerResult::NotImplemented,
})
.map_err(|e| Error::Config(format!("cannot register the control handler: {e}")))?;
let status = |state, exit, controls| ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: state,
controls_accepted: controls,
exit_code: exit,
checkpoint: 0,
wait_hint: std::time::Duration::from_secs(10),
process_id: None,
};
handle
.set_service_status(status(
ServiceState::Running,
ServiceExitCode::Win32(0),
ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
))
.map_err(|e| Error::Config(format!("cannot report Running: {e}")))?;
let outcome = super::run_serve(rx);
let exit = match &outcome {
Ok(()) => ServiceExitCode::Win32(0),
Err(_) => ServiceExitCode::ServiceSpecific(1),
};
let _ = handle.set_service_status(status(
ServiceState::Stopped,
exit,
ServiceControlAccept::empty(),
));
outcome
}
pub fn try_dispatch() -> Result<bool> {
match service_dispatcher::start(SERVICE_NAME, ffi_service_main) {
Ok(()) => Ok(true),
Err(windows_service::Error::Winapi(e)) if e.raw_os_error() == Some(NOT_A_SERVICE) => {
Ok(false)
}
Err(e) => Err(Error::Config(format!(
"the service control manager refused the connection: {e}"
))),
}
}
}
pub use platform::{install, set_state, status, try_dispatch, uninstall};