fix(ariadne): Theseus on/off toggle never reached the tasks; updater read a stale manifest; uninstall left NRPT rules

Theseus (Settings › Plug-ins › Ariadne's Thread):
- The elevated start/stop script was embedded in a double-quoted outer
  PowerShell string, so `$t` was interpolated away before the elevated
  shell saw it. It received `foreach ( in …)`, failed to parse, and the
  outer shell still exited 0 — "Turn on/off" reported success while doing
  nothing, in every shipped build. The script now goes across as
  -EncodedCommand. Off = Stop + Disable (the daemon task has an
  at-startup trigger, so a plain stop came back on reboot); on = Enable +
  Start. Exit 2 = daemon task missing, surfaced as a clear error.
- Version/update check now reads dl.silentmode.st's releases manifest,
  the same one the Theseus updater uses. The silentmode.st copy lagged a
  day behind (still listing Theseus 0.3.31), so a new Ariadne release
  published to dl would not have been offered.
- Install/update/uninstall now propagate the installer's exit code
  (-PassThru; exit $p.ExitCode) instead of always reading as success.

Resolver package (needs a new installer build to reach users):
- uninstall.ps1 removed only the ".bch" NRPT rule; install.ps1 adds one
  per advertised TLD. Sweep every "BNS .<tld> resolver" rule.

Verified: daemon resolves BNS names and passes ICANN A/AAAA through when
run unprivileged on port 15353; the encoded-command construction runs
intact and propagates exit codes 0/2 in an unelevated reproduction.
This commit is contained in:
Local Dev 2026-09-09 23:04:23 +02:00
parent 56eff58fda
commit c9dbcbde86

50
main.js
View file

@ -3797,7 +3797,11 @@ const ARIADNE_UNINSTALL_KEYS = [
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1',
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1',
];
const ARIADNE_MANIFEST_URL = "https://silentmode.st/releases-manifest.json";
// Same manifest the Theseus updater reads (UPDATE_MANIFEST_URL). The copy on
// silentmode.st is a mirror that has lagged behind dl.silentmode.st (2026-09-09:
// it still listed Theseus 0.3.31 while dl had 0.3.44), so the Ariadne "Update"
// button was checking against a stale version list.
const ARIADNE_MANIFEST_URL = "https://dl.silentmode.st/releases-manifest.json";
const ARIADNE_DL_ORIGIN = "https://dl.silentmode.st";
const ARIADNE_MANIFEST_TTL_MS = 30 * 60 * 1000;
let ariadneManifestCache = null; // { at:number, entry:{version,filename,sha256,url}|null }
@ -3920,11 +3924,41 @@ function cmpVersions(a, b) {
function ariadneSetState(on) {
return new Promise((resolve, reject) => {
const { spawn } = require("child_process");
const verb = on ? "Start-ScheduledTask" : "Stop-ScheduledTask";
const cmd = `foreach ($t in 'BNS Resolver Daemon','BNS Sia Bridge') { try { ${verb} -TaskName $t -ErrorAction Stop } catch {} }`;
// Off = stop AND disable, on = enable AND start. The daemon task has an
// at-startup trigger, so a plain Stop-ScheduledTask came back on the
// next reboot and "Turn off" silently didn't stick.
//
// The inner script goes across as -EncodedCommand (base64 UTF-16LE). The
// previous version embedded it in a double-quoted string on the OUTER
// powershell's command line, which interpolated `$t` to nothing before
// the elevated shell ever saw it — the elevated shell got
// `foreach ( in ...)`, failed to parse, and the outer shell still exited
// 0, so the toggle reported success while doing nothing.
const inner = on
? `$ok = $true
foreach ($t in 'BNS Resolver Daemon','BNS Sia Bridge') {
try { Enable-ScheduledTask -TaskName $t -ErrorAction Stop | Out-Null; Start-ScheduledTask -TaskName $t -ErrorAction Stop }
catch { if ($t -eq 'BNS Resolver Daemon') { $ok = $false } }
}
if ($ok) { exit 0 } else { exit 2 }`
: `$ok = $true
foreach ($t in 'BNS Resolver Daemon','BNS Sia Bridge') {
try { Stop-ScheduledTask -TaskName $t -ErrorAction Stop } catch {}
try { Disable-ScheduledTask -TaskName $t -ErrorAction Stop | Out-Null }
catch { if ($t -eq 'BNS Resolver Daemon') { $ok = $false } }
}
if ($ok) { exit 0 } else { exit 2 }`;
const encoded = Buffer.from(inner, "utf16le").toString("base64");
// -PassThru + exit $p.ExitCode: the elevated shell's exit code (0 ok,
// 2 = daemon task missing) reaches us; a declined UAC prompt makes
// Start-Process throw, which exits the outer shell non-zero.
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
"Start-Process powershell -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList '-NoProfile','-Command',\"" + cmd.replace(/"/g, "`\"") + "\""], { windowsHide: true });
ps.on("close", (code) => { if (code === 0) resolve(true); else reject(new Error("UAC declined or task failed (exit " + code + ")")); });
`$p = Start-Process powershell -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand','${encoded}'; exit $p.ExitCode`], { windowsHide: true });
ps.on("close", (code) => {
if (code === 0) resolve(true);
else if (code === 2) reject(new Error("the 'BNS Resolver Daemon' scheduled task is missing — reinstall Ariadne's Thread"));
else reject(new Error("UAC declined or task failed (exit " + code + ")"));
});
ps.on("error", (e) => reject(e));
});
}
@ -3938,8 +3972,10 @@ async function ariadneInstall() {
const exePath = await ariadneDownloadInstaller(entry);
const { spawn } = require("child_process");
return new Promise((resolve, reject) => {
// -PassThru + exit $p.ExitCode so Inno's own exit code reaches us; a bare
// -Wait always returned 0 and a failed silent install looked like success.
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
`Start-Process -FilePath '${exePath.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait`
`$p = Start-Process -FilePath '${exePath.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
], { windowsHide: true });
const cleanup = () => { try { fs.unlinkSync(exePath); } catch {} };
ps.on("close", (code) => { cleanup(); code === 0 ? resolve(true) : reject(new Error("installer exited " + code)); });
@ -3962,7 +3998,7 @@ function ariadneUninstall() {
if (!uninstallCmd) return reject(new Error("Ariadne uninstaller not registered"));
// uninstallCmd typically: "C:\Program Files (x86)\...\unins000.exe" /VERYSILENT
// Spawn it elevated. Inno's silent uninstall respects /VERYSILENT so no UI.
const runCmd = `Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${uninstallCmd.replace(/'/g, "''")} /SUPPRESSMSGBOXES /NORESTART' -Verb RunAs -Wait`;
const runCmd = `$p = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${uninstallCmd.replace(/'/g, "''")} /SUPPRESSMSGBOXES /NORESTART' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`;
const ps2 = spawn("powershell.exe", ["-NoProfile", "-Command", runCmd], { windowsHide: true });
ps2.on("close", (code) => code === 0 ? resolve(true) : reject(new Error("uninstaller exited " + code)));
ps2.on("error", reject);