feat(windows): run as a real SCM service, not a Run-key autostart #107

Merged
razvandimescu merged 16 commits from feat/windows-service into main 2026-04-17 07:02:43 +08:00
2 changed files with 75 additions and 31 deletions
Showing only changes of commit 6789c321bc - Show all commits

View File

@@ -572,7 +572,7 @@ fn windows_backup_path() -> std::path::PathBuf {
#[cfg(windows)] #[cfg(windows)]
fn disable_dnscache() -> Result<bool, String> { fn disable_dnscache() -> Result<bool, String> {
// Check if Dnscache is running (it holds port 53 at kernel level) // Check if Dnscache is running (it can hold port 53)
let output = std::process::Command::new("sc") let output = std::process::Command::new("sc")
.args(["query", "Dnscache"]) .args(["query", "Dnscache"])
.output() .output()
@@ -603,8 +603,16 @@ fn disable_dnscache() -> Result<bool, String> {
return Err("failed to disable Dnscache via registry (run as Administrator?)".into()); return Err("failed to disable Dnscache via registry (run as Administrator?)".into());
} }
eprintln!(" Dnscache disabled. A reboot is required to free port 53."); // Dnscache is disabled for next boot. Check whether port 53 is
Ok(true) // actually blocked right now — on many Windows configurations
// Dnscache doesn't bind port 53 even while running.
let port_blocked = std::net::UdpSocket::bind("127.0.0.1:53").is_err();
if port_blocked {
eprintln!(" Dnscache disabled. A reboot is required to free port 53.");
} else {
eprintln!(" Dnscache disabled. Port 53 is free.");
}
Ok(port_blocked)
} }
#[cfg(windows)] #[cfg(windows)]
@@ -671,31 +679,6 @@ fn install_windows() -> Result<(), String> {
std::fs::write(&path, json).map_err(|e| format!("failed to write backup: {}", e))?; std::fs::write(&path, json).map_err(|e| format!("failed to write backup: {}", e))?;
} }
for name in interfaces.keys() {
let status = std::process::Command::new("netsh")
.args([
"interface",
"ipv4",
"set",
"dnsservers",
name,
"static",
"127.0.0.1",
"primary",
])
.status()
.map_err(|e| format!("failed to set DNS for {}: {}", name, e))?;
if status.success() {
eprintln!(" set DNS for \"{}\" -> 127.0.0.1", name);
} else {
eprintln!(
" warning: failed to set DNS for \"{}\" (run as Administrator?)",
name
);
}
}
let needs_reboot = disable_dnscache()?; let needs_reboot = disable_dnscache()?;
// On re-install, stop the running service first so the binary can be // On re-install, stop the running service first so the binary can be
@@ -710,9 +693,14 @@ fn install_windows() -> Result<(), String> {
let service_exe = install_service_binary()?; let service_exe = install_service_binary()?;
register_service_scm(&service_exe)?; register_service_scm(&service_exe)?;
// If no reboot is pending (Dnscache wasn't running, port 53 free), if needs_reboot {
// start the service immediately. Otherwise it'll launch on next boot. // Dnscache still holds port 53 until reboot. Do NOT redirect DNS
if !needs_reboot { // yet — nothing is listening on 127.0.0.1:53, so redirecting now
// would kill DNS. The service will call redirect_dns_to_localhost()
// on its first startup after reboot.
} else {
redirect_dns_with_interfaces(&interfaces)?;
match start_service_scm() { match start_service_scm() {
Ok(_) => eprintln!(" Service started."), Ok(_) => eprintln!(" Service started."),
Err(e) => eprintln!( Err(e) => eprintln!(
@@ -756,6 +744,45 @@ fn run_sc(args: &[&str]) -> Result<std::process::Output, String> {
Ok(out) Ok(out)
} }
/// Point all active network interfaces at 127.0.0.1 so Numa handles DNS.
/// Called from the service on first boot after a reboot that freed Dnscache.
#[cfg(windows)]
pub fn redirect_dns_to_localhost() -> Result<(), String> {
let interfaces = get_windows_interfaces()?;
redirect_dns_with_interfaces(&interfaces)
}
#[cfg(windows)]
fn redirect_dns_with_interfaces(
interfaces: &std::collections::HashMap<String, WindowsInterfaceDns>,
) -> Result<(), String> {
for name in interfaces.keys() {
let status = std::process::Command::new("netsh")
.args([
"interface",
"ipv4",
"set",
"dnsservers",
name,
"static",
"127.0.0.1",
"primary",
])
.status()
.map_err(|e| format!("failed to set DNS for {}: {}", name, e))?;
if status.success() {
eprintln!(" set DNS for \"{}\" -> 127.0.0.1", name);
} else {
eprintln!(
" warning: failed to set DNS for \"{}\" (run as Administrator?)",
name
);
}
}
Ok(())
}
/// Copy the currently-running binary to the service install location. SCM /// Copy the currently-running binary to the service install location. SCM
/// keeps a handle to this path, so it must be stable across user sessions. /// keeps a handle to this path, so it must be stable across user sessions.
#[cfg(windows)] #[cfg(windows)]

View File

@@ -83,6 +83,23 @@ fn run_service() -> windows_service::Result<()> {
let _ = server_done_tx.send(()); let _ = server_done_tx.send(());
}); });
// Wait for the API to be ready, then ensure DNS points at localhost.
// On first boot after install (Dnscache was disabled, reboot freed
// port 53), the installer deferred the DNS redirect — do it now.
let api_up = (0..20).any(|i| {
if i > 0 {
std::thread::sleep(Duration::from_millis(500));
}
std::net::TcpStream::connect(("127.0.0.1", crate::config::DEFAULT_API_PORT)).is_ok()
});
if api_up {
if let Err(e) = crate::system_dns::redirect_dns_to_localhost() {
log::warn!("could not redirect DNS to localhost: {}", e);
}
} else {
log::error!("numa API did not start within 10s — DNS not redirected");
}
// Wait for either SCM stop or server termination. // Wait for either SCM stop or server termination.
loop { loop {
if shutdown_rx.recv_timeout(Duration::from_millis(500)).is_ok() { if shutdown_rx.recv_timeout(Duration::from_millis(500)).is_ok() {