@evomap/evolver-proxy 2.0.0-beta.19 → 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-proxy.d.ts +12 -0
- package/dist/bin/evolver-proxy.js +257 -56
- package/dist/daemon/proxyDaemon.d.ts +12 -0
- package/dist/daemon/proxyDaemon.js +313 -18
- package/dist/daemon/systemdNotifier.d.ts +2 -0
- package/dist/daemon/systemdNotifier.js +11 -1
- package/dist/llm/upstream.js +54 -7
- package/dist/private/accountAssetCompatibility.d.ts +1 -0
- package/dist/private/accountAssetCompatibility.js +3 -3
- package/dist/private/adapterLoader.js +4 -3
- 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 +18 -7
- package/dist/selfUpdate/executor.js +159 -59
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/index.d.ts +2 -1
- package/dist/selfUpdate/index.js +2 -1
- 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 +3 -0
- package/dist/selfUpdate/releaseBinary.js +50 -4
- package/dist/selfUpdate/transaction.d.ts +8 -0
- package/dist/selfUpdate/transaction.js +166 -18
- package/dist/selfUpdate/unixController.d.ts +8 -0
- package/dist/selfUpdate/unixController.js +366 -38
- package/dist/selfUpdate/windowsController.d.ts +14 -2
- package/dist/selfUpdate/windowsController.js +484 -103
- package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
- package/dist/selfUpdate/windowsUpdater.js +174 -7
- package/package.json +3 -3
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { bindStableUnixRecoveryController, inspectDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from './transaction.js';
|
|
3
3
|
import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
|
|
4
|
+
import { resolveRecoveryControllerAuthority, } from './controllerLifecycleAuthority.js';
|
|
5
|
+
import { DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS, deliverRecoveryChildStartGate, RECOVERY_CHILD_START_GATE_ENV, } from './recoveryChildStartGate.js';
|
|
4
6
|
export const UNIX_RECOVERY_CONTROLLER_ARG = '--evolver-unix-recovery-controller';
|
|
5
7
|
const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
|
|
6
8
|
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
7
9
|
const DEFAULT_STOP_TIMEOUT_MS = 2_000;
|
|
10
|
+
// The child performs up to three native owner-identity probes before acknowledging. On Windows
|
|
11
|
+
// each PowerShell-backed probe has its own 15s budget, so keep enough headroom for a loaded host.
|
|
12
|
+
const DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS = 120_000;
|
|
8
13
|
const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
|
|
9
14
|
const RECOVER_BEFORE_START_STAGES = new Set([
|
|
10
15
|
'preparing',
|
|
@@ -31,23 +36,35 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
31
36
|
const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
|
|
32
37
|
const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
|
|
33
38
|
const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const startupGateTimeoutMs = positiveDuration(options.startupGateTimeoutMs, DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS);
|
|
40
|
+
const startupAttestationTimeoutMs = positiveDuration(options.startupAttestationTimeoutMs, DEFAULT_STARTUP_ATTESTATION_TIMEOUT_MS);
|
|
41
|
+
const spawnBoundTarget = options.spawnTarget ?? spawnTarget;
|
|
42
|
+
let authority;
|
|
43
|
+
const baseTransactionOptions = {
|
|
44
|
+
...options,
|
|
45
|
+
env,
|
|
46
|
+
platform,
|
|
47
|
+
processExecPath,
|
|
48
|
+
};
|
|
49
|
+
const bound = await bindStableUnixRecoveryController(baseTransactionOptions, processExecPath);
|
|
50
|
+
const transactionOptions = {
|
|
51
|
+
...baseTransactionOptions,
|
|
52
|
+
beforeJournalMutation: async () => {
|
|
53
|
+
assertOwnedRecoveryAuthority(authority);
|
|
54
|
+
await options.beforeJournalMutation?.();
|
|
55
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
56
|
+
assertOwnedRecoveryAuthority(authority);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const observed = await inspectDurableSelfUpdate(transactionOptions);
|
|
60
|
+
if (observed.outcome === 'blocked' || observed.stage === 'install_pending') {
|
|
38
61
|
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
39
62
|
return 1;
|
|
40
63
|
}
|
|
41
|
-
if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
|
|
42
|
-
initial = await recoverDurableSelfUpdate(transactionOptions);
|
|
43
|
-
if (initial.outcome === 'blocked') {
|
|
44
|
-
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const pendingAtLaunch = isControllerPending(initial);
|
|
49
64
|
let child;
|
|
65
|
+
let childExit;
|
|
50
66
|
let stoppingSignal;
|
|
67
|
+
let releaseAttempted = false;
|
|
51
68
|
const forwardSignal = (signal) => {
|
|
52
69
|
stoppingSignal = signal;
|
|
53
70
|
child?.kill(signal);
|
|
@@ -56,16 +73,200 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
56
73
|
const onSigint = () => { forwardSignal('SIGINT'); };
|
|
57
74
|
process.once('SIGTERM', onSigterm);
|
|
58
75
|
process.once('SIGINT', onSigint);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
const guardUnconfirmedChild = async (_target, exit) => {
|
|
77
|
+
logger.write('[evolver-controller] child_termination_unconfirmed\n');
|
|
78
|
+
if (authority?.kind === 'owned') {
|
|
79
|
+
try {
|
|
80
|
+
authority.retainProcess();
|
|
81
|
+
logger.write('[evolver-controller] child_guardian_active\n');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
logger.write('[evolver-controller] child_guardian_retain_failed\n');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Without a verifiable PID generation there is no safe lock handoff. Keep this live owner
|
|
89
|
+
// until exact exit instead of returning a dead-owner lock that another process could reclaim.
|
|
90
|
+
await exit;
|
|
91
|
+
};
|
|
92
|
+
const armBoundChild = (target) => {
|
|
93
|
+
if (authority?.kind !== 'owned')
|
|
94
|
+
return true;
|
|
95
|
+
if (target.pid === undefined) {
|
|
96
|
+
logger.write('[evolver-controller] child_guardian_arm_failed\n');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
authority.armProcess(target.pid);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
logger.write('[evolver-controller] child_guardian_arm_failed\n');
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const disarmBoundChild = () => {
|
|
109
|
+
if (authority?.kind === 'owned')
|
|
110
|
+
authority.disarmProcess();
|
|
111
|
+
};
|
|
112
|
+
const stopBoundChild = async (target, exit) => {
|
|
113
|
+
const stopped = await stopNativeChild(target, exit, stopTimeoutMs);
|
|
114
|
+
if (!stopped) {
|
|
115
|
+
await guardUnconfirmedChild(target, exit);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
disarmBoundChild();
|
|
119
|
+
}
|
|
120
|
+
return stopped;
|
|
121
|
+
};
|
|
122
|
+
const releaseAuthority = async () => {
|
|
123
|
+
if (!authority || releaseAttempted)
|
|
124
|
+
return true;
|
|
125
|
+
releaseAttempted = true;
|
|
126
|
+
try {
|
|
127
|
+
authority.release();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
logger.write('[evolver-controller] lifecycle_authority_release_failed\n');
|
|
132
|
+
if (child && childExit)
|
|
133
|
+
await stopBoundChild(child, childExit);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const launchTerminalTarget = async () => {
|
|
138
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
139
|
+
if (stoppingSignal) {
|
|
140
|
+
if (!await releaseAuthority())
|
|
141
|
+
return 1;
|
|
142
|
+
return 128 + signalNumber(stoppingSignal);
|
|
143
|
+
}
|
|
144
|
+
authority.assertAuthorized();
|
|
145
|
+
const prepared = authority.prepareTarget(env);
|
|
146
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
65
147
|
child = launched;
|
|
66
|
-
|
|
148
|
+
childExit = waitForNativeExit(launched);
|
|
149
|
+
const spawned = waitForNativeSpawn(launched);
|
|
150
|
+
// Node publishes the native PID synchronously when spawn succeeds. Arm the guardian before
|
|
151
|
+
// the first await so a hard controller crash cannot expose a stale-reclaimable parent lock.
|
|
152
|
+
if (!armBoundChild(launched)) {
|
|
153
|
+
if (await spawned)
|
|
154
|
+
await stopBoundChild(launched, childExit);
|
|
155
|
+
await releaseAuthority();
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
if (!await spawned) {
|
|
159
|
+
disarmBoundChild();
|
|
160
|
+
await releaseAuthority();
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
if (stoppingSignal) {
|
|
164
|
+
if (!await stopBoundChild(launched, childExit))
|
|
165
|
+
return 1;
|
|
166
|
+
if (!await releaseAuthority())
|
|
167
|
+
return 1;
|
|
168
|
+
return 128 + signalNumber(stoppingSignal);
|
|
169
|
+
}
|
|
170
|
+
authority.assertAuthorized();
|
|
171
|
+
if (!await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs)) {
|
|
172
|
+
logger.write('[evolver-controller] startup_gate_failed\n');
|
|
173
|
+
await stopBoundChild(launched, childExit);
|
|
174
|
+
await releaseAuthority();
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
if (prepared.startupAckToken
|
|
178
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs)) {
|
|
179
|
+
logger.write('[evolver-controller] startup_attestation_failed\n');
|
|
180
|
+
await stopBoundChild(launched, childExit);
|
|
181
|
+
await releaseAuthority();
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
if (!await releaseAuthority())
|
|
185
|
+
return 1;
|
|
186
|
+
return exitCode(await childExit);
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
authority = await resolveRecoveryControllerAuthority(env, options.lifecycleAuthority);
|
|
190
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
191
|
+
let initial = await inspectDurableSelfUpdate(transactionOptions);
|
|
192
|
+
if (initial.outcome === 'blocked' || initial.stage === 'install_pending') {
|
|
193
|
+
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
if (authority.kind === 'delegated' && !stateAllowsDelegatedSpawn(initial)) {
|
|
197
|
+
logger.write('[evolver-controller] lifecycle_authority_blocked\n');
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
if (stoppingSignal) {
|
|
201
|
+
if (!await releaseAuthority())
|
|
202
|
+
return 1;
|
|
203
|
+
return 128 + signalNumber(stoppingSignal);
|
|
204
|
+
}
|
|
205
|
+
if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
|
|
206
|
+
assertOwnedRecoveryAuthority(authority);
|
|
207
|
+
initial = await recoverDurableSelfUpdate(transactionOptions);
|
|
208
|
+
if (initial.outcome === 'blocked') {
|
|
209
|
+
logger.write('[evolver-controller] recovery_blocked_before_start\n');
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const pendingAtLaunch = isControllerPending(initial);
|
|
67
214
|
if (!pendingAtLaunch)
|
|
68
|
-
return
|
|
215
|
+
return await launchTerminalTarget();
|
|
216
|
+
await revalidateBoundUnixController(baseTransactionOptions, processExecPath, bound, authority);
|
|
217
|
+
if (stoppingSignal) {
|
|
218
|
+
if (!await releaseAuthority())
|
|
219
|
+
return 1;
|
|
220
|
+
return 128 + signalNumber(stoppingSignal);
|
|
221
|
+
}
|
|
222
|
+
assertOwnedRecoveryAuthority(authority);
|
|
223
|
+
const prepared = authority.prepareTarget(env);
|
|
224
|
+
const launched = spawnBoundTarget(bound.targetPath, prepared.env, prepared.startupAckToken !== undefined);
|
|
225
|
+
child = launched;
|
|
226
|
+
childExit = waitForNativeExit(launched);
|
|
227
|
+
const spawned = waitForNativeSpawn(launched);
|
|
228
|
+
// Arm before yielding for the same hard-crash boundary as terminal launches.
|
|
229
|
+
if (!armBoundChild(launched)) {
|
|
230
|
+
if (await spawned)
|
|
231
|
+
await stopBoundChild(launched, childExit);
|
|
232
|
+
await releaseAuthority();
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
if (!await spawned) {
|
|
236
|
+
disarmBoundChild();
|
|
237
|
+
await releaseAuthority();
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
if (stoppingSignal) {
|
|
241
|
+
if (!await stopBoundChild(launched, childExit))
|
|
242
|
+
return 1;
|
|
243
|
+
if (!await releaseAuthority())
|
|
244
|
+
return 1;
|
|
245
|
+
return 128 + signalNumber(stoppingSignal);
|
|
246
|
+
}
|
|
247
|
+
authority.assertAuthorized();
|
|
248
|
+
const startupGateAccepted = await deliverRecoveryChildStartGate(launched, prepared.startupGateToken, startupGateTimeoutMs);
|
|
249
|
+
if (!startupGateAccepted) {
|
|
250
|
+
logger.write('[evolver-controller] startup_gate_failed\n');
|
|
251
|
+
}
|
|
252
|
+
if (!startupGateAccepted || (prepared.startupAckToken
|
|
253
|
+
&& !await waitForStartupAttestation(launched, prepared.startupAckToken, startupAttestationTimeoutMs))) {
|
|
254
|
+
if (!await stopBoundChild(launched, childExit))
|
|
255
|
+
return 1;
|
|
256
|
+
const rollback = await rollbackDurableSelfUpdate(transactionOptions, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
|
|
257
|
+
if (rollback.outcome === 'confirmed') {
|
|
258
|
+
if (!await releaseAuthority())
|
|
259
|
+
return 1;
|
|
260
|
+
return exitCode(await childExit);
|
|
261
|
+
}
|
|
262
|
+
if (rollback.outcome !== 'rolled_back') {
|
|
263
|
+
logger.write('[evolver-controller] startup_attestation_failed\n');
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
child = undefined;
|
|
267
|
+
childExit = undefined;
|
|
268
|
+
return await launchTerminalTarget();
|
|
269
|
+
}
|
|
69
270
|
const deadline = Date.now() + confirmationTimeoutMs;
|
|
70
271
|
for (;;) {
|
|
71
272
|
const event = await Promise.race([
|
|
@@ -73,8 +274,10 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
73
274
|
delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
|
|
74
275
|
.then(() => ({ type: 'poll' })),
|
|
75
276
|
]);
|
|
277
|
+
assertOwnedRecoveryAuthority(authority);
|
|
76
278
|
if (stoppingSignal) {
|
|
77
|
-
await
|
|
279
|
+
if (!await stopBoundChild(launched, childExit))
|
|
280
|
+
return 1;
|
|
78
281
|
let rollback;
|
|
79
282
|
try {
|
|
80
283
|
rollback = await rollbackDurableSelfUpdate(transactionOptions, 'controller_stopped_during_health_check');
|
|
@@ -87,6 +290,8 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
87
290
|
logger.write('[evolver-controller] rollback_blocked\n');
|
|
88
291
|
return 1;
|
|
89
292
|
}
|
|
293
|
+
if (!await releaseAuthority())
|
|
294
|
+
return 1;
|
|
90
295
|
return 128 + signalNumber(stoppingSignal);
|
|
91
296
|
}
|
|
92
297
|
let state;
|
|
@@ -101,19 +306,25 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
101
306
|
continue;
|
|
102
307
|
}
|
|
103
308
|
}
|
|
104
|
-
if (state?.outcome === 'confirmed')
|
|
309
|
+
if (state?.outcome === 'confirmed') {
|
|
310
|
+
if (!await releaseAuthority())
|
|
311
|
+
return 1;
|
|
105
312
|
return exitCode(await childExit);
|
|
313
|
+
}
|
|
106
314
|
if (state?.outcome === 'blocked') {
|
|
107
|
-
await
|
|
315
|
+
await stopBoundChild(launched, childExit);
|
|
108
316
|
return 1;
|
|
109
317
|
}
|
|
110
318
|
if (state?.outcome === 'rolled_back') {
|
|
111
|
-
await
|
|
112
|
-
|
|
113
|
-
|
|
319
|
+
if (!await stopBoundChild(launched, childExit))
|
|
320
|
+
return 1;
|
|
321
|
+
child = undefined;
|
|
322
|
+
childExit = undefined;
|
|
323
|
+
return await launchTerminalTarget();
|
|
114
324
|
}
|
|
115
325
|
if (event.type === 'exit' || Date.now() >= deadline) {
|
|
116
|
-
await
|
|
326
|
+
if (!await stopBoundChild(launched, childExit))
|
|
327
|
+
return 1;
|
|
117
328
|
const rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
|
|
118
329
|
? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
|
|
119
330
|
: 'health_confirmation_timeout');
|
|
@@ -121,46 +332,163 @@ export async function runUnixRecoveryController(options = {}) {
|
|
|
121
332
|
logger.write('[evolver-controller] rollback_blocked\n');
|
|
122
333
|
return 1;
|
|
123
334
|
}
|
|
124
|
-
if (rollback.outcome === 'confirmed')
|
|
335
|
+
if (rollback.outcome === 'confirmed') {
|
|
336
|
+
if (!await releaseAuthority())
|
|
337
|
+
return 1;
|
|
125
338
|
return exitCode(await childExit);
|
|
126
|
-
|
|
127
|
-
|
|
339
|
+
}
|
|
340
|
+
child = undefined;
|
|
341
|
+
childExit = undefined;
|
|
342
|
+
return await launchTerminalTarget();
|
|
128
343
|
}
|
|
129
344
|
}
|
|
130
345
|
}
|
|
346
|
+
catch {
|
|
347
|
+
logger.write('[evolver-controller] lifecycle_authority_blocked\n');
|
|
348
|
+
if (child && childExit)
|
|
349
|
+
await stopBoundChild(child, childExit);
|
|
350
|
+
await releaseAuthority();
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
131
353
|
finally {
|
|
132
354
|
process.off('SIGTERM', onSigterm);
|
|
133
355
|
process.off('SIGINT', onSigint);
|
|
356
|
+
if (!releaseAttempted)
|
|
357
|
+
await releaseAuthority();
|
|
134
358
|
}
|
|
135
359
|
}
|
|
136
360
|
function isControllerPending(state) {
|
|
137
361
|
return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
|
|
138
362
|
}
|
|
139
|
-
function
|
|
363
|
+
function stateAllowsDelegatedSpawn(state) {
|
|
364
|
+
return state.outcome === 'none'
|
|
365
|
+
|| state.outcome === 'confirmed'
|
|
366
|
+
|| state.outcome === 'rolled_back';
|
|
367
|
+
}
|
|
368
|
+
function assertOwnedRecoveryAuthority(authority) {
|
|
369
|
+
if (authority?.kind !== 'owned') {
|
|
370
|
+
throw new Error('self_update_recovery_controller_owner_lease_required');
|
|
371
|
+
}
|
|
372
|
+
authority.assertAuthorized();
|
|
373
|
+
}
|
|
374
|
+
async function revalidateBoundUnixController(options, processExecPath, expected, authority) {
|
|
375
|
+
authority.assertAuthorized();
|
|
376
|
+
const actual = await bindStableUnixRecoveryController(options, processExecPath);
|
|
377
|
+
if (actual.controllerPath !== expected.controllerPath || actual.targetPath !== expected.targetPath) {
|
|
378
|
+
throw new Error('self_update_unix_recovery_controller_binding_changed');
|
|
379
|
+
}
|
|
380
|
+
authority.assertAuthorized();
|
|
381
|
+
}
|
|
382
|
+
function spawnTarget(targetPath, env, startupAttestation) {
|
|
383
|
+
const startupGate = env[RECOVERY_CHILD_START_GATE_ENV] !== undefined;
|
|
140
384
|
return spawn(targetPath, ['proxy'], {
|
|
141
385
|
env,
|
|
142
|
-
stdio:
|
|
386
|
+
stdio: startupGate
|
|
387
|
+
? ['inherit', 'inherit', 'inherit', startupAttestation ? 'pipe' : 'ignore', 'pipe']
|
|
388
|
+
: startupAttestation
|
|
389
|
+
? ['inherit', 'inherit', 'inherit', 'pipe']
|
|
390
|
+
: 'inherit',
|
|
143
391
|
windowsHide: true,
|
|
144
392
|
});
|
|
145
393
|
}
|
|
146
394
|
function waitForNativeExit(child) {
|
|
147
395
|
return new Promise((resolve) => {
|
|
148
|
-
|
|
149
|
-
|
|
396
|
+
let spawned = child.pid !== undefined;
|
|
397
|
+
const cleanup = () => {
|
|
398
|
+
child.off('spawn', onSpawn);
|
|
399
|
+
child.off('error', onError);
|
|
400
|
+
};
|
|
401
|
+
const onSpawn = () => { spawned = true; };
|
|
402
|
+
const onError = (error) => {
|
|
403
|
+
// A post-spawn process error does not prove that the native child exited.
|
|
404
|
+
if (spawned)
|
|
405
|
+
return;
|
|
406
|
+
cleanup();
|
|
407
|
+
resolve({ code: null, signal: null, error });
|
|
408
|
+
};
|
|
409
|
+
child.once('spawn', onSpawn);
|
|
410
|
+
child.on('error', onError);
|
|
411
|
+
child.once('exit', (code, signal) => {
|
|
412
|
+
cleanup();
|
|
413
|
+
resolve({ code, signal });
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function waitForNativeSpawn(child) {
|
|
418
|
+
if (child.pid !== undefined)
|
|
419
|
+
return Promise.resolve(true);
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const finish = (spawned) => {
|
|
422
|
+
child.off('spawn', onSpawn);
|
|
423
|
+
child.off('error', onError);
|
|
424
|
+
resolve(spawned);
|
|
425
|
+
};
|
|
426
|
+
const onSpawn = () => { finish(true); };
|
|
427
|
+
const onError = () => { finish(false); };
|
|
428
|
+
child.once('spawn', onSpawn);
|
|
429
|
+
child.once('error', onError);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function waitForStartupAttestation(child, token, timeoutMs) {
|
|
433
|
+
const stream = child.stdio[3];
|
|
434
|
+
if (!stream || typeof stream.on !== 'function')
|
|
435
|
+
return Promise.resolve(false);
|
|
436
|
+
const readable = stream;
|
|
437
|
+
const expected = `${token}\n`;
|
|
438
|
+
return new Promise((resolve) => {
|
|
439
|
+
let raw = '';
|
|
440
|
+
let settled = false;
|
|
441
|
+
const finish = (result) => {
|
|
442
|
+
if (settled)
|
|
443
|
+
return;
|
|
444
|
+
settled = true;
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
readable.off('data', onData);
|
|
447
|
+
readable.off('end', onEnd);
|
|
448
|
+
// Keep the error listener attached after timeout. stopNativeChild may close
|
|
449
|
+
// the child pipe with ECONNRESET; removing it here turns expected cleanup
|
|
450
|
+
// into an unhandled error in the test runner and in long-lived controllers.
|
|
451
|
+
resolve(result);
|
|
452
|
+
};
|
|
453
|
+
const onData = (chunk) => {
|
|
454
|
+
raw += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
455
|
+
if (Buffer.byteLength(raw, 'utf8') > 128 || !expected.startsWith(raw))
|
|
456
|
+
finish(false);
|
|
457
|
+
};
|
|
458
|
+
const onEnd = () => { finish(raw === expected); };
|
|
459
|
+
const onError = () => { finish(false); };
|
|
460
|
+
const timer = setTimeout(() => { finish(false); }, timeoutMs);
|
|
461
|
+
readable.on('data', onData);
|
|
462
|
+
readable.once('end', onEnd);
|
|
463
|
+
readable.once('error', onError);
|
|
150
464
|
});
|
|
151
465
|
}
|
|
152
466
|
async function stopNativeChild(child, exit, timeoutMs) {
|
|
153
467
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
154
|
-
return;
|
|
155
|
-
|
|
468
|
+
return true;
|
|
469
|
+
try {
|
|
470
|
+
child.kill('SIGTERM');
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// A failed signal is not an exit; the bounded native-exit wait remains authoritative.
|
|
474
|
+
}
|
|
156
475
|
const stopped = await Promise.race([
|
|
157
476
|
exit.then(() => true),
|
|
158
477
|
delay(timeoutMs).then(() => false),
|
|
159
478
|
]);
|
|
160
479
|
if (!stopped) {
|
|
161
|
-
|
|
162
|
-
|
|
480
|
+
try {
|
|
481
|
+
child.kill('SIGKILL');
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// A failed signal is not an exit; the second bounded wait still protects authority.
|
|
485
|
+
}
|
|
486
|
+
return Promise.race([
|
|
487
|
+
exit.then(() => true),
|
|
488
|
+
delay(timeoutMs).then(() => false),
|
|
489
|
+
]);
|
|
163
490
|
}
|
|
491
|
+
return true;
|
|
164
492
|
}
|
|
165
493
|
function exitCode(exit) {
|
|
166
494
|
if (exit.error)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type ChildProcess } from 'node:child_process';
|
|
2
2
|
import { type SelfUpdateRecoveryOptions } from './transaction.js';
|
|
3
|
+
import { type WindowsUpdaterHelperTrust } from './windowsUpdater.js';
|
|
4
|
+
import { type RecoveryControllerAuthorityDependencies } from './controllerLifecycleAuthority.js';
|
|
3
5
|
export declare const WINDOWS_RECOVERY_CONTROLLER_ARG = "--evolver-windows-recovery-controller";
|
|
4
6
|
export declare const WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG = "--evolver-windows-recovery-controller-provision";
|
|
5
|
-
type RunUpdaterWorker = (workerPath: string, stateDir: string, env: NodeJS.ProcessEnv) => Promise<number>;
|
|
7
|
+
type RunUpdaterWorker = (workerPath: string, stateDir: string, env: NodeJS.ProcessEnv, startupGateToken: string) => Promise<number>;
|
|
8
|
+
type SpawnUpdaterWorker = (workerPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
|
|
6
9
|
export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
|
|
7
10
|
argv?: readonly string[];
|
|
8
11
|
processExecPath?: string;
|
|
@@ -10,13 +13,22 @@ export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOpti
|
|
|
10
13
|
confirmationTimeoutMs?: number;
|
|
11
14
|
pollIntervalMs?: number;
|
|
12
15
|
stopTimeoutMs?: number;
|
|
16
|
+
workerTimeoutMs?: number;
|
|
17
|
+
startupGateTimeoutMs?: number;
|
|
18
|
+
startupAttestationTimeoutMs?: number;
|
|
13
19
|
logger?: {
|
|
14
20
|
write(chunk: string): unknown;
|
|
15
21
|
};
|
|
16
22
|
/** Test-only process adapter; production always uses the fixed target plus the fixed `proxy` argv. */
|
|
17
|
-
spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
|
|
23
|
+
spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv, startupAttestation: boolean) => ChildProcess;
|
|
18
24
|
/** Test-only worker adapter; production executes the fixed updater path with the fixed worker argv. */
|
|
19
25
|
runUpdaterWorker?: RunUpdaterWorker;
|
|
26
|
+
/** Test-only native spawn seam; production still runs the fixed updater path and argv. */
|
|
27
|
+
spawnUpdaterWorker?: SpawnUpdaterWorker;
|
|
28
|
+
/** Test seam; production always evaluates the native Windows updater owner/writer policy. */
|
|
29
|
+
assertUpdaterHelperTrust?: WindowsUpdaterHelperTrust;
|
|
30
|
+
/** Test seam for exact lifecycle-owner and supervised-activation authority. */
|
|
31
|
+
lifecycleAuthority?: RecoveryControllerAuthorityDependencies;
|
|
20
32
|
}
|
|
21
33
|
export declare function maybeRunWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number | undefined>;
|
|
22
34
|
export declare function runWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number>;
|