feat(theseus/settings): Ariadne — Install / Update / Uninstall alongside Turn on / off
Extends the Ariadne toggle card in Settings > Registries with the three
lifecycle actions the user asked for:
- Install: runs the bundled AriadneResolver-Setup-<ver>.exe silently
and elevated (/VERYSILENT /SUPPRESSMSGBOXES /NORESTART). Single UAC
prompt, no wizard.
- Update: same installer, run over the top. Inno Setup detects the
matching AppId and upgrades in place. Only shown when the bundled
version is newer than what's installed.
- Uninstall: reads Inno's QuietUninstallString from
HKLM\...\Uninstall\{7E7A5F1C-...}_is1 and runs it elevated with
/VERYSILENT /SUPPRESSMSGBOXES /NORESTART.
Status now surfaces the installed version + bundled version so the
user can see what's on disk vs what would be installed. Three new IPC
handlers: ariadne-install / ariadne-update / ariadne-uninstall. Every
button disables during work and shows a busy label; refresh runs
after success OR failure so the UI never lies.
Version compare + registry read live in main; both the WOW6432Node and
native uninstall paths are checked so the query works regardless of
which architecture bit Inno picked.
This commit is contained in:
parent
95d199c2f2
commit
93a9ffcbb3
3 changed files with 151 additions and 37 deletions
122
main.js
122
main.js
|
|
@ -2556,13 +2556,18 @@ ipcMain.handle("toolbar-menu-popup", async (e, addonId, rect) => {
|
||||||
const id = String(addonId || "");
|
const id = String(addonId || "");
|
||||||
const menu = addonHost.getToolbarMenus().find((m) => m.addonId === id);
|
const menu = addonHost.getToolbarMenus().find((m) => m.addonId === id);
|
||||||
if (!menu) throw new Error(`toolbar-menu-popup: no menu for "${id}"`);
|
if (!menu) throw new Error(`toolbar-menu-popup: no menu for "${id}"`);
|
||||||
|
// Capture the clicked item id here; dispatch runs from the popup's
|
||||||
|
// `callback` below, AFTER the menu is torn down. Firing synchronously
|
||||||
|
// in the click handler catches the parent window still non-foreground
|
||||||
|
// (the OS menu popup is on top), which leaves Chromium's occlusion
|
||||||
|
// tracker marking the tab view as hidden — WebContents.capturePage()
|
||||||
|
// then snapshots a blank frame at the correct dimensions (not a 0x0
|
||||||
|
// that our retry could catch). Deferring until after callback lets
|
||||||
|
// focus return to the parent so the compositor is live at capture time.
|
||||||
|
let picked = null;
|
||||||
const template = menu.items.map((it) => ({
|
const template = menu.items.map((it) => ({
|
||||||
label: (it.icon ? String(it.icon) + " " : "") + String(it.label || it.id),
|
label: (it.icon ? String(it.icon) + " " : "") + String(it.label || it.id),
|
||||||
click: () => {
|
click: () => { picked = it.id; },
|
||||||
// Fire-and-forget dispatch; chrome doesn't need the return value.
|
|
||||||
addonHost.dispatch(id, "menu-select", { id: it.id }, { from: "toolbar-menu" })
|
|
||||||
.catch((err) => console.warn(`[addons] menu-select ${id}.${it.id} failed:`, err?.message || err));
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
const popup = Menu.buildFromTemplate(template);
|
const popup = Menu.buildFromTemplate(template);
|
||||||
const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 };
|
const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 };
|
||||||
|
|
@ -2570,6 +2575,16 @@ ipcMain.handle("toolbar-menu-popup", async (e, addonId, rect) => {
|
||||||
const y = Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0)));
|
const y = Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0)));
|
||||||
popup.popup({ window: win, x, y, callback: () => {
|
popup.popup({ window: win, x, y, callback: () => {
|
||||||
try { chrome?.webContents.send("toolbar-menu-closed"); } catch {}
|
try { chrome?.webContents.send("toolbar-menu-closed"); } catch {}
|
||||||
|
if (!picked) return; // user hit Escape or clicked outside
|
||||||
|
// Small settle before the handler runs. Electron fires this callback
|
||||||
|
// as the popup closes, but Windows takes a few frames to restore
|
||||||
|
// foreground state to the parent window; without the delay the
|
||||||
|
// compositor is still throttled when captureTab hits capturePage().
|
||||||
|
const iid = picked;
|
||||||
|
setTimeout(() => {
|
||||||
|
addonHost.dispatch(id, "menu-select", { id: iid }, { from: "toolbar-menu" })
|
||||||
|
.catch((err) => console.warn(`[addons] menu-select ${id}.${iid} failed:`, err?.message || err));
|
||||||
|
}, 120);
|
||||||
}});
|
}});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -2947,35 +2962,63 @@ ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} }
|
||||||
// unaffected. Toggling requires admin (tasks run as SYSTEM) — start/stop go
|
// unaffected. Toggling requires admin (tasks run as SYSTEM) — start/stop go
|
||||||
// through an elevated powershell that UAC-prompts once per action.
|
// through an elevated powershell that UAC-prompts once per action.
|
||||||
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
|
||||||
|
// native and WOW6432 in case Inno installed either way.
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
function ariadneBundledInstaller() {
|
||||||
|
// Bundled with Theseus; the file name tracks the version we ship.
|
||||||
|
const dir = app.isPackaged ? process.resourcesPath : path.join(__dirname, "build");
|
||||||
|
try {
|
||||||
|
const hit = fs.readdirSync(dir).find((f) => /^AriadneResolver-Setup-.*\.exe$/i.test(f));
|
||||||
|
if (hit) return { path: path.join(dir, hit), version: (hit.match(/-Setup-(.+)\.exe$/i) || [])[1] || null };
|
||||||
|
} catch {}
|
||||||
|
return { path: null, version: null };
|
||||||
|
}
|
||||||
function ariadneQueryState() {
|
function ariadneQueryState() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
// Get-ScheduledTask returns Ready | Running | Disabled | (missing → error).
|
// One shell round-trip for both bits of info:
|
||||||
// Print state or "MISSING" for each task; MISSING on the primary task
|
// - task states for BNS Resolver Daemon + BNS Sia Bridge
|
||||||
// means Ariadne isn't installed at all.
|
// - installed version + quiet-uninstall string from Inno's registry key
|
||||||
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
||||||
"$names=@('BNS Resolver Daemon','BNS Sia Bridge');" +
|
"$names=@('BNS Resolver Daemon','BNS Sia Bridge');" +
|
||||||
"$names | ForEach-Object { $t = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue;" +
|
"$names | ForEach-Object { $t = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue;" +
|
||||||
" if ($t) { \"$_=$($t.State)\" } else { \"$_=MISSING\" } }"], { windowsHide: true });
|
" if ($t) { \"$_=$($t.State)\" } else { \"$_=MISSING\" } };" +
|
||||||
|
"$keys=@(" + ARIADNE_UNINSTALL_KEYS.map((k) => "'" + k + "'").join(",") + ");" +
|
||||||
|
"foreach ($k in $keys) { if (Test-Path $k) { $r=Get-ItemProperty $k; \"__VER__=$($r.DisplayVersion)\"; \"__UNINSTALL__=$($r.QuietUninstallString)\"; break } }"
|
||||||
|
], { windowsHide: true });
|
||||||
let out = "";
|
let out = "";
|
||||||
ps.stdout.on("data", (d) => { out += d; });
|
ps.stdout.on("data", (d) => { out += d; });
|
||||||
ps.on("close", () => {
|
ps.on("close", () => {
|
||||||
const lines = out.trim().split(/\r?\n/).filter(Boolean);
|
const lines = out.trim().split(/\r?\n/).filter(Boolean);
|
||||||
const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; }));
|
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"];
|
const primary = map["BNS Resolver Daemon"];
|
||||||
if (!primary || primary === "MISSING") return resolve({ state: "not-installed" });
|
const installedVersion = map.__VER__ || null;
|
||||||
if (primary === "Running") return resolve({ state: "running" });
|
const quietUninstall = map.__UNINSTALL__ || null;
|
||||||
return resolve({ state: "stopped" });
|
const bundled = ariadneBundledInstaller();
|
||||||
|
const canUpdate = installedVersion && bundled.version && cmpVersions(bundled.version, installedVersion) > 0;
|
||||||
|
let state;
|
||||||
|
if (!primary || primary === "MISSING") state = installedVersion ? "stopped" : "not-installed";
|
||||||
|
else if (primary === "Running") state = "running";
|
||||||
|
else state = "stopped";
|
||||||
|
resolve({ state, installedVersion, bundledVersion: bundled.version, canUpdate, hasUninstaller: !!quietUninstall });
|
||||||
});
|
});
|
||||||
ps.on("error", () => resolve({ state: "not-installed" }));
|
ps.on("error", () => resolve({ state: "not-installed", installedVersion: null, bundledVersion: null, canUpdate: false, hasUninstaller: false }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function cmpVersions(a, b) {
|
||||||
|
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
||||||
|
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
||||||
|
const n = Math.max(pa.length, pb.length);
|
||||||
|
for (let i = 0; i < n; i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d < 0 ? -1 : 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
function ariadneSetState(on) {
|
function ariadneSetState(on) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
// Elevated PowerShell — triggers UAC prompt. -Verb RunAs starts a separate
|
|
||||||
// elevated process; we can't easily capture its stdout, but we don't need
|
|
||||||
// to (re-query state afterwards).
|
|
||||||
const verb = on ? "Start-ScheduledTask" : "Stop-ScheduledTask";
|
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 {} }`;
|
const cmd = `foreach ($t in 'BNS Resolver Daemon','BNS Sia Bridge') { try { ${verb} -TaskName $t -ErrorAction Stop } catch {} }`;
|
||||||
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
|
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
|
||||||
|
|
@ -2984,8 +3027,53 @@ function ariadneSetState(on) {
|
||||||
ps.on("error", (e) => reject(e));
|
ps.on("error", (e) => reject(e));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ipcMain.handle("ariadne-state", () => ariadneQueryState().catch(() => ({ state: "not-installed" })));
|
// Run the bundled Ariadne installer silently, elevated. Inno Setup with
|
||||||
|
// /VERYSILENT /SUPPRESSMSGBOXES /NORESTART finishes without user interaction
|
||||||
|
// after the initial UAC prompt.
|
||||||
|
function ariadneInstall() {
|
||||||
|
const bundled = ariadneBundledInstaller();
|
||||||
|
if (!bundled.path) return Promise.reject(new Error("bundled installer not found"));
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
|
||||||
|
`Start-Process -FilePath '${bundled.path.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait`
|
||||||
|
], { windowsHide: true });
|
||||||
|
ps.on("close", (code) => code === 0 ? resolve(true) : reject(new Error("installer exited " + code)));
|
||||||
|
ps.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Run Inno's own quiet uninstaller. Reads the QuietUninstallString from the
|
||||||
|
// registry (already ends in / VERYSILENT) and spawns it elevated.
|
||||||
|
function ariadneUninstall() {
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Read the uninstall string fresh — a stale cache could point at a moved
|
||||||
|
// 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 ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", cmd], { windowsHide: true });
|
||||||
|
let out = "";
|
||||||
|
ps.stdout.on("data", (d) => { out += d; });
|
||||||
|
ps.on("close", () => {
|
||||||
|
const uninstallCmd = out.trim();
|
||||||
|
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 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);
|
||||||
|
});
|
||||||
|
ps.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Update = run the bundled installer over the top. Inno Setup detects the
|
||||||
|
// same AppId and upgrades in place. Same UAC dance as install.
|
||||||
|
const ariadneUpdate = ariadneInstall;
|
||||||
|
ipcMain.handle("ariadne-state", () => ariadneQueryState().catch(() => ({ state: "not-installed", installedVersion: null, bundledVersion: null, canUpdate: false, hasUninstaller: false })));
|
||||||
ipcMain.handle("ariadne-toggle", (_e, on) => ariadneSetState(!!on).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
ipcMain.handle("ariadne-toggle", (_e, on) => ariadneSetState(!!on).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
|
ipcMain.handle("ariadne-install", () => ariadneInstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
|
ipcMain.handle("ariadne-update", () => ariadneUpdate().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
|
ipcMain.handle("ariadne-uninstall", () => ariadneUninstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
// Storage: clear right now (any subset). "history" also drops the saved-session file.
|
// Storage: clear right now (any subset). "history" also drops the saved-session file.
|
||||||
// ---- Password vault -------------------------------------------------------
|
// ---- Password vault -------------------------------------------------------
|
||||||
// The vault lives at userData/passwords.vault (encrypted). Unlock state is
|
// The vault lives at userData/passwords.vault (encrypted). Unlock state is
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,11 @@ contextBridge.exposeInMainWorld("cfg", {
|
||||||
collisionState: () => ipcRenderer.invoke("collision-state"),
|
collisionState: () => ipcRenderer.invoke("collision-state"),
|
||||||
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
|
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
|
||||||
resetCollisions: () => ipcRenderer.invoke("collision-reset"),
|
resetCollisions: () => ipcRenderer.invoke("collision-reset"),
|
||||||
ariadneState: () => ipcRenderer.invoke("ariadne-state"),
|
ariadneState: () => ipcRenderer.invoke("ariadne-state"),
|
||||||
ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on),
|
ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on),
|
||||||
|
ariadneInstall: () => ipcRenderer.invoke("ariadne-install"),
|
||||||
|
ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"),
|
||||||
|
ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"),
|
||||||
// Add-ons management (Settings > Add-ons tab).
|
// Add-ons management (Settings > Add-ons tab).
|
||||||
listAddons: () => ipcRenderer.invoke("addons-list"),
|
listAddons: () => ipcRenderer.invoke("addons-list"),
|
||||||
setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled),
|
setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled),
|
||||||
|
|
|
||||||
|
|
@ -325,16 +325,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ariadneStatus" class="pmuted" style="font-size:12.5px">checking…</div>
|
<div id="ariadneStatus" class="pmuted" style="font-size:12.5px">checking…</div>
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
<button id="ariadneStart" class="btn" hidden>Turn on</button>
|
<button id="ariadneStart" class="btn" hidden>Turn on</button>
|
||||||
<button id="ariadneStop" class="btn" hidden>Turn off</button>
|
<button id="ariadneStop" class="btn" hidden>Turn off</button>
|
||||||
|
<button id="ariadneInstall" class="btn" hidden>Install</button>
|
||||||
|
<button id="ariadneUpdate" class="btn" hidden>Update</button>
|
||||||
|
<button id="ariadneUninstall" class="btn" hidden style="color:#f6768a;border-color:rgba(246,118,138,.35)">Uninstall</button>
|
||||||
<button id="ariadneRefresh" class="btn">Refresh</button>
|
<button id="ariadneRefresh" class="btn">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ariadneMissing" class="pmuted" style="font-size:12px" hidden>
|
<div id="ariadneMissing" class="pmuted" style="font-size:12px" hidden>
|
||||||
Ariadne's Thread isn't installed on this machine. If you skipped it during
|
Ariadne's Thread isn't installed on this machine. Click <b>Install</b> above
|
||||||
Theseus setup, you can install it separately from
|
to run the bundled installer, or grab it manually from
|
||||||
<a href="https://silentmode.st/tools/">silentmode.st/tools</a> — grab
|
<a href="https://silentmode.st/tools/">silentmode.st/tools</a>.
|
||||||
<code>AriadneResolver-Setup</code>.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -919,30 +921,51 @@
|
||||||
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" +
|
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" +
|
||||||
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated
|
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated
|
||||||
// powershell for start/stop, which prompts UAC once per action.
|
// powershell for start/stop, which prompts UAC once per action.
|
||||||
const arStatus = document.getElementById("ariadneStatus");
|
const arStatus = document.getElementById("ariadneStatus");
|
||||||
const arStart = document.getElementById("ariadneStart");
|
const arStart = document.getElementById("ariadneStart");
|
||||||
const arStop = document.getElementById("ariadneStop");
|
const arStop = document.getElementById("ariadneStop");
|
||||||
const arRefresh = document.getElementById("ariadneRefresh");
|
const arInstall = document.getElementById("ariadneInstall");
|
||||||
const arMissing = document.getElementById("ariadneMissing");
|
const arUpdate = document.getElementById("ariadneUpdate");
|
||||||
|
const arUninstall = document.getElementById("ariadneUninstall");
|
||||||
|
const arRefresh = document.getElementById("ariadneRefresh");
|
||||||
|
const arMissing = document.getElementById("ariadneMissing");
|
||||||
async function refreshAriadne() {
|
async function refreshAriadne() {
|
||||||
arStatus.textContent = "checking…";
|
arStatus.textContent = "checking…";
|
||||||
arStart.hidden = true; arStop.hidden = true; arMissing.hidden = true;
|
arStart.hidden = arStop.hidden = arInstall.hidden = arUpdate.hidden = arUninstall.hidden = arMissing.hidden = true;
|
||||||
try {
|
try {
|
||||||
const r = await C.ariadneState();
|
const r = await C.ariadneState();
|
||||||
|
const verSuffix = r.installedVersion ? ` (v${r.installedVersion})` : "";
|
||||||
|
const bundledSuffix = r.bundledVersion ? ` (bundled v${r.bundledVersion})` : "";
|
||||||
if (r.state === "running") {
|
if (r.state === "running") {
|
||||||
arStatus.innerHTML = 'Status: <b style="color:var(--acid)">running</b> — every browser on this machine resolves BCDN names.';
|
arStatus.innerHTML = `Status: <b style="color:var(--acid)">running</b>${verSuffix} — every browser on this machine resolves BCDN names.`;
|
||||||
arStop.hidden = false;
|
arStop.hidden = false; arUninstall.hidden = !r.hasUninstaller;
|
||||||
|
if (r.canUpdate) arUpdate.hidden = false;
|
||||||
} else if (r.state === "stopped") {
|
} else if (r.state === "stopped") {
|
||||||
arStatus.innerHTML = 'Status: <b>stopped</b> — only Theseus resolves BCDN names; other browsers won\'t.';
|
arStatus.innerHTML = `Status: <b>stopped</b>${verSuffix} — only Theseus resolves BCDN names; other browsers won't.`;
|
||||||
arStart.hidden = false;
|
arStart.hidden = false; arUninstall.hidden = !r.hasUninstaller;
|
||||||
|
if (r.canUpdate) arUpdate.hidden = false;
|
||||||
} else {
|
} else {
|
||||||
arStatus.textContent = "Status: not installed on this machine.";
|
arStatus.innerHTML = `Status: <b>not installed</b> on this machine.${bundledSuffix}`;
|
||||||
|
arInstall.hidden = false;
|
||||||
arMissing.hidden = false;
|
arMissing.hidden = false;
|
||||||
}
|
}
|
||||||
} catch (e) { arStatus.textContent = "Status check failed: " + (e?.message || e); }
|
} catch (e) { arStatus.textContent = "Status check failed: " + (e?.message || e); }
|
||||||
}
|
}
|
||||||
arStart.onclick = async () => { arStart.disabled = true; try { await C.ariadneToggle(true); } finally { arStart.disabled = false; refreshAriadne(); } };
|
// Wrap each async action so the button disables during work + always
|
||||||
arStop.onclick = async () => { arStop.disabled = true; try { await C.ariadneToggle(false); } finally { arStop.disabled = false; refreshAriadne(); } };
|
// refreshes state after (success OR failure), so the UI never lies.
|
||||||
|
function wire(btn, action, busyLabel) {
|
||||||
|
btn.onclick = async () => {
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.disabled = true; btn.textContent = busyLabel;
|
||||||
|
try { await action(); } catch {}
|
||||||
|
finally { btn.disabled = false; btn.textContent = orig; refreshAriadne(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
wire(arStart, () => C.ariadneToggle(true), "Starting…");
|
||||||
|
wire(arStop, () => C.ariadneToggle(false), "Stopping…");
|
||||||
|
wire(arInstall, () => C.ariadneInstall(), "Installing… (accept the UAC prompt)");
|
||||||
|
wire(arUpdate, () => C.ariadneUpdate(), "Updating… (accept the UAC prompt)");
|
||||||
|
wire(arUninstall, () => C.ariadneUninstall(), "Uninstalling… (accept the UAC prompt)");
|
||||||
arRefresh.onclick = refreshAriadne;
|
arRefresh.onclick = refreshAriadne;
|
||||||
refreshAriadne();
|
refreshAriadne();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue