use super::super::*;
#[cfg(windows)]
pub(crate) fn install_windows_service(
binary: &Path,
config_path: &Path,
profile: Option<&str>,
enable_event_log: bool,
) -> Result<()> {
let config_abs =
std::fs::canonicalize(config_path).unwrap_or_else(|_| config_path.to_path_buf());
let config_str = config_abs.display().to_string();
let config_str = config_str.strip_prefix(r"\\?\").unwrap_or(&config_str);
let binary_str = binary.display().to_string();
let binary_str = binary_str.strip_prefix(r"\\?\").unwrap_or(&binary_str);
let mut bin_args = format!(
"\"{}\" daemon service --config \"{}\"",
binary_str, config_str,
);
if let Some(p) = profile {
bin_args.push_str(&format!(" --profile \"{}\"", p));
}
if enable_event_log {
bin_args.push_str(" --enable-event-log");
}
let output = std::process::Command::new("sc.exe")
.args([
"create",
"cfgd",
"binPath=",
&bin_args,
"start=",
"auto",
"DisplayName=",
"cfgd Configuration Manager",
])
.output()
.map_err(|e| DaemonError::ServiceInstallFailed {
message: format!("sc.exe create failed: {}", e),
})?;
if !output.status.success() {
return Err(DaemonError::ServiceInstallFailed {
message: format!(
"sc.exe create failed: {}",
crate::stdout_lossy_trimmed(&output)
),
}
.into());
}
if let Err(e) = std::process::Command::new("sc.exe")
.args([
"description",
"cfgd",
"Declarative machine configuration management daemon",
])
.output()
{
tracing::warn!(error = %e, "failed to set Windows Service description");
}
if enable_event_log {
register_event_source();
}
if let Err(e) = std::process::Command::new("sc.exe")
.args(["start", "cfgd"])
.output()
{
tracing::warn!(error = %e, "failed to start Windows Service");
}
tracing::info!(
event_log = enable_event_log,
"installed Windows Service: cfgd"
);
Ok(())
}
#[cfg(windows)]
fn register_event_source() {
let key = r"HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\cfgd";
let msg_file = r"%SystemRoot%\System32\EventCreate.exe";
let _ = std::process::Command::new("reg.exe")
.args([
"add",
key,
"/v",
"EventMessageFile",
"/t",
"REG_EXPAND_SZ",
"/d",
msg_file,
"/f",
])
.output();
let _ = std::process::Command::new("reg.exe")
.args([
"add",
key,
"/v",
"TypesSupported",
"/t",
"REG_DWORD",
"/d",
"0x7",
"/f",
])
.output();
}
#[cfg(windows)]
pub(crate) fn uninstall_windows_service() -> Result<()> {
if let Err(e) = std::process::Command::new("sc.exe")
.args(["stop", "cfgd"])
.output()
{
tracing::debug!(error = %e, "sc.exe stop (pre-uninstall)");
}
let output = std::process::Command::new("sc.exe")
.args(["delete", "cfgd"])
.output()
.map_err(|e| DaemonError::ServiceInstallFailed {
message: format!("sc.exe delete failed: {}", e),
})?;
if !output.status.success() {
let stdout = crate::stdout_lossy_trimmed(&output);
if stdout.contains("1060") || stdout.contains("does not exist") {
tracing::debug!("cfgd Windows Service not found; nothing to remove");
return Ok(());
}
return Err(DaemonError::ServiceInstallFailed {
message: format!("sc.exe delete failed: {}", stdout),
}
.into());
}
let _ = std::process::Command::new("reg.exe")
.args([
"delete",
r"HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\cfgd",
"/f",
])
.output();
tracing::info!("removed Windows Service: cfgd");
Ok(())
}
#[cfg(windows)]
static SERVICE_HOOKS: std::sync::OnceLock<Arc<dyn DaemonHooks>> = std::sync::OnceLock::new();
#[cfg(windows)]
pub fn run_as_windows_service(hooks: Arc<dyn DaemonHooks>) -> Result<()> {
use windows_service::service_dispatcher;
let _ = SERVICE_HOOKS.set(hooks);
service_dispatcher::start("cfgd", ffi_service_main).map_err(|e| DaemonError::ServiceError {
message: format!("failed to start service dispatcher: {}", e),
})?;
Ok(())
}
#[cfg(not(windows))]
pub fn run_as_windows_service(_hooks: Arc<dyn DaemonHooks>) -> Result<()> {
Err(DaemonError::ServiceError {
message: "Windows Service mode is only available on Windows".to_string(),
}
.into())
}
#[cfg(windows)]
extern "system" fn ffi_service_main(_argc: u32, _argv: *mut *mut u16) {
if let Err(e) = windows_service_main() {
tracing::error!(error = %e, "windows service main failed");
}
}
#[cfg(windows)]
fn event_log_requested() -> bool {
if std::env::var("CFGD_WINDOWS_EVENT_LOG")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
{
return true;
}
std::env::args().any(|a| a == "--enable-event-log")
}
#[cfg(windows)]
pub(crate) fn init_windows_logging() {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let log_dir = std::env::var("LOCALAPPDATA")
.map(|d| PathBuf::from(d).join("cfgd"))
.unwrap_or_else(|_| crate::default_config_dir());
let _ = std::fs::create_dir_all(&log_dir);
let log_path = log_dir.join("daemon.log");
let file = match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
Ok(f) => f,
Err(_) => return,
};
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(std::sync::Mutex::new(file))
.with_ansi(false)
.with_target(false);
let event_log_layer = if event_log_requested() {
Some(super::windows_eventlog::EventLogLayer)
} else {
None
};
let _ = tracing_subscriber::registry()
.with(file_layer)
.with(event_log_layer)
.try_init();
}
#[cfg(windows)]
pub(crate) fn windows_service_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
use windows_service::service::*;
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
init_windows_logging();
let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel();
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
ServiceControl::Stop | ServiceControl::Shutdown => {
let _ = shutdown_tx.send(());
ServiceControlHandlerResult::NoError
}
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle = service_control_handler::register("cfgd", event_handler)?;
status_handle.set_service_status(ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::StartPending,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 1,
wait_hint: std::time::Duration::from_secs(10),
process_id: None,
})?;
let args: Vec<String> = std::env::args().collect();
let mut config_path = crate::default_config_dir().join("config.yaml");
let mut profile_override: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--config" if i + 1 < args.len() => {
config_path = PathBuf::from(&args[i + 1]);
i += 2;
}
"--profile" if i + 1 < args.len() => {
profile_override = Some(args[i + 1].clone());
i += 2;
}
_ => {
i += 1;
}
}
}
let hooks = SERVICE_HOOKS
.get()
.ok_or("SERVICE_HOOKS not initialized — run_as_windows_service must be called first")?
.clone();
let rt = tokio::runtime::Runtime::new()?;
let printer = Arc::new(crate::output::Printer::new(crate::output::Verbosity::Quiet));
rt.spawn(async move {
if let Err(e) = run_daemon(config_path, profile_override, printer, hooks).await {
tracing::error!(error = %e, "daemon error");
}
});
status_handle.set_service_status(ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: std::time::Duration::default(),
process_id: None,
})?;
let _ = shutdown_rx.recv();
status_handle.set_service_status(ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::StopPending,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 1,
wait_hint: std::time::Duration::from_secs(5),
process_id: None,
})?;
rt.shutdown_timeout(std::time::Duration::from_secs(5));
super::windows_eventlog::deregister_source();
status_handle.set_service_status(ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: std::time::Duration::default(),
process_id: None,
})?;
Ok(())
}