@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22
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/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.d.ts +105 -7
- package/dist/bin/evolver-proxy.js +877 -121
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +26 -16
- package/dist/daemon/proxyDaemon.d.ts +65 -0
- package/dist/daemon/proxyDaemon.js +1384 -29
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +48 -0
- package/dist/daemon/systemdNotifier.js +163 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/legacyNodeId.d.ts +11 -13
- package/dist/lifecycle/legacyNodeId.js +35 -20
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +72 -2
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +27 -11
- package/dist/selfUpdate/executor.js +233 -58
- package/dist/selfUpdate/failureCodes.d.ts +10 -0
- package/dist/selfUpdate/failureCodes.js +13 -0
- package/dist/selfUpdate/index.d.ts +5 -1
- package/dist/selfUpdate/index.js +5 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +13 -0
- package/dist/selfUpdate/releaseBinary.js +93 -10
- package/dist/selfUpdate/transaction.d.ts +117 -0
- package/dist/selfUpdate/transaction.js +1322 -0
- package/dist/selfUpdate/unixController.d.ts +23 -0
- package/dist/selfUpdate/unixController.js +514 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +35 -0
- package/dist/selfUpdate/windowsController.js +655 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
- package/dist/selfUpdate/windowsUpdater.js +882 -0
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +10 -3
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import { bindStableWindowsRecoveryController, inspectDurableSelfUpdate, markWindowsInstallApplied, provisionStableWindowsRecoveryController, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from './transaction.js';
|
|
4
|
+
import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
|
|
5
|
+
import { revalidatePendingWindowsUpdaterHelper, resolveWindowsUpdaterPaths, WINDOWS_UPDATER_WORKER_ARG, } from './windowsUpdater.js';
|
|
6
|
+
import { resolveRecoveryControllerAuthority, } from './controllerLifecycleAuthority.js';
|
|
7
|
+
import { DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS, deliverRecoveryChildStartGate, RECOVERY_CHILD_START_GATE_ENV, } from './recoveryChildStartGate.js';
|
|
8
|
+
export const WINDOWS_RECOVERY_CONTROLLER_ARG = '--evolver-windows-recovery-controller';
|
|
9
|
+
export const WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG = '--evolver-windows-recovery-controller-provision';
|
|
10
|
+
const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
|
|
11
|
+
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
12
|
+
const DEFAULT_STOP_TIMEOUT_MS = 2_000;
|
|
13
|
+
const DEFAULT_WORKER_TIMEOUT_MS = 120_000;
|
|
14
|
+
// The child performs up to three native owner-identity probes before acknowledging. Each
|
|
15
|
+
// PowerShell-backed Windows probe has its own 15s budget, so keep enough headroom for CI hosts.
|
|
16
|
+
const DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS = 120_000;
|
|
17
|
+
const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
|
|
18
|
+
const RECOVER_BEFORE_START_STAGES = new Set([
|
|
19
|
+
'preparing',
|
|
20
|
+
'downloaded',
|
|
21
|
+
'verified',
|
|
22
|
+
'backed_up',
|
|
23
|
+
'install_pending',
|
|
24
|
+
'rolling_back',
|
|
25
|
+
'rollback_pending',
|
|
26
|
+
]);
|
|
27
|
+
const PENDING_SWAP_JOURNAL_STAGES = new Set([
|
|
28
|
+
'install_pending',
|
|
29
|
+
'restarted',
|
|
30
|
+
'rolling_back',
|
|
31
|
+
'rollback_pending',
|
|
32
|
+
]);
|
|
33
|
+
class NativeChildTerminationUnconfirmedError extends Error {
|
|
34
|
+
child;
|
|
35
|
+
exit;
|
|
36
|
+
constructor(child, exit) {
|
|
37
|
+
super('windows_updater_worker_termination_unconfirmed');
|
|
38
|
+
this.child = child;
|
|
39
|
+
this.exit = exit;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function maybeRunWindowsRecoveryController(options = {}) {
|
|
43
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
44
|
+
const command = argv.length === 2 && argv[0] === 'proxy' ? argv[1] : undefined;
|
|
45
|
+
if (command !== WINDOWS_RECOVERY_CONTROLLER_ARG && command !== WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if ((options.platform ?? process.platform) !== 'win32')
|
|
49
|
+
return 64;
|
|
50
|
+
try {
|
|
51
|
+
if (command === WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG) {
|
|
52
|
+
await provisionStableWindowsRecoveryController({
|
|
53
|
+
...options,
|
|
54
|
+
replaceExisting: options.env?.['EVOLVER_INTERNAL_BOOTSTRAP_EXCLUSIVE'] !== '1',
|
|
55
|
+
}, options.processExecPath ?? process.execPath);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
return await runWindowsRecoveryController(options);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function runWindowsRecoveryController(options = {}) {
|
|
65
|
+
const env = options.env ?? process.env;
|
|
66
|
+
const platform = options.platform ?? process.platform;
|
|
67
|
+
const processExecPath = options.processExecPath ?? process.execPath;
|
|
68
|
+
const logger = options.logger ?? process.stderr;
|
|
69
|
+
const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
|
|
70
|
+
const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
|
|
71
|
+
const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
|
|
72
|
+
const workerTimeoutMs = positiveDuration(options.workerTimeoutMs, DEFAULT_WORKER_TIMEOUT_MS);
|
|
73
|
+
const startupGateTimeoutMs = positiveDuration(options.startupGateTimeoutMs, DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS);
|
|
74
|
+
const startupAttestationTimeoutMs = positiveDuration(options.startupAttestationTimeoutMs, DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS);
|
|
75
|
+
const spawnBoundTarget = options.spawnTarget ?? spawnTarget;
|
|
76
|
+
let authority;
|
|
77
|
+
const bindingOptions = { ...options, env, platform };
|
|
78
|
+
const bound = await bindStableWindowsRecoveryController(bindingOptions, processExecPath);
|
|
79
|
+
const transactionOptions = {
|
|
80
|
+
...options,
|
|
81
|
+
env,
|
|
82
|
+
platform,
|
|
83
|
+
processExecPath,
|
|
84
|
+
stateDir: bound.stateDir,
|
|
85
|
+
targetPath: bound.targetPath,
|
|
86
|
+
beforeJournalMutation: async () => {
|
|
87
|
+
assertOwnedRecoveryAuthority(authority);
|
|
88
|
+
await options.beforeJournalMutation?.();
|
|
89
|
+
await revalidateBoundWindowsController(bindingOptions, processExecPath, bound, authority);
|
|
90
|
+
assertOwnedRecoveryAuthority(authority);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const observed = await inspectDurableSelfUpdate(transactionOptions);
|
|
94
|
+
if (observed.outcome === 'blocked') {
|
|
95
|
+
logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
let child;
|
|
99
|
+
let childExit;
|
|
100
|
+
let releaseAttempted = false;
|
|
101
|
+
const guardUnconfirmedChild = async (_target, exit) => {
|
|
102
|
+
logger.write('[evolver-windows-controller] child_termination_unconfirmed\n');
|
|
103
|
+
if (authority?.kind === 'owned') {
|
|
104
|
+
try {
|
|
105
|
+
authority.retainProcess();
|
|
106
|
+
logger.write('[evolver-windows-controller] child_guardian_active\n');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
logger.write('[evolver-windows-controller] child_guardian_retain_failed\n');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Without a verifiable PID generation there is no safe lock handoff. Keep this live owner
|
|
114
|
+
// until exact exit instead of returning a dead-owner lock that another process could reclaim.
|
|
115
|
+
await exit;
|
|
116
|
+
};
|
|
117
|
+
const armBoundChild = (target) => {
|
|
118
|
+
if (authority?.kind !== 'owned')
|
|
119
|
+
return;
|
|
120
|
+
if (target.pid === undefined) {
|
|
121
|
+
throw new Error('self_update_recovery_controller_child_pid_unavailable');
|
|
122
|
+
}
|
|
123
|
+
authority.armProcess(target.pid);
|
|
124
|
+
};
|
|
125
|
+
const disarmBoundChild = () => {
|
|
126
|
+
if (authority?.kind === 'owned')
|
|
127
|
+
authority.disarmProcess();
|
|
128
|
+
};
|
|
129
|
+
const stopBoundChild = async (target, exit) => {
|
|
130
|
+
const stopped = await stopNativeChild(target, exit, stopTimeoutMs);
|
|
131
|
+
if (!stopped) {
|
|
132
|
+
await guardUnconfirmedChild(target, exit);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
disarmBoundChild();
|
|
136
|
+
}
|
|
137
|
+
return stopped;
|
|
138
|
+
};
|
|
139
|
+
const runBoundUpdaterWorker = options.runUpdaterWorker
|
|
140
|
+
?? ((workerPath, stateDir, workerEnv, startupGateToken) => runUpdaterWorker(workerPath, stateDir, workerEnv, startupGateToken, workerTimeoutMs, stopTimeoutMs, startupGateTimeoutMs, armBoundChild, disarmBoundChild, () => { assertOwnedRecoveryAuthority(authority); }, options.spawnUpdaterWorker ?? spawnUpdaterWorker));
|
|
141
|
+
const releaseAuthority = async () => {
|
|
142
|
+
if (!authority || releaseAttempted)
|
|
143
|
+
return true;
|
|
144
|
+
releaseAttempted = true;
|
|
145
|
+
try {
|
|
146
|
+
authority.release();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
logger.write('[evolver-windows-controller] lifecycle_authority_release_failed\n');
|
|
151
|
+
if (child && childExit)
|
|
152
|
+
await stopBoundChild(child, childExit);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const launchTerminalTarget = async () => {
|
|
157
|
+
await revalidateBoundWindowsController(bindingOptions, processExecPath, bound, authority);
|
|
158
|
+
authority.assertAuthorized();
|
|
159
|
+
const prepared = authority.prepareTarget(env);
|
|
160
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
161
|
+
child = launched;
|
|
162
|
+
const launchedExit = waitForNativeExit(launched);
|
|
163
|
+
childExit = launchedExit;
|
|
164
|
+
const spawned = waitForNativeSpawn(launched);
|
|
165
|
+
try {
|
|
166
|
+
// Node publishes the native PID synchronously when spawn succeeds. Arm the guardian before
|
|
167
|
+
// the first await so a hard controller crash cannot expose a stale parent-owner lock.
|
|
168
|
+
armBoundChild(launched);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
logger.write('[evolver-windows-controller] child_guardian_arm_failed\n');
|
|
172
|
+
if (await spawned)
|
|
173
|
+
await stopBoundChild(launched, launchedExit);
|
|
174
|
+
await releaseAuthority();
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
if (!await spawned) {
|
|
178
|
+
disarmBoundChild();
|
|
179
|
+
await releaseAuthority();
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
authority.assertAuthorized();
|
|
183
|
+
if (!await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs)) {
|
|
184
|
+
logger.write('[evolver-windows-controller] startup_gate_failed\n');
|
|
185
|
+
await stopBoundChild(launched, launchedExit);
|
|
186
|
+
await releaseAuthority();
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
if (prepared.startupAckToken
|
|
190
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs)) {
|
|
191
|
+
logger.write('[evolver-windows-controller] startup_attestation_failed\n');
|
|
192
|
+
await stopBoundChild(launched, launchedExit);
|
|
193
|
+
await releaseAuthority();
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
if (!await releaseAuthority())
|
|
197
|
+
return 1;
|
|
198
|
+
return exitCode(await launchedExit);
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
authority = await resolveRecoveryControllerAuthority(env, options.lifecycleAuthority);
|
|
202
|
+
await revalidateBoundWindowsController(bindingOptions, processExecPath, bound, authority);
|
|
203
|
+
let initial = await inspectDurableSelfUpdate(transactionOptions);
|
|
204
|
+
if (initial.outcome === 'blocked') {
|
|
205
|
+
logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
if (authority.kind === 'delegated' && !stateAllowsDelegatedSpawn(initial)) {
|
|
209
|
+
logger.write('[evolver-windows-controller] lifecycle_authority_blocked\n');
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
if (await pendingSwapExists(bound.stateDir)) {
|
|
213
|
+
if (initial.stage === undefined || !PENDING_SWAP_JOURNAL_STAGES.has(initial.stage)) {
|
|
214
|
+
logger.write('[evolver-windows-controller] pending_swap_blocked\n');
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
assertOwnedRecoveryAuthority(authority);
|
|
218
|
+
const workerExitCode = await executeBoundUpdaterWorker(runBoundUpdaterWorker, bound.stateDir, env, authority, options.assertUpdaterHelperTrust);
|
|
219
|
+
assertOwnedRecoveryAuthority(authority);
|
|
220
|
+
if (workerExitCode !== 0) {
|
|
221
|
+
let rollback;
|
|
222
|
+
try {
|
|
223
|
+
rollback = await rollbackDurableSelfUpdate(transactionOptions, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
|
|
224
|
+
rollback = await completePreparedRollback(rollback, transactionOptions, bound.stateDir, env, runBoundUpdaterWorker, authority, options.assertUpdaterHelperTrust);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
logger.write('[evolver-windows-controller] pending_swap_blocked\n');
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
if (rollback.outcome !== 'rolled_back') {
|
|
231
|
+
logger.write('[evolver-windows-controller] pending_swap_blocked\n');
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
return await launchTerminalTarget();
|
|
235
|
+
}
|
|
236
|
+
initial = await inspectDurableSelfUpdate(transactionOptions);
|
|
237
|
+
}
|
|
238
|
+
if (initial.stage === 'install_pending') {
|
|
239
|
+
assertOwnedRecoveryAuthority(authority);
|
|
240
|
+
initial = await markWindowsInstallApplied(transactionOptions);
|
|
241
|
+
if (initial.outcome === 'blocked') {
|
|
242
|
+
logger.write('[evolver-windows-controller] install_not_applied\n');
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (initial.outcome === 'blocked') {
|
|
247
|
+
logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
|
|
251
|
+
assertOwnedRecoveryAuthority(authority);
|
|
252
|
+
initial = await recoverAndApplyRollback(transactionOptions, bound.stateDir, env, runBoundUpdaterWorker, authority, options.assertUpdaterHelperTrust);
|
|
253
|
+
if (initial.outcome === 'blocked' || initial.outcome === 'rollback_pending') {
|
|
254
|
+
logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const pendingAtLaunch = isControllerPending(initial);
|
|
259
|
+
if (!pendingAtLaunch)
|
|
260
|
+
return await launchTerminalTarget();
|
|
261
|
+
await revalidateBoundWindowsController(bindingOptions, processExecPath, bound, authority);
|
|
262
|
+
assertOwnedRecoveryAuthority(authority);
|
|
263
|
+
const prepared = authority.prepareTarget(env);
|
|
264
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
265
|
+
child = launched;
|
|
266
|
+
const launchedExit = waitForNativeExit(launched);
|
|
267
|
+
childExit = launchedExit;
|
|
268
|
+
const spawned = waitForNativeSpawn(launched);
|
|
269
|
+
try {
|
|
270
|
+
// Arm before yielding for the same hard-crash boundary as terminal launches.
|
|
271
|
+
armBoundChild(launched);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
logger.write('[evolver-windows-controller] child_guardian_arm_failed\n');
|
|
275
|
+
if (await spawned)
|
|
276
|
+
await stopBoundChild(launched, launchedExit);
|
|
277
|
+
await releaseAuthority();
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
if (!await spawned) {
|
|
281
|
+
disarmBoundChild();
|
|
282
|
+
await releaseAuthority();
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
authority.assertAuthorized();
|
|
286
|
+
const startupGateAccepted = await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs);
|
|
287
|
+
if (!startupGateAccepted) {
|
|
288
|
+
logger.write('[evolver-windows-controller] startup_gate_failed\n');
|
|
289
|
+
}
|
|
290
|
+
if (!startupGateAccepted || (prepared.startupAckToken
|
|
291
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs))) {
|
|
292
|
+
if (!await stopBoundChild(launched, launchedExit))
|
|
293
|
+
return 1;
|
|
294
|
+
let rollback = await rollbackDurableSelfUpdate(transactionOptions, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
|
|
295
|
+
rollback = await completePreparedRollback(rollback, transactionOptions, bound.stateDir, env, runBoundUpdaterWorker, authority, options.assertUpdaterHelperTrust);
|
|
296
|
+
if (rollback.outcome === 'confirmed') {
|
|
297
|
+
if (!await releaseAuthority())
|
|
298
|
+
return 1;
|
|
299
|
+
return exitCode(await launchedExit);
|
|
300
|
+
}
|
|
301
|
+
if (rollback.outcome !== 'rolled_back') {
|
|
302
|
+
logger.write('[evolver-windows-controller] startup_attestation_failed\n');
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
child = undefined;
|
|
306
|
+
childExit = undefined;
|
|
307
|
+
return await launchTerminalTarget();
|
|
308
|
+
}
|
|
309
|
+
const deadline = Date.now() + confirmationTimeoutMs;
|
|
310
|
+
for (;;) {
|
|
311
|
+
const event = await Promise.race([
|
|
312
|
+
launchedExit.then((exit) => ({ type: 'exit', exit })),
|
|
313
|
+
delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
|
|
314
|
+
.then(() => ({ type: 'poll' })),
|
|
315
|
+
]);
|
|
316
|
+
assertOwnedRecoveryAuthority(authority);
|
|
317
|
+
let state;
|
|
318
|
+
try {
|
|
319
|
+
state = await inspectDurableSelfUpdate(transactionOptions);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
if (event.type === 'exit' || Date.now() >= deadline) {
|
|
323
|
+
logger.write('[evolver-windows-controller] recovery_state_unavailable\n');
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (state?.outcome === 'confirmed') {
|
|
330
|
+
if (!await releaseAuthority())
|
|
331
|
+
return 1;
|
|
332
|
+
return exitCode(await launchedExit);
|
|
333
|
+
}
|
|
334
|
+
if (state?.outcome === 'blocked') {
|
|
335
|
+
await stopBoundChild(launched, launchedExit);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
if (state?.outcome === 'rolled_back') {
|
|
339
|
+
if (!await stopBoundChild(launched, launchedExit))
|
|
340
|
+
return 1;
|
|
341
|
+
child = undefined;
|
|
342
|
+
childExit = undefined;
|
|
343
|
+
return await launchTerminalTarget();
|
|
344
|
+
}
|
|
345
|
+
if (event.type === 'exit' || Date.now() >= deadline) {
|
|
346
|
+
if (!await stopBoundChild(launched, launchedExit))
|
|
347
|
+
return 1;
|
|
348
|
+
let rollback;
|
|
349
|
+
try {
|
|
350
|
+
rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
|
|
351
|
+
? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
|
|
352
|
+
: 'health_confirmation_timeout');
|
|
353
|
+
rollback = await completePreparedRollback(rollback, transactionOptions, bound.stateDir, env, runBoundUpdaterWorker, authority, options.assertUpdaterHelperTrust);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
logger.write('[evolver-windows-controller] rollback_blocked\n');
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
if (rollback.outcome === 'confirmed') {
|
|
360
|
+
if (!await releaseAuthority())
|
|
361
|
+
return 1;
|
|
362
|
+
return exitCode(await launchedExit);
|
|
363
|
+
}
|
|
364
|
+
if (rollback.outcome !== 'rolled_back') {
|
|
365
|
+
logger.write('[evolver-windows-controller] rollback_blocked\n');
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
child = undefined;
|
|
369
|
+
childExit = undefined;
|
|
370
|
+
return await launchTerminalTarget();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof NativeChildTerminationUnconfirmedError) {
|
|
376
|
+
await guardUnconfirmedChild(error.child, error.exit);
|
|
377
|
+
}
|
|
378
|
+
logger.write('[evolver-windows-controller] lifecycle_authority_blocked\n');
|
|
379
|
+
if (child && childExit)
|
|
380
|
+
await stopBoundChild(child, childExit);
|
|
381
|
+
await releaseAuthority();
|
|
382
|
+
return 1;
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
if (!releaseAttempted)
|
|
386
|
+
await releaseAuthority();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function recoverAndApplyRollback(options, stateDir, env, runBoundUpdaterWorker, authority, assertUpdaterHelperTrust) {
|
|
390
|
+
assertOwnedRecoveryAuthority(authority);
|
|
391
|
+
const recovery = await recoverDurableSelfUpdate(options);
|
|
392
|
+
return completePreparedRollback(recovery, options, stateDir, env, runBoundUpdaterWorker, authority, assertUpdaterHelperTrust);
|
|
393
|
+
}
|
|
394
|
+
async function completePreparedRollback(recovery, options, stateDir, env, runBoundUpdaterWorker, authority, assertUpdaterHelperTrust) {
|
|
395
|
+
if (recovery.outcome !== 'rollback_pending')
|
|
396
|
+
return recovery;
|
|
397
|
+
const workerExitCode = await executeBoundUpdaterWorker(runBoundUpdaterWorker, stateDir, env, authority, assertUpdaterHelperTrust);
|
|
398
|
+
if (workerExitCode !== 0) {
|
|
399
|
+
return { ...recovery, outcome: 'blocked', failureCode: 'rollback_apply_failed' };
|
|
400
|
+
}
|
|
401
|
+
assertOwnedRecoveryAuthority(authority);
|
|
402
|
+
return recoverDurableSelfUpdate(options);
|
|
403
|
+
}
|
|
404
|
+
async function pendingSwapExists(stateDir) {
|
|
405
|
+
const pendingPath = resolveWindowsUpdaterPaths(stateDir).pendingPath;
|
|
406
|
+
try {
|
|
407
|
+
const info = await lstat(pendingPath);
|
|
408
|
+
if (info.isSymbolicLink() || !info.isFile())
|
|
409
|
+
throw new Error('windows_controller_pending_unsafe');
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (isErrno(error, 'ENOENT'))
|
|
414
|
+
return false;
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function isControllerPending(state) {
|
|
419
|
+
return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
|
|
420
|
+
}
|
|
421
|
+
function stateAllowsDelegatedSpawn(state) {
|
|
422
|
+
return state.outcome === 'none'
|
|
423
|
+
|| state.outcome === 'confirmed'
|
|
424
|
+
|| state.outcome === 'rolled_back';
|
|
425
|
+
}
|
|
426
|
+
function assertOwnedRecoveryAuthority(authority) {
|
|
427
|
+
if (authority?.kind !== 'owned') {
|
|
428
|
+
throw new Error('self_update_recovery_controller_owner_lease_required');
|
|
429
|
+
}
|
|
430
|
+
authority.assertAuthorized();
|
|
431
|
+
}
|
|
432
|
+
async function revalidateBoundWindowsController(options, processExecPath, expected, authority) {
|
|
433
|
+
authority.assertAuthorized();
|
|
434
|
+
const actual = await bindStableWindowsRecoveryController(options, processExecPath);
|
|
435
|
+
if (actual.controllerPath !== expected.controllerPath
|
|
436
|
+
|| actual.stateDir !== expected.stateDir
|
|
437
|
+
|| actual.targetPath !== expected.targetPath) {
|
|
438
|
+
throw new Error('self_update_windows_recovery_controller_binding_changed');
|
|
439
|
+
}
|
|
440
|
+
authority.assertAuthorized();
|
|
441
|
+
}
|
|
442
|
+
function spawnTarget(targetPath, env, startupAttestation) {
|
|
443
|
+
const startupGate = env[RECOVERY_CHILD_START_GATE_ENV] !== undefined;
|
|
444
|
+
return spawn(targetPath, ['proxy'], {
|
|
445
|
+
env,
|
|
446
|
+
stdio: startupGate
|
|
447
|
+
? ['inherit', 'inherit', 'inherit', startupAttestation ? 'pipe' : 'ignore', 'pipe']
|
|
448
|
+
: startupAttestation
|
|
449
|
+
? ['inherit', 'inherit', 'inherit', 'pipe']
|
|
450
|
+
: 'inherit',
|
|
451
|
+
windowsHide: true,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function runUpdaterWorker(workerPath, _stateDir, env, startupGateToken, timeoutMs, stopTimeoutMs, startupGateTimeoutMs, armChild, disarmChild, assertChildAuthority, spawnWorker) {
|
|
455
|
+
const child = spawnWorker(workerPath, env);
|
|
456
|
+
const exit = waitForNativeExit(child);
|
|
457
|
+
const spawned = waitForNativeSpawn(child);
|
|
458
|
+
try {
|
|
459
|
+
// Protect the updater PID generation before yielding to its spawn event.
|
|
460
|
+
armChild(child);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
if (await spawned && !await stopNativeChild(child, exit, stopTimeoutMs)) {
|
|
464
|
+
throw new NativeChildTerminationUnconfirmedError(child, exit);
|
|
465
|
+
}
|
|
466
|
+
throw new Error('windows_updater_worker_guardian_arm_failed');
|
|
467
|
+
}
|
|
468
|
+
if (!await spawned) {
|
|
469
|
+
disarmChild();
|
|
470
|
+
return 1;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
assertChildAuthority();
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
if (!await stopNativeChild(child, exit, stopTimeoutMs)) {
|
|
477
|
+
throw new NativeChildTerminationUnconfirmedError(child, exit);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
disarmChild();
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// The lost authority remains the primary failure.
|
|
484
|
+
}
|
|
485
|
+
throw new Error('windows_updater_worker_authority_lost_before_start_gate');
|
|
486
|
+
}
|
|
487
|
+
if (!await deliverRecoveryChildStartGate(child, startupGateToken, startupGateTimeoutMs)) {
|
|
488
|
+
if (!await stopNativeChild(child, exit, stopTimeoutMs)) {
|
|
489
|
+
throw new NativeChildTerminationUnconfirmedError(child, exit);
|
|
490
|
+
}
|
|
491
|
+
disarmChild();
|
|
492
|
+
return 1;
|
|
493
|
+
}
|
|
494
|
+
const event = await Promise.race([
|
|
495
|
+
exit.then((result) => ({ type: 'exit', result })),
|
|
496
|
+
delay(timeoutMs).then(() => ({ type: 'timeout' })),
|
|
497
|
+
]);
|
|
498
|
+
if (event.type === 'exit') {
|
|
499
|
+
disarmChild();
|
|
500
|
+
return exitCode(event.result);
|
|
501
|
+
}
|
|
502
|
+
if (!await stopNativeChild(child, exit, stopTimeoutMs)) {
|
|
503
|
+
throw new NativeChildTerminationUnconfirmedError(child, exit);
|
|
504
|
+
}
|
|
505
|
+
disarmChild();
|
|
506
|
+
return 1;
|
|
507
|
+
}
|
|
508
|
+
function spawnUpdaterWorker(workerPath, env) {
|
|
509
|
+
return spawn(workerPath, ['proxy', WINDOWS_UPDATER_WORKER_ARG], {
|
|
510
|
+
env,
|
|
511
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ignore', 'pipe'],
|
|
512
|
+
windowsHide: true,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function executeBoundUpdaterWorker(runner, stateDir, env, authority, assertUpdaterHelperTrust) {
|
|
516
|
+
assertOwnedRecoveryAuthority(authority);
|
|
517
|
+
const workerEnv = { ...env, EVOLVER_SELF_UPDATE_STATE_DIR: stateDir };
|
|
518
|
+
const prepared = authority.prepareWorker(workerEnv);
|
|
519
|
+
assertOwnedRecoveryAuthority(authority);
|
|
520
|
+
const paths = resolveWindowsUpdaterPaths(stateDir);
|
|
521
|
+
const bound = await revalidatePendingWindowsUpdaterHelper({
|
|
522
|
+
stateDir,
|
|
523
|
+
helperPath: paths.helperPath,
|
|
524
|
+
platform: 'win32',
|
|
525
|
+
...(assertUpdaterHelperTrust ? { assertHelperTrust: assertUpdaterHelperTrust } : {}),
|
|
526
|
+
});
|
|
527
|
+
assertOwnedRecoveryAuthority(authority);
|
|
528
|
+
const result = await runner(bound.executablePath, bound.stateDir, prepared.env, prepared.startupGateToken);
|
|
529
|
+
assertOwnedRecoveryAuthority(authority);
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
function waitForNativeExit(child) {
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
let spawned = child.pid !== undefined;
|
|
535
|
+
const cleanup = () => {
|
|
536
|
+
child.off('spawn', onSpawn);
|
|
537
|
+
child.off('error', onError);
|
|
538
|
+
};
|
|
539
|
+
const onSpawn = () => { spawned = true; };
|
|
540
|
+
const onError = (error) => {
|
|
541
|
+
// A post-spawn process error does not prove that the native child exited.
|
|
542
|
+
if (spawned)
|
|
543
|
+
return;
|
|
544
|
+
cleanup();
|
|
545
|
+
resolve({ code: null, signal: null, error });
|
|
546
|
+
};
|
|
547
|
+
child.once('spawn', onSpawn);
|
|
548
|
+
child.on('error', onError);
|
|
549
|
+
child.once('exit', (code, signal) => {
|
|
550
|
+
cleanup();
|
|
551
|
+
resolve({ code, signal });
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function waitForNativeSpawn(child) {
|
|
556
|
+
if (child.pid !== undefined)
|
|
557
|
+
return Promise.resolve(true);
|
|
558
|
+
return new Promise((resolve) => {
|
|
559
|
+
const finish = (spawned) => {
|
|
560
|
+
child.off('spawn', onSpawn);
|
|
561
|
+
child.off('error', onError);
|
|
562
|
+
resolve(spawned);
|
|
563
|
+
};
|
|
564
|
+
const onSpawn = () => { finish(true); };
|
|
565
|
+
const onError = () => { finish(false); };
|
|
566
|
+
child.once('spawn', onSpawn);
|
|
567
|
+
child.once('error', onError);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function waitForStartupAttestation(child, token, timeoutMs) {
|
|
571
|
+
const stream = child.stdio[3];
|
|
572
|
+
if (!stream || typeof stream.on !== 'function')
|
|
573
|
+
return Promise.resolve(false);
|
|
574
|
+
const readable = stream;
|
|
575
|
+
const expected = `${token}\n`;
|
|
576
|
+
return new Promise((resolve) => {
|
|
577
|
+
let raw = '';
|
|
578
|
+
let settled = false;
|
|
579
|
+
const finish = (result) => {
|
|
580
|
+
if (settled)
|
|
581
|
+
return;
|
|
582
|
+
settled = true;
|
|
583
|
+
clearTimeout(timer);
|
|
584
|
+
readable.off('data', onData);
|
|
585
|
+
readable.off('end', onEnd);
|
|
586
|
+
// Keep the error listener attached after timeout. stopNativeChild may close
|
|
587
|
+
// the child pipe with ECONNRESET; removing it here turns expected cleanup
|
|
588
|
+
// into an unhandled error in the test runner and in long-lived controllers.
|
|
589
|
+
resolve(result);
|
|
590
|
+
};
|
|
591
|
+
const onData = (chunk) => {
|
|
592
|
+
raw += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
593
|
+
if (Buffer.byteLength(raw, 'utf8') > 128 || !expected.startsWith(raw))
|
|
594
|
+
finish(false);
|
|
595
|
+
};
|
|
596
|
+
const onEnd = () => { finish(raw === expected); };
|
|
597
|
+
const onError = () => { finish(false); };
|
|
598
|
+
const timer = setTimeout(() => { finish(false); }, timeoutMs);
|
|
599
|
+
readable.on('data', onData);
|
|
600
|
+
readable.once('end', onEnd);
|
|
601
|
+
readable.once('error', onError);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function stopNativeChild(child, exit, timeoutMs) {
|
|
605
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
606
|
+
return true;
|
|
607
|
+
try {
|
|
608
|
+
child.kill('SIGTERM');
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// A failed signal is not an exit; the bounded native-exit wait remains authoritative.
|
|
612
|
+
}
|
|
613
|
+
const stopped = await Promise.race([
|
|
614
|
+
exit.then(() => true),
|
|
615
|
+
delay(timeoutMs).then(() => false),
|
|
616
|
+
]);
|
|
617
|
+
if (!stopped) {
|
|
618
|
+
try {
|
|
619
|
+
child.kill('SIGKILL');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// A failed signal is not an exit; the second bounded wait still protects authority.
|
|
623
|
+
}
|
|
624
|
+
return Promise.race([
|
|
625
|
+
exit.then(() => true),
|
|
626
|
+
delay(timeoutMs).then(() => false),
|
|
627
|
+
]);
|
|
628
|
+
}
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
function exitCode(exit) {
|
|
632
|
+
if (exit.error)
|
|
633
|
+
return 1;
|
|
634
|
+
if (exit.code !== null)
|
|
635
|
+
return exit.code;
|
|
636
|
+
return exit.signal === null ? 1 : 128 + signalNumber(exit.signal);
|
|
637
|
+
}
|
|
638
|
+
function signalNumber(signal) {
|
|
639
|
+
if (signal === 'SIGINT')
|
|
640
|
+
return 2;
|
|
641
|
+
if (signal === 'SIGTERM')
|
|
642
|
+
return 15;
|
|
643
|
+
if (signal === 'SIGKILL')
|
|
644
|
+
return 9;
|
|
645
|
+
return 1;
|
|
646
|
+
}
|
|
647
|
+
function positiveDuration(value, fallback) {
|
|
648
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
649
|
+
}
|
|
650
|
+
function delay(ms) {
|
|
651
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
652
|
+
}
|
|
653
|
+
function isErrno(error, code) {
|
|
654
|
+
return typeof error === 'object' && error !== null && error.code === code;
|
|
655
|
+
}
|