refactor(server): extract shared runtime and implement service install/run in arbiter-server.exe
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-04-02 18:31:05 +02:00
parent 86052c9350
commit 2fb5bb3d84
9 changed files with 587 additions and 48 deletions

View File

@@ -0,0 +1,229 @@
use std::{
ffi::OsString,
path::{Path, PathBuf},
process::Command,
sync::mpsc,
time::Duration,
};
use miette::{Context as _, IntoDiagnostic as _, miette};
use windows_service::{
define_windows_service,
service::{
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
},
service_control_handler::{self, ServiceControlHandlerResult},
service_dispatcher,
service_manager::{ServiceManager, ServiceManagerAccess},
};
use crate::cli::{ServiceInstallArgs, ServiceRunArgs};
use arbiter_server::runtime::{RunConfig, run_server_until_shutdown};
const SERVICE_NAME: &str = "ArbiterServer";
const SERVICE_DISPLAY_NAME: &str = "Arbiter Server";
pub fn default_service_data_dir() -> PathBuf {
let base = std::env::var_os("PROGRAMDATA")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
base.join("Arbiter")
}
pub fn install_service(args: ServiceInstallArgs) -> miette::Result<()> {
ensure_admin_rights()?;
let executable = std::env::current_exe().into_diagnostic()?;
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
std::fs::create_dir_all(&data_dir)
.into_diagnostic()
.with_context(|| format!("failed to create service data dir: {}", data_dir.display()))?;
ensure_token_acl_contract(&data_dir)?;
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let manager = ServiceManager::local_computer(None::<&str>, manager_access)
.into_diagnostic()
.wrap_err("failed to open Service Control Manager")?;
let launch_arguments = vec![
OsString::from("service"),
OsString::from("run"),
OsString::from("--data-dir"),
data_dir.as_os_str().to_os_string(),
];
let service_info = ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from(SERVICE_DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::OnDemand,
error_control: ServiceErrorControl::Normal,
executable_path: executable,
launch_arguments,
dependencies: vec![],
account_name: Some(OsString::from(r"NT AUTHORITY\LocalService")),
account_password: None,
};
let service = manager
.create_service(
&service_info,
ServiceAccess::QUERY_STATUS | ServiceAccess::START,
)
.into_diagnostic()
.wrap_err("failed to create Windows service in SCM")?;
if args.start {
service
.start::<&str>(&[])
.into_diagnostic()
.wrap_err("service created but failed to start")?;
}
Ok(())
}
pub fn run_service_dispatcher(args: ServiceRunArgs) -> miette::Result<()> {
SERVICE_RUN_ARGS
.set(args)
.map_err(|_| miette!("service runtime args are already initialized"))?;
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
.into_diagnostic()
.wrap_err("failed to start service dispatcher")?;
Ok(())
}
define_windows_service!(ffi_service_main, service_main);
static SERVICE_RUN_ARGS: std::sync::OnceLock<ServiceRunArgs> = std::sync::OnceLock::new();
fn service_main(_arguments: Vec<OsString>) {
if let Err(error) = run_service_main() {
tracing::error!(error = ?error, "Windows service main failed");
}
}
fn run_service_main() -> miette::Result<()> {
let args = SERVICE_RUN_ARGS
.get()
.cloned()
.ok_or_else(|| miette!("service run args are missing"))?;
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
let status_handle =
service_control_handler::register(SERVICE_NAME, move |control| match control {
ServiceControl::Stop => {
let _ = shutdown_tx.send(());
ServiceControlHandlerResult::NoError
}
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
_ => ServiceControlHandlerResult::NotImplemented,
})
.into_diagnostic()
.wrap_err("failed to register service control handler")?;
set_status(
&status_handle,
ServiceState::StartPending,
ServiceControlAccept::empty(),
)?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.into_diagnostic()
.wrap_err("failed to build tokio runtime for service")?;
set_status(
&status_handle,
ServiceState::Running,
ServiceControlAccept::STOP,
)?;
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
let config = RunConfig {
addr: args.listen_addr,
data_dir: Some(data_dir),
log_arbiter_url: true,
};
let result = runtime.block_on(run_server_until_shutdown(config, async move {
let _ = tokio::task::spawn_blocking(move || shutdown_rx.recv()).await;
}));
set_status(
&status_handle,
ServiceState::Stopped,
ServiceControlAccept::empty(),
)?;
result
}
fn set_status(
status_handle: &service_control_handler::ServiceStatusHandle,
current_state: ServiceState,
controls_accepted: ServiceControlAccept,
) -> miette::Result<()> {
status_handle
.set_service_status(ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state,
controls_accepted,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::from_secs(10),
process_id: None,
})
.into_diagnostic()
.wrap_err("failed to update service state")?;
Ok(())
}
fn ensure_admin_rights() -> miette::Result<()> {
let status = Command::new("net")
.arg("session")
.status()
.into_diagnostic()
.wrap_err("failed to check administrator rights")?;
if status.success() {
Ok(())
} else {
Err(miette!(
"administrator privileges are required to install Windows service"
))
}
}
fn ensure_token_acl_contract(data_dir: &Path) -> miette::Result<()> {
// IMPORTANT: This ACL setup is intentionally explicit and should not be simplified away,
// because service-account and interactive-user access requirements are different in production.
let target = data_dir.as_os_str();
let status = Command::new("icacls")
.arg(target)
.arg("/grant")
.arg("*S-1-5-19:(OI)(CI)M")
.arg("/grant")
.arg("*S-1-5-32-545:(OI)(CI)RX")
.arg("/T")
.arg("/C")
.status()
.into_diagnostic()
.wrap_err("failed to apply ACLs for service data directory")?;
if status.success() {
Ok(())
} else {
Err(miette!(
"failed to ensure ACL contract for service data directory"
))
}
}