fix(theseus): Ariadne's Thread card — find the installed resolver, answer in ~2 s

Two reasons the Plug-ins card looked dead ("only a Refresh button"):

1. The state check ran Get-ScheduledTask, whose module import took 8–10 s
   cold, and only then fetched the release manifest. Every button is hidden
   during "checking…", so for 10–15 s the card showed nothing but Refresh.
   Task state now comes from the Task Scheduler COM object (numeric, locale-
   independent — schtasks.exe prints localized words on non-English
   Windows) and the manifest fetch runs in parallel: ~2 s.

2. The Inno installer's AppId is written as {{…}}, which Inno registers as
   {…}}_is1 (doubled closing brace). Theseus looked for the single-brace
   key, never found it, and so never knew the installed version — no Update
   button, no Uninstall button. The entry is now found by DisplayName.
This commit is contained in:
Local Dev 2026-09-16 20:15:19 +02:00
parent 27a1243e4d
commit ed323713d7

80
main.js
View file

@ -4067,10 +4067,16 @@ ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} }
const ARIADNE_TASKS = ["BNS Resolver Daemon", "BNS Sia Bridge"]; const ARIADNE_TASKS = ["BNS Resolver Daemon", "BNS Sia Bridge"];
// Inno Setup's AppId + "_is1" is the uninstall registry key. Check both // Inno Setup's AppId + "_is1" is the uninstall registry key. Check both
// native and WOW6432 in case Inno installed either way. // native and WOW6432 in case Inno installed either way.
const ARIADNE_UNINSTALL_KEYS = [ // PowerShell fragment that leaves Ariadne's Inno uninstall entry in $r (or
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1', // $null). Found by DisplayName rather than by key path: the installer's
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1', // AppId is written as `{{…}}`, which Inno stores as `{…}}_is1` (doubled
]; // closing brace), so the literal key path Theseus used to look for never
// matched — the panel showed no version, no Update, no Uninstall. Matching
// on the name also survives a future AppId fix.
const ARIADNE_REG_LOOKUP =
"$r=$null;foreach($root in 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'){" +
"if($r){break};foreach($k in (Get-ChildItem $root -ErrorAction SilentlyContinue)){$p=Get-ItemProperty $k.PSPath -ErrorAction SilentlyContinue;" +
"if($p.DisplayName -like 'Ariadne Resolver*' -and $p.QuietUninstallString){$r=$p;break}}};";
// Same manifest the Theseus updater reads (UPDATE_MANIFEST_URL). The copy on // 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: // 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" // it still listed Theseus 0.3.31 while dl had 0.3.44), so the Ariadne "Update"
@ -4153,39 +4159,45 @@ function ariadneDownloadInstaller(entry) {
} }
function ariadneQueryState() { function ariadneQueryState() {
return new Promise((resolve) => { const { spawn } = require("child_process");
const { spawn } = require("child_process"); // One shell round-trip for both bits of info:
// One shell round-trip for both bits of info: // - task states for BNS Resolver Daemon + BNS Sia Bridge
// - task states for BNS Resolver Daemon + BNS Sia Bridge // - installed version + quiet-uninstall string from Inno's registry entry
// - installed version + quiet-uninstall string from Inno's registry key // Task state comes from the Task Scheduler COM object, not Get-ScheduledTask:
// that cmdlet imports the ScheduledTasks module, which took 810 s cold on the
// 0.3.48 install and left the panel on "checking…" with every button hidden
// for that long (user report 2026-09-16). The COM state is a number, so it
// is also locale-independent (schtasks.exe prints localized status words).
// The manifest fetch used to run after this shell finished; it now runs in
// parallel.
const shell = new Promise((resolve) => {
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
"$names=@('BNS Resolver Daemon','BNS Sia Bridge');" + "$svc=New-Object -ComObject Schedule.Service;$svc.Connect();$f=$svc.GetFolder('\\');" +
"$names | ForEach-Object { $t = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue;" + "foreach($n in 'BNS Resolver Daemon','BNS Sia Bridge'){try{$t=$f.GetTask($n);\"$n=$($t.State)\"}catch{\"$n=MISSING\"}};" +
" if ($t) { \"$_=$($t.State)\" } else { \"$_=MISSING\" } };" + ARIADNE_REG_LOOKUP +
"$keys=@(" + ARIADNE_UNINSTALL_KEYS.map((k) => "'" + k + "'").join(",") + ");" + "if ($r) { \"__VER__=$($r.DisplayVersion)\"; \"__UNINSTALL__=$($r.QuietUninstallString)\" }"
"foreach ($k in $keys) { if (Test-Path $k) { $r=Get-ItemProperty $k; \"__VER__=$($r.DisplayVersion)\"; \"__UNINSTALL__=$($r.QuietUninstallString)\"; break } }"
], { windowsHide: true }); ], { windowsHide: true });
let out = ""; let out = "";
ps.stdout.on("data", (d) => { out += d; }); ps.stdout.on("data", (d) => { out += d; });
ps.on("close", async () => { ps.on("close", () => resolve(out));
const lines = out.trim().split(/\r?\n/).filter(Boolean); ps.on("error", () => resolve(""));
const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; })); });
const primary = map["BNS Resolver Daemon"]; return Promise.all([shell, ariadneManifestFetch()]).then(([out, latest]) => {
const installedVersion = map.__VER__ || null; const lines = out.trim().split(/\r?\n/).filter(Boolean);
const quietUninstall = map.__UNINSTALL__ || null; const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; }));
const latest = await ariadneManifestFetch(); const primary = map["BNS Resolver Daemon"]; // TASK_STATE: 1 disabled, 2 queued, 3 ready, 4 running
const latestVersion = latest ? latest.version : null; const installedVersion = map.__VER__ || null;
const canUpdate = !!(installedVersion && latestVersion && cmpVersions(latestVersion, installedVersion) > 0); const quietUninstall = map.__UNINSTALL__ || null;
let state; const latestVersion = latest ? latest.version : null;
if (!primary || primary === "MISSING") state = installedVersion ? "stopped" : "not-installed"; const canUpdate = !!(installedVersion && latestVersion && cmpVersions(latestVersion, installedVersion) > 0);
else if (primary === "Running") state = "running"; let state;
else state = "stopped"; if (!primary || primary === "MISSING") state = installedVersion ? "stopped" : "not-installed";
// The "bundledVersion" field name is kept for renderer compatibility -- else if (primary === "4" || primary === "Running") state = "running";
// it now carries the latest version advertised by silentmode.st's else state = "stopped";
// releases manifest, not a version physically bundled with Theseus. // The "bundledVersion" field name is kept for renderer compatibility --
resolve({ state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall }); // it now carries the latest version advertised by silentmode.st's
}); // releases manifest, not a version physically bundled with Theseus.
ps.on("error", () => resolve({ state: "not-installed", installedVersion: null, bundledVersion: null, canUpdate: false, hasUninstaller: false })); return { state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall };
}); });
} }
function cmpVersions(a, b) { function cmpVersions(a, b) {
@ -4263,7 +4275,7 @@ function ariadneUninstall() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Read the uninstall string fresh — a stale cache could point at a moved // Read the uninstall string fresh — a stale cache could point at a moved
// file. Extract the path (may be quoted) + any trailing args. // file. Extract the path (may be quoted) + any trailing args.
const cmd = `$keys=@(${ARIADNE_UNINSTALL_KEYS.map((k) => "'" + k + "'").join(",")});foreach ($k in $keys){if(Test-Path $k){$r=Get-ItemProperty $k;Write-Host $r.QuietUninstallString;break}}`; const cmd = ARIADNE_REG_LOOKUP + "if($r){Write-Host $r.QuietUninstallString}";
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", cmd], { windowsHide: true }); const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", cmd], { windowsHide: true });
let out = ""; let out = "";
ps.stdout.on("data", (d) => { out += d; }); ps.stdout.on("data", (d) => { out += d; });