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
5 changed files with 78 additions and 10 deletions
Showing only changes of commit 22ec684e48 - Show all commits

48
build.rs Normal file
View File

@@ -0,0 +1,48 @@
fn main() {
// --long forces "TAG-N-gSHA[-dirty]" format even on exact tag matches,
// making parsing unambiguous for pre-release tags like v0.14.0-rc1.
let git_version = std::process::Command::new("git")
.args(["describe", "--tags", "--always", "--dirty", "--long"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|raw| parse_git_describe(raw.trim()));
if let Some(v) = git_version {
println!("cargo:rustc-env=NUMA_BUILD_VERSION={}", v);
}
println!("cargo:rerun-if-changed=.git/HEAD");
}
/// Parse `git describe --long` output into a SemVer-compatible string.
/// "v0.13.1-0-ga87f907" → "0.13.1"
/// "v0.13.1-9-ga87f907" → "0.13.1+a87f907"
/// "v0.14.0-rc1-0-ga87f907" → "0.14.0-rc1"
/// "v0.14.0-rc1-3-ga87f907-dirty" → "0.14.0-rc1+a87f907-dirty"
/// "a87f907" → "0.0.0+a87f907"
fn parse_git_describe(s: &str) -> Option<String> {
let s = s.strip_prefix('v').unwrap_or(s);
let dirty = s.ends_with("-dirty");
let s = s.strip_suffix("-dirty").unwrap_or(s);
// --long format: TAG-N-gSHA. Split from the right so tags with hyphens work.
let gpos = s.rfind("-g")?;
let sha = &s[gpos + 2..];
let rest = &s[..gpos];
let npos = rest.rfind('-')?;
let n: u32 = rest[npos + 1..].parse().ok()?;
let tag = &rest[..npos];
if tag.is_empty() {
return Some(format!("0.0.0+{}", sha));
}
Some(match (n, dirty) {
(0, false) => tag.to_string(),
(0, true) => format!("{}+{}-dirty", tag, sha),
(_, false) => format!("{}+{}", tag, sha),
(_, true) => format!("{}+{}-dirty", tag, sha),
})
}

View File

@@ -552,7 +552,11 @@ body {
@media (max-width: 700px) { @media (max-width: 700px) {
.stats-row { grid-template-columns: repeat(2, 1fr); } .stats-row { grid-template-columns: repeat(2, 1fr); }
.dashboard { padding: 1rem; } .dashboard { padding: 1rem; }
.header { padding: 1rem; } .header { padding: 0.8rem 1rem; }
.logo { font-size: 1.4rem; }
.tagline { display: none; }
#headerVersion { display: none; }
#phoneSetup { display: none; }
} }
</style> </style>
</head> </head>
@@ -561,6 +565,7 @@ body {
<div class="header"> <div class="header">
<div class="header-left"> <div class="header-left">
<div class="logo">Numa</div> <div class="logo">Numa</div>
<span id="headerVersion" style="font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);"></span>
<div class="tagline">DNS that governs itself</div> <div class="tagline">DNS that governs itself</div>
</div> </div>
<div style="display:flex;align-items:center;gap:1.2rem;"> <div style="display:flex;align-items:center;gap:1.2rem;">
@@ -1136,16 +1141,20 @@ async function refresh() {
document.getElementById('totalQueries').textContent = formatNumber(q.total); document.getElementById('totalQueries').textContent = formatNumber(q.total);
document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs); document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs);
document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs); document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs);
document.getElementById('headerVersion').textContent = stats.version ? 'v' + stats.version : '';
document.getElementById('footerUpstream').textContent = stats.upstream || ''; document.getElementById('footerUpstream').textContent = stats.upstream || '';
document.getElementById('footerConfig').textContent = stats.config_path || ''; document.getElementById('footerConfig').textContent = stats.config_path || '';
document.getElementById('footerData').textContent = stats.data_dir || ''; document.getElementById('footerData').textContent = stats.data_dir || '';
const modeEl = document.getElementById('footerMode');
modeEl.textContent = stats.mode || '—';
modeEl.style.color = stats.mode === 'recursive' ? 'var(--emerald)' : 'var(--amber)';
document.getElementById('footerDnssec').textContent = stats.dnssec ? 'on' : 'off'; document.getElementById('footerDnssec').textContent = stats.dnssec ? 'on' : 'off';
document.getElementById('footerDnssec').style.color = stats.dnssec ? 'var(--emerald)' : 'var(--text-dim)'; document.getElementById('footerDnssec').style.color = stats.dnssec ? 'var(--emerald)' : 'var(--text-dim)';
document.getElementById('footerSrtt').textContent = stats.srtt ? 'on' : 'off'; document.getElementById('footerSrtt').textContent = stats.srtt ? 'on' : 'off';
document.getElementById('footerSrtt').style.color = stats.srtt ? 'var(--emerald)' : 'var(--text-dim)'; document.getElementById('footerSrtt').style.color = stats.srtt ? 'var(--emerald)' : 'var(--text-dim)';
if (!document.getElementById('footerLogs').textContent) {
const isMac = stats.data_dir && stats.data_dir.includes('/usr/local/');
document.getElementById('footerLogs').textContent = isMac
? '/usr/local/var/log/numa.log'
: 'journalctl -u numa -f';
}
// LAN status indicator // LAN status indicator
const lanEl = document.getElementById('lanToggle'); const lanEl = document.getElementById('lanToggle');
@@ -1345,6 +1354,7 @@ function renderBlockingInfo(info) {
} }
function renderAllowlist(entries) { function renderAllowlist(entries) {
if (document.activeElement && document.activeElement.id === 'allowDomainInput') return;
const el = document.getElementById('blockingAllowlist'); const el = document.getElementById('blockingAllowlist');
const count = entries.length; const count = entries.length;
el.innerHTML = ` el.innerHTML = `
@@ -1504,14 +1514,14 @@ refresh();
setInterval(refresh, 2000); setInterval(refresh, 2000);
</script> </script>
<div style="text-align:center;padding:0.8rem;font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);"> <div style="text-align:center;padding:0.8rem 0.8rem 0.4rem;font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);line-height:1.8;">
Config: <span id="footerConfig" style="user-select:all;color:var(--emerald);"></span> Config: <span id="footerConfig" style="user-select:all;color:var(--emerald);"></span>
· Data: <span id="footerData" style="user-select:all;color:var(--emerald);"></span> · Data: <span id="footerData" style="user-select:all;color:var(--emerald);"></span>
· Upstream: <span id="footerUpstream" style="user-select:all;color:var(--emerald);"></span> · Logs: <span id="footerLogs" style="user-select:all;color:var(--emerald);"></span>
· Mode: <span id="footerMode" style="color:var(--text-dim);"></span> <br>
Upstream: <span id="footerUpstream" style="user-select:all;color:var(--emerald);"></span>
· DNSSEC: <span id="footerDnssec" style="color:var(--text-dim);"></span> · DNSSEC: <span id="footerDnssec" style="color:var(--text-dim);"></span>
· SRTT: <span id="footerSrtt" style="color:var(--text-dim);"></span> · SRTT: <span id="footerSrtt" style="color:var(--text-dim);"></span>
· Logs: <span style="user-select:all;color:var(--emerald);">macOS: /usr/local/var/log/numa.log · Linux: journalctl -u numa -f</span>
· <a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener" style="color:var(--amber);text-decoration:none;">GitHub</a> · <a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener" style="color:var(--amber);text-decoration:none;">GitHub</a>
</div> </div>

View File

@@ -160,6 +160,7 @@ struct QueryLogResponse {
#[derive(Serialize)] #[derive(Serialize)]
struct StatsResponse { struct StatsResponse {
version: &'static str,
uptime_secs: u64, uptime_secs: u64,
upstream: String, upstream: String,
mode: &'static str, // "recursive" or "forward" — never "auto" at runtime mode: &'static str, // "recursive" or "forward" — never "auto" at runtime
@@ -539,6 +540,7 @@ async fn stats(State(ctx): State<Arc<ServerCtx>>) -> Json<StatsResponse> {
}; };
Json(StatsResponse { Json(StatsResponse {
version: crate::version(),
uptime_secs: snap.uptime_secs, uptime_secs: snap.uptime_secs,
upstream, upstream,
mode: ctx.upstream_mode.as_str(), mode: ctx.upstream_mode.as_str(),

View File

@@ -43,7 +43,7 @@ impl HealthMeta {
#[cfg(test)] #[cfg(test)]
pub fn test_fixture() -> Self { pub fn test_fixture() -> Self {
HealthMeta { HealthMeta {
version: env!("CARGO_PKG_VERSION"), version: crate::version(),
hostname: "test-host".to_string(), hostname: "test-host".to_string(),
sni: "numa.numa".to_string(), sni: "numa.numa".to_string(),
dot_enabled: false, dot_enabled: false,
@@ -99,7 +99,7 @@ impl HealthMeta {
} }
HealthMeta { HealthMeta {
version: env!("CARGO_PKG_VERSION"), version: crate::version(),
hostname: crate::hostname(), hostname: crate::hostname(),
sni: "numa.numa".to_string(), sni: "numa.numa".to_string(),
dot_enabled, dot_enabled,

View File

@@ -38,6 +38,14 @@ pub(crate) mod testutil;
pub type Error = Box<dyn std::error::Error + Send + Sync>; pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// Build version string. On tagged releases: `0.13.1`. On commits ahead
/// of a tag: `0.13.1+a87f907`. With uncommitted changes: `0.13.1+a87f907-dirty`.
/// Falls back to `CARGO_PKG_VERSION` when built outside a git repo (e.g.
/// from a source tarball).
pub fn version() -> &'static str {
option_env!("NUMA_BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
}
/// Detect the machine hostname via the `hostname` command. Returns the /// Detect the machine hostname via the `hostname` command. Returns the
/// full hostname (e.g., `macbook-pro.local`), or `"numa"` if the command /// full hostname (e.g., `macbook-pro.local`), or `"numa"` if the command
/// fails. Call sites that need the short form (e.g., mDNS instance /// fails. Call sites that need the short form (e.g., mDNS instance