relay-companion 0.1.577 → 0.1.578
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -33,7 +33,10 @@ async function pauseRecoveryChecks({ homeDir, timeoutMs = 3 * 60_000, pollMs = 2
|
|
|
33
33
|
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
|
|
34
34
|
const intent = require("./recovery-intent.cjs");
|
|
35
35
|
const wasStopped = intent.stopped(homeDir);
|
|
36
|
-
|
|
36
|
+
// Recorded as held by this setup, so Relay's own setup and pill launch during
|
|
37
|
+
// activation leave it paused; a dead holder no longer counts.
|
|
38
|
+
const hold = () => intent.setStopped(true, homeDir, { heldBy: "setup", pid: process.pid });
|
|
39
|
+
hold();
|
|
37
40
|
const runLock = path.join(homeDir, ".relay", "recovery", "run.lock");
|
|
38
41
|
for (const deadline = Date.now() + timeoutMs; ;) {
|
|
39
42
|
try { bootstrap.acquireCanonicalLock(runLock).release(); break; }
|
|
@@ -44,7 +47,7 @@ async function pauseRecoveryChecks({ homeDir, timeoutMs = 3 * 60_000, pollMs = 2
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
return {
|
|
47
|
-
repause:
|
|
50
|
+
repause: hold,
|
|
48
51
|
resume: succeeded => intent.setStopped(succeeded ? false : wasStopped, homeDir),
|
|
49
52
|
};
|
|
50
53
|
}
|
|
@@ -107,6 +110,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
107
110
|
homeDir = os.homedir(), verify = verifyBundle, extract = extractBundle, activate = bootstrap.activateRuntime,
|
|
108
111
|
acquireLock = bootstrap.acquireCanonicalLock, health = require("./runtime-health.cjs").exactRuntimeHealth,
|
|
109
112
|
recover = recovery.recoverWithCandidate, pauseRecovery = pauseRecoveryChecks,
|
|
113
|
+
hidePill = require("./previous-pill.cjs").hidePreviousPill,
|
|
110
114
|
drain = require("./update-activity.cjs").drainCalls, download = bootstrap.downloadVerifiedArtifact,
|
|
111
115
|
onProgress = () => {}, signal } = {}) {
|
|
112
116
|
onProgress({ phase: "verifying", canCancel: false });
|
|
@@ -128,7 +132,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
128
132
|
const journalPath = path.join(homeDir, ".relay", "application-migration.json");
|
|
129
133
|
let releaseDrain;
|
|
130
134
|
let downloadDirectory;
|
|
131
|
-
let pausedRecovery, succeeded = false;
|
|
135
|
+
let pausedRecovery, previousPill, succeeded = false;
|
|
132
136
|
try {
|
|
133
137
|
const recordedCurrent = recovery.marker(path.join(runtimeRoot, "current.json"));
|
|
134
138
|
const removed = recovery.marker(path.join(homeDir, ".relay", "application-uninstall.json"));
|
|
@@ -199,6 +203,8 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
199
203
|
return { ok: true, applicationOnly: true, owner, runtime: current, updateOwner: "canonical-runtime" };
|
|
200
204
|
}
|
|
201
205
|
pausedRecovery = await pauseRecovery({ homeDir });
|
|
206
|
+
// Only the old pill goes now; its background service runs until the switch.
|
|
207
|
+
if (current?.active === true) previousPill = hidePill({ current });
|
|
202
208
|
if (bundle.receipt.runtimeDelivery === "download") {
|
|
203
209
|
signal?.throwIfAborted();
|
|
204
210
|
// Stage beside releases: tar requires archive and destination on one volume.
|
|
@@ -275,6 +281,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
275
281
|
try { releaseDrain?.(); }
|
|
276
282
|
finally {
|
|
277
283
|
try { pausedRecovery?.resume(succeeded); } catch { /* recovery reads a missing intent as running */ }
|
|
284
|
+
if (!succeeded) try { previousPill?.restore(); } catch { /* recovery restores a missing pill */ }
|
|
278
285
|
lock.release();
|
|
279
286
|
if (downloadDirectory) fs.rmSync(downloadDirectory, { recursive: true, force: true });
|
|
280
287
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The installer's setup leaves the existing Relay running while it downloads,
|
|
3
|
+
// so a failed download still leaves a working Relay. Its pill, though, stayed
|
|
4
|
+
// on screen beside the setup window looking active (Shane, 2026-10-02). Hide
|
|
5
|
+
// only the old pill when setup starts; the background service keeps running
|
|
6
|
+
// until activation switches over and starts the new pill. If setup fails, the
|
|
7
|
+
// previous Relay opens its pill again.
|
|
8
|
+
const { spawnSync, spawn } = require("node:child_process");
|
|
9
|
+
|
|
10
|
+
const MAC_PILL_LABEL = "work.relay.companion.pill";
|
|
11
|
+
// Every stock pill runs relay-companion's overlay/main.cjs under Electron.
|
|
12
|
+
const WINDOWS_STOP_PILL_PS = [
|
|
13
|
+
"$p=Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {",
|
|
14
|
+
" $_.CommandLine -and ($_.CommandLine -match '[\\\\/]relay-companion[\\\\/]') -and ($_.CommandLine -match '[\\\\/]overlay[\\\\/]main')",
|
|
15
|
+
"}; foreach($x in $p){ try { Invoke-CimMethod -InputObject $x -MethodName Terminate -ErrorAction Stop | Out-Null } catch {} }",
|
|
16
|
+
].join(" ");
|
|
17
|
+
|
|
18
|
+
function hidePreviousPill({ current, platform = process.platform, uid = process.getuid?.(), run = spawnSync, launch = spawn } = {}) {
|
|
19
|
+
let hidden = false;
|
|
20
|
+
try {
|
|
21
|
+
if (platform === "win32") {
|
|
22
|
+
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_STOP_PILL_PS], { windowsHide: true, timeout: 60_000 });
|
|
23
|
+
hidden = !result?.error && result?.status === 0;
|
|
24
|
+
} else if (platform === "darwin") {
|
|
25
|
+
// launchd keeps the pill alive, so stop its job rather than its process.
|
|
26
|
+
const result = run("/bin/launchctl", ["bootout", `gui/${uid}/${MAC_PILL_LABEL}`], { timeout: 30_000 });
|
|
27
|
+
hidden = !result?.error && result?.status === 0;
|
|
28
|
+
} else {
|
|
29
|
+
const result = run("pkill", ["-u", String(uid), "-f", "relay-companion/overlay/main"], { timeout: 30_000 });
|
|
30
|
+
hidden = !result?.error && result?.status === 0;
|
|
31
|
+
}
|
|
32
|
+
} catch { hidden = false; }
|
|
33
|
+
return {
|
|
34
|
+
hidden,
|
|
35
|
+
// Only an active previous runtime can reopen its own pill.
|
|
36
|
+
restore() {
|
|
37
|
+
if (!hidden || current?.active !== true || typeof current.node !== "string" || typeof current.bin !== "string") return false;
|
|
38
|
+
try {
|
|
39
|
+
const child = launch(current.node, [current.bin, "pill"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
40
|
+
child.unref?.();
|
|
41
|
+
return true;
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { hidePreviousPill, WINDOWS_STOP_PILL_PS, MAC_PILL_LABEL };
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const fs = require("node:fs"), os = require("node:os"), path = require("node:path");
|
|
3
3
|
const file = homeDir => path.join(homeDir, ".relay", "recovery", "intent.json");
|
|
4
|
+
function read(homeDir) {
|
|
5
|
+
try { return { value: JSON.parse(fs.readFileSync(file(homeDir), "utf8")) }; }
|
|
6
|
+
catch (error) { return { missing: error.code === "ENOENT" }; }
|
|
7
|
+
}
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === "EPERM"; }
|
|
10
|
+
}
|
|
11
|
+
// The installer's setup pauses recovery while it replaces Relay and records
|
|
12
|
+
// itself as the holder. A hold whose setup process is gone no longer counts,
|
|
13
|
+
// so a killed setup can never leave recovery paused for good.
|
|
14
|
+
function heldBySetup(value) {
|
|
15
|
+
return value?.stopped === true && value.heldBy === "setup" && Number.isInteger(value.pid) && alive(value.pid);
|
|
16
|
+
}
|
|
4
17
|
function stopped(homeDir = os.homedir()) {
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
const { value, missing } = read(homeDir);
|
|
19
|
+
if (!value) return !missing;
|
|
20
|
+
if (value.stopped === true && value.heldBy === "setup") return heldBySetup(value);
|
|
21
|
+
return value.stopped === true;
|
|
22
|
+
}
|
|
23
|
+
function setStopped(value, homeDir = os.homedir(), { heldBy, pid } = {}) {
|
|
24
|
+
require("./recovery-launcher.cjs").write(file(homeDir), { schema: 1, stopped: value, at: Date.now(), ...(value && heldBy ? { heldBy, pid } : {}) });
|
|
7
25
|
}
|
|
8
|
-
|
|
9
|
-
|
|
26
|
+
// Starting Relay (setup, the pill) resumes recovery, except while an
|
|
27
|
+
// installer's setup holds the pause; that setup lifts it when it ends.
|
|
28
|
+
function resumeUnlessHeld(homeDir = os.homedir()) {
|
|
29
|
+
if (heldBySetup(read(homeDir).value)) return false;
|
|
30
|
+
setStopped(false, homeDir);
|
|
31
|
+
return true;
|
|
10
32
|
}
|
|
11
|
-
module.exports = { stopped, setStopped };
|
|
33
|
+
module.exports = { stopped, setStopped, resumeUnlessHeld };
|
package/package.json
CHANGED