@pushary/agent-hooks 0.89.8 → 0.89.9
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.
- package/CHANGELOG.md +13 -0
- package/dist/bin/pushary-claude.js +1 -1
- package/dist/bin/pushary-clean.js +2 -2
- package/dist/bin/pushary-daemon.js +44 -9
- package/dist/bin/pushary-doctor.js +26 -5
- package/dist/bin/pushary-setup.js +16 -3
- package/dist/bin/pushary-status.js +1 -1
- package/dist/bin/pushary-upgrade.js +12 -4
- package/dist/{chunk-R2AGKCXL.js → chunk-543KNJKC.js} +49 -19
- package/dist/{chunk-ZL7HEF2T.js → chunk-U5TQCQ25.js} +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.89.9
|
|
4
|
+
|
|
5
|
+
### Setup proves managed control reaches the server
|
|
6
|
+
|
|
7
|
+
The background daemon now records successful heartbeats separately from
|
|
8
|
+
successful control-queue responses. Setup and upgrade wait for both before
|
|
9
|
+
reporting managed control ready, and doctor reports either missing proof.
|
|
10
|
+
|
|
11
|
+
An authenticated, once-per-minute challenge round trip gives the server the
|
|
12
|
+
same readiness evidence without launching an agent or changing queued work.
|
|
13
|
+
Machine status exposes that observation additively so current clients keep
|
|
14
|
+
their existing fields while newer CLIs can verify capabilities end to end.
|
|
15
|
+
|
|
3
16
|
## 0.89.8
|
|
4
17
|
|
|
5
18
|
### Daemon upgrades keep a recoverable service
|
|
@@ -24,7 +24,7 @@ import "../chunk-ATBKJNWS.js";
|
|
|
24
24
|
import "../chunk-AIGDBRIJ.js";
|
|
25
25
|
import {
|
|
26
26
|
disableDaemonService
|
|
27
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-543KNJKC.js";
|
|
28
28
|
import "../chunk-XHKBHWLX.js";
|
|
29
29
|
import {
|
|
30
30
|
execNpm
|
|
@@ -177,7 +177,7 @@ var guardedDaemonRemoval = async () => {
|
|
|
177
177
|
wouldNote("stop and uninstall", "Pushary background service");
|
|
178
178
|
return true;
|
|
179
179
|
}
|
|
180
|
-
return (await disableDaemonService()).kind === "ok";
|
|
180
|
+
return (await disableDaemonService({ requireAbsenceProof: true })).kind === "ok";
|
|
181
181
|
};
|
|
182
182
|
var guardedInstructionBlockRemoval = (path) => {
|
|
183
183
|
if (!dryRun) return removeInstructionBlock(path);
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
inspectConfiguredManagedProviders
|
|
4
4
|
} from "../chunk-EA3E74P2.js";
|
|
5
5
|
import "../chunk-J2JZJZKB.js";
|
|
6
|
+
import "../chunk-BAE45YLP.js";
|
|
6
7
|
import {
|
|
7
8
|
HEARTBEAT_INTERVAL_MS,
|
|
8
9
|
sendHeartbeat
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-BAE45YLP.js";
|
|
10
|
+
} from "../chunk-U5TQCQ25.js";
|
|
11
11
|
import "../chunk-VA7VUNRD.js";
|
|
12
12
|
import "../chunk-ATBKJNWS.js";
|
|
13
13
|
import "../chunk-AIGDBRIJ.js";
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
replaceDaemonService,
|
|
24
24
|
requestDaemonHandoff,
|
|
25
25
|
stopDaemonRunning
|
|
26
|
-
} from "../chunk-
|
|
26
|
+
} from "../chunk-543KNJKC.js";
|
|
27
27
|
import {
|
|
28
28
|
detectProviderLiveCapabilities,
|
|
29
29
|
hasSpawnProvider,
|
|
@@ -116,8 +116,15 @@ var STOP_PROTOCOL_VERSION = 2;
|
|
|
116
116
|
var MAX_SPAWNS_PER_WINDOW = 5;
|
|
117
117
|
var SPAWN_WINDOW_MS = 6e4;
|
|
118
118
|
var VERSION_CHECK_INTERVAL_MS = 2e3;
|
|
119
|
+
var CONTROL_PROBE_INTERVAL_MS = 6e4;
|
|
119
120
|
var MAX_TRACKED_PROCESSES = 100;
|
|
120
121
|
var MAX_COMPLETED_STOPS = 20;
|
|
122
|
+
var controlProbeDue = (lastProbeAt, at = Date.now()) => lastProbeAt === 0 || at - lastProbeAt >= CONTROL_PROBE_INTERVAL_MS;
|
|
123
|
+
var parseControlProbe = (value) => {
|
|
124
|
+
if (typeof value !== "object" || value === null) return null;
|
|
125
|
+
const id = value.id;
|
|
126
|
+
return typeof id === "string" && id.length > 0 && id.length <= 128 ? id : null;
|
|
127
|
+
};
|
|
121
128
|
var daemonSignals = (platform = process.platform) => platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
122
129
|
var parseSpawnClaim = (value) => {
|
|
123
130
|
if (typeof value !== "object" || value === null) return null;
|
|
@@ -294,6 +301,7 @@ var runSpawnDaemon = async () => {
|
|
|
294
301
|
let stopped = false;
|
|
295
302
|
let idleTicks = 0;
|
|
296
303
|
let errorStreak = 0;
|
|
304
|
+
let lastControlProbeAt = 0;
|
|
297
305
|
const spawnTimestamps = [];
|
|
298
306
|
const launchedProcesses = /* @__PURE__ */ new Map();
|
|
299
307
|
const completedStops = /* @__PURE__ */ new Map();
|
|
@@ -432,18 +440,41 @@ var runSpawnDaemon = async () => {
|
|
|
432
440
|
});
|
|
433
441
|
}
|
|
434
442
|
};
|
|
443
|
+
const acknowledgeControlProbe = async (probeId) => {
|
|
444
|
+
const response = await fetch(`${baseUrl}${DRAIN_PATH}`, {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
447
|
+
body: JSON.stringify({ machineId, controlProbeAck: probeId }),
|
|
448
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
449
|
+
}).catch(() => null);
|
|
450
|
+
return response?.ok === true;
|
|
451
|
+
};
|
|
435
452
|
const drain = async (signal) => {
|
|
453
|
+
const controlProbe = controlProbeDue(lastControlProbeAt);
|
|
454
|
+
if (controlProbe) lastControlProbeAt = Date.now();
|
|
436
455
|
const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
|
|
437
456
|
method: "POST",
|
|
438
457
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
439
|
-
body: JSON.stringify({
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
machineId,
|
|
460
|
+
spawnProtocol: SPAWN_PROTOCOL_VERSION,
|
|
461
|
+
stopProtocol: STOP_PROTOCOL_VERSION,
|
|
462
|
+
controlProbe
|
|
463
|
+
}),
|
|
440
464
|
signal
|
|
441
465
|
});
|
|
442
466
|
if (!res.ok) throw new DrainError(res.status);
|
|
443
467
|
const data = await res.json();
|
|
444
468
|
const response = typeof data === "object" && data !== null ? data : {};
|
|
445
469
|
await Promise.all(parseStopRequests(response.stops).map(runStop));
|
|
446
|
-
|
|
470
|
+
const controlProbeId = parseControlProbe(response.controlProbe);
|
|
471
|
+
return {
|
|
472
|
+
request: parseSpawnClaim(response.spawn),
|
|
473
|
+
controlProbeId,
|
|
474
|
+
// Released servers ignore the request field and omit the response field.
|
|
475
|
+
// Their successful authenticated drain remains the strongest proof they expose.
|
|
476
|
+
legacyControlProof: controlProbe && !Object.hasOwn(response, "controlProbe")
|
|
477
|
+
};
|
|
447
478
|
};
|
|
448
479
|
const acknowledge = async (body) => {
|
|
449
480
|
if (!body) return;
|
|
@@ -507,18 +538,22 @@ var runSpawnDaemon = async () => {
|
|
|
507
538
|
inflight = controller;
|
|
508
539
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
509
540
|
try {
|
|
510
|
-
const
|
|
541
|
+
const drained = await drain(controller.signal);
|
|
511
542
|
if (!daemonLease.isOwner()) {
|
|
512
543
|
finish();
|
|
513
544
|
return;
|
|
514
545
|
}
|
|
515
546
|
errorStreak = 0;
|
|
516
|
-
if (
|
|
547
|
+
if (drained.request) {
|
|
517
548
|
idleTicks = 0;
|
|
518
|
-
await launchClaim(
|
|
549
|
+
await launchClaim(drained.request);
|
|
519
550
|
} else {
|
|
520
551
|
idleTicks++;
|
|
521
552
|
}
|
|
553
|
+
const controlProved = drained.legacyControlProof || drained.controlProbeId !== null && await acknowledgeControlProbe(drained.controlProbeId);
|
|
554
|
+
if (controlProved) {
|
|
555
|
+
daemonLease.recordSuccessfulControl();
|
|
556
|
+
}
|
|
522
557
|
} catch (err) {
|
|
523
558
|
if (isAuthFailure(err)) {
|
|
524
559
|
const status = err.status;
|
|
@@ -582,7 +617,7 @@ var runSpawnDaemon = async () => {
|
|
|
582
617
|
stderr(hasSpawnProvider(providerCapabilities) ? `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
|
|
583
618
|
` : `[pushary] daemon online \u2014 no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.
|
|
584
619
|
`);
|
|
585
|
-
timer = setTimeout(tick,
|
|
620
|
+
timer = setTimeout(tick, 0);
|
|
586
621
|
const signals = daemonSignals();
|
|
587
622
|
await new Promise((resolve) => {
|
|
588
623
|
const onSignal = () => finish();
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
geminiWired,
|
|
9
9
|
pusharyHookExecutable
|
|
10
10
|
} from "../chunk-BAE45YLP.js";
|
|
11
|
+
import {
|
|
12
|
+
readMachineReadiness
|
|
13
|
+
} from "../chunk-U5TQCQ25.js";
|
|
11
14
|
import {
|
|
12
15
|
agentProbes,
|
|
13
16
|
detectAgent,
|
|
@@ -31,10 +34,11 @@ import {
|
|
|
31
34
|
import "../chunk-ATBKJNWS.js";
|
|
32
35
|
import "../chunk-AIGDBRIJ.js";
|
|
33
36
|
import {
|
|
37
|
+
daemonHasRemoteProof,
|
|
34
38
|
daemonServiceManagerAvailable,
|
|
35
39
|
hasDaemonServiceDefinition,
|
|
36
40
|
readDaemonStatus
|
|
37
|
-
} from "../chunk-
|
|
41
|
+
} from "../chunk-543KNJKC.js";
|
|
38
42
|
import {
|
|
39
43
|
detectProviderLiveCapabilities,
|
|
40
44
|
hasSpawnProvider,
|
|
@@ -558,7 +562,8 @@ var main = async () => {
|
|
|
558
562
|
...codexInPlay ? ["codex"] : [],
|
|
559
563
|
...geminiInPlay ? ["gemini"] : []
|
|
560
564
|
];
|
|
561
|
-
const
|
|
565
|
+
const controlCapabilities = detectProviderLiveCapabilities(configuredManagedProviders);
|
|
566
|
+
const controlSupported = hasSpawnProvider(controlCapabilities);
|
|
562
567
|
const controlMode = readControlMode();
|
|
563
568
|
const daemon = readDaemonStatus();
|
|
564
569
|
const serviceDefined = hasDaemonServiceDefinition();
|
|
@@ -571,8 +576,8 @@ var main = async () => {
|
|
|
571
576
|
});
|
|
572
577
|
const controlRequired = controlPolicy === "enabled";
|
|
573
578
|
const controlUnsupported = controlMode === "on" && controlPolicy !== "enabled";
|
|
574
|
-
const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemon
|
|
575
|
-
const daemonFailure = daemonOk ? null : controlUnsupported ? "control is required but no usable provider/service-manager pair is available" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`;
|
|
579
|
+
const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemonHasRemoteProof(daemon) : controlPolicy === "legacy" ? false : daemon.kind === "absent" && !serviceDefined;
|
|
580
|
+
const daemonFailure = daemonOk ? null : controlUnsupported ? "control is required but no usable provider/service-manager pair is available" : controlPolicy === "enabled" && daemon.kind === "running" ? "the daemon has not completed an authenticated heartbeat and control poll" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`;
|
|
576
581
|
const daemonDetail = daemonFailure ? controlUnsupported ? `${daemonFailure}; re-run setup with \`--control off\`, or restore both requirements` : controlPolicy === "disabled" ? `${daemonFailure}; re-run setup with \`--control off\`` : controlPolicy === "legacy" ? `${daemonFailure}; run setup with \`--control off\`, or restore both requirements` : `${daemonFailure}; run \`pushary daemon install\` to install or repair the background service` : controlPolicy === "disabled" ? "disabled by setup" : controlPolicy === "preserve-absent" ? "legacy absence preserved" : !controlRequired && daemon.kind === "absent" ? "managed control is unsupported on this provider or operating-system session" : null;
|
|
577
582
|
check(
|
|
578
583
|
daemonOk,
|
|
@@ -584,9 +589,25 @@ var main = async () => {
|
|
|
584
589
|
`daemon ${daemon.daemonVersion ?? "none"}`,
|
|
585
590
|
`installed ${daemon.installedVersion}`,
|
|
586
591
|
`machine ${daemon.machineId ?? "none"}`,
|
|
587
|
-
`heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}
|
|
592
|
+
`heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`,
|
|
593
|
+
`control ${daemon.lastSuccessfulControlAt ?? "none yet"}`
|
|
588
594
|
].filter((value) => value !== null).join(" \xB7 ")
|
|
589
595
|
);
|
|
596
|
+
if (controlPolicy === "enabled" && apiKey && daemon.machineId) {
|
|
597
|
+
const serverProof = await readMachineReadiness(apiKey, ["spawn", ...controlCapabilities], {
|
|
598
|
+
machineId: daemon.machineId
|
|
599
|
+
});
|
|
600
|
+
if (serverProof.kind === "unsupported") {
|
|
601
|
+
console.log(` ${warn} Remote-control server proof unavailable ${dim(`(${serverProof.detail})`)}`);
|
|
602
|
+
} else {
|
|
603
|
+
check(
|
|
604
|
+
serverProof.kind === "ready",
|
|
605
|
+
"Remote-control server proof",
|
|
606
|
+
serverProof.kind === "ready" ? "heartbeat \xB7 capabilities \xB7 control queue" : serverProof.detail,
|
|
607
|
+
serverProof.kind === "failed" ? "unreachable" : void 0
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
590
611
|
const cursorPluginDir2 = cursorPluginDir();
|
|
591
612
|
if (existsSync(cursorPluginDir2)) {
|
|
592
613
|
checking("Cursor");
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
codexWired,
|
|
8
8
|
geminiWired
|
|
9
9
|
} from "../chunk-BAE45YLP.js";
|
|
10
|
+
import {
|
|
11
|
+
waitForMachineReadiness
|
|
12
|
+
} from "../chunk-U5TQCQ25.js";
|
|
10
13
|
import {
|
|
11
14
|
runBrowserLogin
|
|
12
15
|
} from "../chunk-Z3Z4VK5F.js";
|
|
@@ -50,14 +53,16 @@ import {
|
|
|
50
53
|
guardedArgv
|
|
51
54
|
} from "../chunk-AIGDBRIJ.js";
|
|
52
55
|
import {
|
|
56
|
+
daemonHasRemoteProof,
|
|
53
57
|
daemonServiceManagerAvailable,
|
|
54
58
|
disableDaemonService,
|
|
55
59
|
ensureDaemonRunning,
|
|
56
60
|
hasDaemonServiceDefinition,
|
|
57
61
|
readDaemonStatus,
|
|
58
62
|
replaceDaemonService,
|
|
59
|
-
stopDaemonService
|
|
60
|
-
|
|
63
|
+
stopDaemonService,
|
|
64
|
+
waitForDaemonRemoteProof
|
|
65
|
+
} from "../chunk-543KNJKC.js";
|
|
61
66
|
import {
|
|
62
67
|
detectProviderLiveCapabilities,
|
|
63
68
|
hasSpawnProvider,
|
|
@@ -2125,6 +2130,14 @@ var main = async () => {
|
|
|
2125
2130
|
return { kind: "unavailable", detail: formatError(err) };
|
|
2126
2131
|
}
|
|
2127
2132
|
})() : controlMode === "on" || controlPolicy === "legacy" ? { kind: "unavailable", detail: "Managed control is required but no usable provider/service-manager pair is available" } : null;
|
|
2133
|
+
const localControlProof = daemon?.kind === "running" || daemon?.kind === "started" ? await waitForDaemonRemoteProof() : null;
|
|
2134
|
+
const serverControlProof = localControlProof && daemonHasRemoteProof(localControlProof) ? await waitForMachineReadiness(trimmedKey, ["spawn", ...controlCapabilities]) : null;
|
|
2135
|
+
const daemonReady = daemon === null ? null : localControlProof !== null && daemonHasRemoteProof(localControlProof) && (serverControlProof?.kind === "ready" || serverControlProof?.kind === "unsupported");
|
|
2136
|
+
if (serverControlProof?.kind === "unsupported") {
|
|
2137
|
+
console.log(` ${yellow("!")} This server predates control-readiness reporting; local heartbeat and queue checks passed.`);
|
|
2138
|
+
} else if (serverControlProof && serverControlProof.kind !== "ready") {
|
|
2139
|
+
console.log(` ${yellow("!")} Managed control could not be verified by the server: ${serverControlProof.detail}`);
|
|
2140
|
+
}
|
|
2128
2141
|
const closing = describeClosing({
|
|
2129
2142
|
agentsOk,
|
|
2130
2143
|
configuredNothing,
|
|
@@ -2132,7 +2145,7 @@ var main = async () => {
|
|
|
2132
2145
|
failed,
|
|
2133
2146
|
reachChecked,
|
|
2134
2147
|
reachesYou,
|
|
2135
|
-
daemonReady: controlLifecycleFailure ? false :
|
|
2148
|
+
daemonReady: controlLifecycleFailure ? false : daemonReady,
|
|
2136
2149
|
// null when the check did not run, so it changes nothing. Only an outright
|
|
2137
2150
|
// `false` is allowed to hold back "Setup complete.".
|
|
2138
2151
|
roundTripAnswered: roundTrip === null ? null : roundTrip.kind === "answered" ? true : roundTrip.kind === "unanswered" ? false : null
|
|
@@ -4,18 +4,23 @@ import {
|
|
|
4
4
|
} from "../chunk-EA3E74P2.js";
|
|
5
5
|
import "../chunk-J2JZJZKB.js";
|
|
6
6
|
import "../chunk-BAE45YLP.js";
|
|
7
|
+
import {
|
|
8
|
+
waitForMachineReadiness
|
|
9
|
+
} from "../chunk-U5TQCQ25.js";
|
|
7
10
|
import "../chunk-VA7VUNRD.js";
|
|
8
11
|
import "../chunk-ATBKJNWS.js";
|
|
9
12
|
import "../chunk-AIGDBRIJ.js";
|
|
10
13
|
import {
|
|
14
|
+
daemonHasRemoteProof,
|
|
11
15
|
daemonServiceManagerAvailable,
|
|
12
16
|
disableDaemonService,
|
|
13
17
|
ensureDaemonRunning,
|
|
14
18
|
hasDaemonServiceDefinition,
|
|
15
19
|
readDaemonStatus,
|
|
16
20
|
replaceDaemonService,
|
|
17
|
-
stopDaemonService
|
|
18
|
-
|
|
21
|
+
stopDaemonService,
|
|
22
|
+
waitForDaemonRemoteProof
|
|
23
|
+
} from "../chunk-543KNJKC.js";
|
|
19
24
|
import {
|
|
20
25
|
detectProviderLiveCapabilities,
|
|
21
26
|
hasSpawnProvider,
|
|
@@ -129,9 +134,10 @@ var finishUpgrade = async () => {
|
|
|
129
134
|
const controlMode = readControlMode();
|
|
130
135
|
const daemonStatus = readDaemonStatus();
|
|
131
136
|
const serviceWasPresent = daemonStatus.kind !== "absent" || hasDaemonServiceDefinition();
|
|
137
|
+
const controlCapabilities = detectProviderLiveCapabilities(configuredControlProviders);
|
|
132
138
|
const controlPolicy = resolveControlPolicy({
|
|
133
139
|
mode: controlMode,
|
|
134
|
-
providerAvailable: hasSpawnProvider(
|
|
140
|
+
providerAvailable: hasSpawnProvider(controlCapabilities),
|
|
135
141
|
managerAvailable: daemonServiceManagerAvailable(),
|
|
136
142
|
servicePresent: serviceWasPresent
|
|
137
143
|
});
|
|
@@ -150,7 +156,9 @@ var finishUpgrade = async () => {
|
|
|
150
156
|
const service = await replaceDaemonService();
|
|
151
157
|
const daemon = service.kind === "ok" ? await ensureDaemonRunning({ spawnDaemon: () => {
|
|
152
158
|
} }) : null;
|
|
153
|
-
|
|
159
|
+
const localProof = daemon?.kind === "running" || daemon?.kind === "started" ? await waitForDaemonRemoteProof() : null;
|
|
160
|
+
const serverProof = localProof && daemonHasRemoteProof(localProof) ? await waitForMachineReadiness(apiKey, ["spawn", ...controlCapabilities]) : null;
|
|
161
|
+
daemonReady = service.kind === "ok" && localProof !== null && daemonHasRemoteProof(localProof) && (serverProof?.kind === "ready" || serverProof?.kind === "unsupported");
|
|
154
162
|
}
|
|
155
163
|
} else if (controlMode === "on" && controlPolicy === "unsupported") {
|
|
156
164
|
daemonReady = false;
|
|
@@ -31,6 +31,14 @@ import { createHash, randomUUID } from "crypto";
|
|
|
31
31
|
import { homedir } from "os";
|
|
32
32
|
import { execFileSync } from "child_process";
|
|
33
33
|
var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
|
|
34
|
+
var DAEMON_REMOTE_PROOF_MAX_AGE_MS = 18e4;
|
|
35
|
+
var DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS = 3e4;
|
|
36
|
+
var remoteProofIsFresh = (timestamp, now) => {
|
|
37
|
+
if (timestamp === null) return false;
|
|
38
|
+
const age = now - Date.parse(timestamp);
|
|
39
|
+
return Number.isFinite(age) && age >= -DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS && age < DAEMON_REMOTE_PROOF_MAX_AGE_MS;
|
|
40
|
+
};
|
|
41
|
+
var daemonHasRemoteProof = (status, now = Date.now()) => status.kind === "running" && remoteProofIsFresh(status.lastSuccessfulHeartbeatAt, now) && remoteProofIsFresh(status.lastSuccessfulControlAt, now);
|
|
34
42
|
var runDaemonServiceCommand = (command, args) => execFileSync(command, [...args], {
|
|
35
43
|
encoding: "utf-8",
|
|
36
44
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -241,7 +249,7 @@ WantedBy=default.target
|
|
|
241
249
|
"-NoProfile",
|
|
242
250
|
"-NonInteractive",
|
|
243
251
|
"-Command",
|
|
244
|
-
"$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($task.
|
|
252
|
+
"$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($null -eq $task.Settings -or $null -eq $task.Settings.Enabled) { exit 5 }; if (-not $task.Settings.Enabled) { exit 4 }"
|
|
245
253
|
],
|
|
246
254
|
negativeExitCodes: [3, 4]
|
|
247
255
|
},
|
|
@@ -313,16 +321,10 @@ var applyDaemonService = (action, definition, options = {}) => {
|
|
|
313
321
|
const steps = action === "uninstall" ? [...definition.stop, ...definition.uninstall] : definition[action];
|
|
314
322
|
runDaemonServiceSteps(steps, run);
|
|
315
323
|
if (action === "uninstall") {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!isExpectedProbeFailure(definition.registrationProbe, err)) {
|
|
321
|
-
throw new Error(`Could not verify native background service is absent: ${commandFailureOutput(err) || String(err)}`);
|
|
322
|
-
}
|
|
323
|
-
registered = false;
|
|
324
|
-
}
|
|
325
|
-
if (registered) throw new Error("Native background service remains registered");
|
|
324
|
+
const state = probeDaemonServiceState(definition, run);
|
|
325
|
+
if (state.kind !== "ok") throw new Error(`Could not verify native background service is absent: ${state.detail}`);
|
|
326
|
+
if (state.registered) throw new Error("Native background service remains registered");
|
|
327
|
+
if (state.running) throw new Error("Native background service remains active");
|
|
326
328
|
safeUnlink(definition.definitionPath);
|
|
327
329
|
const reload = definition.reloadAfterUninstall;
|
|
328
330
|
if (reload) run(reload.command, reload.args);
|
|
@@ -412,7 +414,7 @@ var currentConfigurationId = () => {
|
|
|
412
414
|
var parseDaemonState = (value) => {
|
|
413
415
|
if (typeof value !== "object" || value === null) return null;
|
|
414
416
|
const state = value;
|
|
415
|
-
if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt))) return null;
|
|
417
|
+
if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt)) || !(state.lastSuccessfulControlAt === void 0 || state.lastSuccessfulControlAt === null || isTimestamp(state.lastSuccessfulControlAt))) return null;
|
|
416
418
|
return {
|
|
417
419
|
instanceId: state.instanceId,
|
|
418
420
|
pid: state.pid,
|
|
@@ -421,7 +423,8 @@ var parseDaemonState = (value) => {
|
|
|
421
423
|
machineId: state.machineId,
|
|
422
424
|
startedAt: state.startedAt,
|
|
423
425
|
updatedAt: state.updatedAt,
|
|
424
|
-
lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt
|
|
426
|
+
lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt,
|
|
427
|
+
lastSuccessfulControlAt: state.lastSuccessfulControlAt ?? null
|
|
425
428
|
};
|
|
426
429
|
};
|
|
427
430
|
var parseLockOwner = (value) => {
|
|
@@ -443,7 +446,8 @@ var readDaemonStatus = (options = {}) => {
|
|
|
443
446
|
daemonVersion: null,
|
|
444
447
|
startedAt: null,
|
|
445
448
|
updatedAt: null,
|
|
446
|
-
lastSuccessfulHeartbeatAt: null
|
|
449
|
+
lastSuccessfulHeartbeatAt: null,
|
|
450
|
+
lastSuccessfulControlAt: null
|
|
447
451
|
};
|
|
448
452
|
}
|
|
449
453
|
const value = state.kind === "ok" ? parseDaemonState(state.value) : null;
|
|
@@ -460,7 +464,8 @@ var readDaemonStatus = (options = {}) => {
|
|
|
460
464
|
daemonVersion: value.version,
|
|
461
465
|
startedAt: value.startedAt,
|
|
462
466
|
updatedAt: value.updatedAt,
|
|
463
|
-
lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt
|
|
467
|
+
lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt,
|
|
468
|
+
lastSuccessfulControlAt: value.lastSuccessfulControlAt
|
|
464
469
|
};
|
|
465
470
|
}
|
|
466
471
|
return {
|
|
@@ -472,7 +477,8 @@ var readDaemonStatus = (options = {}) => {
|
|
|
472
477
|
daemonVersion: null,
|
|
473
478
|
startedAt: null,
|
|
474
479
|
updatedAt: null,
|
|
475
|
-
lastSuccessfulHeartbeatAt: null
|
|
480
|
+
lastSuccessfulHeartbeatAt: null,
|
|
481
|
+
lastSuccessfulControlAt: null
|
|
476
482
|
};
|
|
477
483
|
};
|
|
478
484
|
var safeUnlink = (path) => {
|
|
@@ -540,7 +546,8 @@ var acquireDaemonLease = (options = {}) => {
|
|
|
540
546
|
machineId,
|
|
541
547
|
startedAt,
|
|
542
548
|
updatedAt: startedAt,
|
|
543
|
-
lastSuccessfulHeartbeatAt: null
|
|
549
|
+
lastSuccessfulHeartbeatAt: null,
|
|
550
|
+
lastSuccessfulControlAt: null
|
|
544
551
|
};
|
|
545
552
|
try {
|
|
546
553
|
writeJsonAtomic(statePath(dir), state, 384);
|
|
@@ -576,6 +583,14 @@ var acquireDaemonLease = (options = {}) => {
|
|
|
576
583
|
lastSuccessfulHeartbeatAt: timestamp
|
|
577
584
|
});
|
|
578
585
|
},
|
|
586
|
+
recordSuccessfulControl: (at = now()) => {
|
|
587
|
+
const timestamp = new Date(at).toISOString();
|
|
588
|
+
updateState({
|
|
589
|
+
...currentState,
|
|
590
|
+
updatedAt: timestamp,
|
|
591
|
+
lastSuccessfulControlAt: timestamp
|
|
592
|
+
});
|
|
593
|
+
},
|
|
579
594
|
shouldYield: () => {
|
|
580
595
|
if (!isOwner()) return true;
|
|
581
596
|
const target = readJsonSafe(targetPath(dir));
|
|
@@ -645,7 +660,8 @@ var deactivateDaemonService = async (action, options) => {
|
|
|
645
660
|
const platform = options.platform ?? process.platform;
|
|
646
661
|
const supported = ["darwin", "linux", "win32"].includes(platform);
|
|
647
662
|
const managerAvailable = supported && daemonServiceManagerAvailable(options);
|
|
648
|
-
const
|
|
663
|
+
const absenceMustBeProven = options.requireAbsenceProof || hasDaemonServiceDefinition(options);
|
|
664
|
+
const service = supported && !managerAvailable && absenceMustBeProven ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService(action, options) : { kind: "ok" };
|
|
649
665
|
const stopped = await stopDaemonRunning(options);
|
|
650
666
|
const failures = [
|
|
651
667
|
service.kind === "ok" ? null : service.detail,
|
|
@@ -956,8 +972,21 @@ var ensureDaemonRunning = async (options = {}) => {
|
|
|
956
972
|
}
|
|
957
973
|
return { kind: "unavailable", status, detail: `daemon did not become healthy (${status.kind})` };
|
|
958
974
|
};
|
|
975
|
+
var waitForDaemonRemoteProof = async (options = {}) => {
|
|
976
|
+
const sleep = options.sleep ?? wait;
|
|
977
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
978
|
+
const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
|
|
979
|
+
let status = readDaemonStatus(options);
|
|
980
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
981
|
+
status = readDaemonStatus(options);
|
|
982
|
+
if (daemonHasRemoteProof(status)) return status;
|
|
983
|
+
if (attempt + 1 < attempts) await sleep(pollIntervalMs);
|
|
984
|
+
}
|
|
985
|
+
return status;
|
|
986
|
+
};
|
|
959
987
|
|
|
960
988
|
export {
|
|
989
|
+
daemonHasRemoteProof,
|
|
961
990
|
daemonServiceManagerAvailable,
|
|
962
991
|
DAEMON_LEASE_RENEW_INTERVAL_MS,
|
|
963
992
|
parseDaemonAction,
|
|
@@ -970,5 +999,6 @@ export {
|
|
|
970
999
|
stopDaemonService,
|
|
971
1000
|
disableDaemonService,
|
|
972
1001
|
replaceDaemonService,
|
|
973
|
-
ensureDaemonRunning
|
|
1002
|
+
ensureDaemonRunning,
|
|
1003
|
+
waitForDaemonRemoteProof
|
|
974
1004
|
};
|
|
@@ -46,9 +46,38 @@ var parseMachine = (value) => {
|
|
|
46
46
|
repoKey: typeof raw.repoKey === "string" ? raw.repoKey : null,
|
|
47
47
|
capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((c) => typeof c === "string") : [],
|
|
48
48
|
lastSeenAt: typeof raw.lastSeenAt === "string" ? raw.lastSeenAt : "",
|
|
49
|
+
lastControlAt: typeof raw.lastControlAt === "string" ? raw.lastControlAt : raw.lastControlAt === null ? null : void 0,
|
|
49
50
|
online: raw.online === true
|
|
50
51
|
};
|
|
51
52
|
};
|
|
53
|
+
var machineReadiness = (machines, machineId, expectedCapabilities) => {
|
|
54
|
+
const machine = machines.find((item) => item.machineId === machineId);
|
|
55
|
+
if (!machine) return { kind: "not-ready", detail: "this machine has not heartbeated to the server" };
|
|
56
|
+
if (!machine.online) return { kind: "not-ready", detail: "the server reports this machine offline" };
|
|
57
|
+
if (machine.lastControlAt === void 0) {
|
|
58
|
+
return { kind: "unsupported", detail: "the server does not report control readiness yet" };
|
|
59
|
+
}
|
|
60
|
+
if (machine.lastControlAt === null || !Number.isFinite(Date.parse(machine.lastControlAt))) {
|
|
61
|
+
return { kind: "not-ready", detail: "the server has not observed this machine polling the control queue" };
|
|
62
|
+
}
|
|
63
|
+
const missing = expectedCapabilities.filter((capability) => !machine.capabilities.includes(capability));
|
|
64
|
+
return missing.length === 0 ? { kind: "ready", machine } : { kind: "not-ready", detail: `the server is missing capabilities: ${missing.join(", ")}` };
|
|
65
|
+
};
|
|
66
|
+
var readMachineReadiness = async (apiKey, expectedCapabilities, deps = {}) => {
|
|
67
|
+
const listed = await listMachines(apiKey, deps);
|
|
68
|
+
return listed.kind === "ok" ? machineReadiness(listed.machines, deps.machineId ?? getMachineId(), expectedCapabilities) : listed.kind === "unsupported" ? { kind: "unsupported", detail: "the server does not report machine readiness yet" } : listed;
|
|
69
|
+
};
|
|
70
|
+
var waitForMachineReadiness = async (apiKey, expectedCapabilities, deps = {}) => {
|
|
71
|
+
const attempts = deps.attempts ?? 21;
|
|
72
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
73
|
+
let result = { kind: "not-ready", detail: "this machine has not heartbeated to the server" };
|
|
74
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
75
|
+
result = await readMachineReadiness(apiKey, expectedCapabilities, deps);
|
|
76
|
+
if (result.kind === "ready" || result.kind === "unsupported" || result.kind === "failed" && /^HTTP (?:401|403)$/.test(result.detail)) return result;
|
|
77
|
+
if (attempt + 1 < attempts) await sleep(250);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
52
81
|
var listMachines = async (apiKey, deps = {}) => {
|
|
53
82
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
54
83
|
const baseUrl = deps.baseUrl ?? getBaseUrl();
|
|
@@ -81,6 +110,8 @@ var describeLastSeen = (iso, now = Date.now()) => {
|
|
|
81
110
|
export {
|
|
82
111
|
HEARTBEAT_INTERVAL_MS,
|
|
83
112
|
sendHeartbeat,
|
|
113
|
+
readMachineReadiness,
|
|
114
|
+
waitForMachineReadiness,
|
|
84
115
|
listMachines,
|
|
85
116
|
describeLastSeen
|
|
86
117
|
};
|