@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.5

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.
@@ -0,0 +1,15 @@
1
+ import { type SelfUpdateRecoveryOptions } from './transaction.js';
2
+ export declare const UNIX_RECOVERY_CONTROLLER_ARG = "--evolver-unix-recovery-controller";
3
+ export interface UnixRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
4
+ argv?: readonly string[];
5
+ processExecPath?: string;
6
+ platform?: NodeJS.Platform;
7
+ confirmationTimeoutMs?: number;
8
+ pollIntervalMs?: number;
9
+ stopTimeoutMs?: number;
10
+ logger?: {
11
+ write(chunk: string): unknown;
12
+ };
13
+ }
14
+ export declare function maybeRunUnixRecoveryController(options?: UnixRecoveryControllerOptions): Promise<number | undefined>;
15
+ export declare function runUnixRecoveryController(options?: UnixRecoveryControllerOptions): Promise<number>;
@@ -0,0 +1,186 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { bindStableUnixRecoveryController, inspectDurableSelfUpdate, recoverDurableSelfUpdate, rollbackDurableSelfUpdate, } from './transaction.js';
3
+ import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
4
+ export const UNIX_RECOVERY_CONTROLLER_ARG = '--evolver-unix-recovery-controller';
5
+ const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
6
+ const DEFAULT_POLL_INTERVAL_MS = 100;
7
+ const DEFAULT_STOP_TIMEOUT_MS = 2_000;
8
+ const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
9
+ const RECOVER_BEFORE_START_STAGES = new Set([
10
+ 'preparing',
11
+ 'downloaded',
12
+ 'verified',
13
+ 'backed_up',
14
+ 'rolling_back',
15
+ 'rollback_pending',
16
+ ]);
17
+ export async function maybeRunUnixRecoveryController(options = {}) {
18
+ const argv = options.argv ?? process.argv.slice(2);
19
+ if (argv.length !== 2 || argv[0] !== 'proxy' || argv[1] !== UNIX_RECOVERY_CONTROLLER_ARG)
20
+ return undefined;
21
+ if ((options.platform ?? process.platform) === 'win32') {
22
+ throw new Error('unix_recovery_controller_unsupported_platform');
23
+ }
24
+ return runUnixRecoveryController(options);
25
+ }
26
+ export async function runUnixRecoveryController(options = {}) {
27
+ const env = options.env ?? process.env;
28
+ const platform = options.platform ?? process.platform;
29
+ const processExecPath = options.processExecPath ?? process.execPath;
30
+ const logger = options.logger ?? process.stderr;
31
+ const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
32
+ const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
33
+ const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
34
+ const transactionOptions = { ...options, env, platform, processExecPath };
35
+ const { targetPath } = await bindStableUnixRecoveryController(transactionOptions, processExecPath);
36
+ let initial = await inspectDurableSelfUpdate(transactionOptions);
37
+ if (initial.outcome === 'blocked' || initial.stage === 'install_pending') {
38
+ logger.write('[evolver-controller] recovery_blocked_before_start\n');
39
+ return 1;
40
+ }
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
+ let child;
50
+ let stoppingSignal;
51
+ const forwardSignal = (signal) => {
52
+ stoppingSignal = signal;
53
+ child?.kill(signal);
54
+ };
55
+ const onSigterm = () => { forwardSignal('SIGTERM'); };
56
+ const onSigint = () => { forwardSignal('SIGINT'); };
57
+ process.once('SIGTERM', onSigterm);
58
+ process.once('SIGINT', onSigint);
59
+ try {
60
+ const launched = spawn(targetPath, ['proxy'], {
61
+ env,
62
+ stdio: 'inherit',
63
+ windowsHide: true,
64
+ });
65
+ child = launched;
66
+ const childExit = waitForNativeExit(launched);
67
+ if (!pendingAtLaunch)
68
+ return exitCode(await childExit);
69
+ const deadline = Date.now() + confirmationTimeoutMs;
70
+ for (;;) {
71
+ const event = await Promise.race([
72
+ childExit.then((exit) => ({ type: 'exit', exit })),
73
+ delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
74
+ .then(() => ({ type: 'poll' })),
75
+ ]);
76
+ if (stoppingSignal) {
77
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
78
+ let rollback;
79
+ try {
80
+ rollback = await rollbackDurableSelfUpdate(transactionOptions, 'controller_stopped_during_health_check');
81
+ }
82
+ catch {
83
+ logger.write('[evolver-controller] rollback_blocked\n');
84
+ return 1;
85
+ }
86
+ if (rollback.outcome !== 'rolled_back' && rollback.outcome !== 'confirmed') {
87
+ logger.write('[evolver-controller] rollback_blocked\n');
88
+ return 1;
89
+ }
90
+ return 128 + signalNumber(stoppingSignal);
91
+ }
92
+ let state;
93
+ try {
94
+ state = await inspectDurableSelfUpdate(transactionOptions);
95
+ }
96
+ catch {
97
+ if (event.type === 'exit' || Date.now() >= deadline) {
98
+ logger.write('[evolver-controller] recovery_state_unavailable\n');
99
+ }
100
+ else {
101
+ continue;
102
+ }
103
+ }
104
+ if (state?.outcome === 'confirmed')
105
+ return exitCode(await childExit);
106
+ if (state?.outcome === 'blocked') {
107
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
108
+ return 1;
109
+ }
110
+ if (state?.outcome === 'rolled_back') {
111
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
112
+ child = spawnTarget(targetPath, env);
113
+ return exitCode(await waitForNativeExit(child));
114
+ }
115
+ if (event.type === 'exit' || Date.now() >= deadline) {
116
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
117
+ const rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
118
+ ? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
119
+ : 'health_confirmation_timeout');
120
+ if (rollback.outcome !== 'rolled_back' && rollback.outcome !== 'confirmed') {
121
+ logger.write('[evolver-controller] rollback_blocked\n');
122
+ return 1;
123
+ }
124
+ if (rollback.outcome === 'confirmed')
125
+ return exitCode(await childExit);
126
+ child = spawnTarget(targetPath, env);
127
+ return exitCode(await waitForNativeExit(child));
128
+ }
129
+ }
130
+ }
131
+ finally {
132
+ process.off('SIGTERM', onSigterm);
133
+ process.off('SIGINT', onSigint);
134
+ }
135
+ }
136
+ function isControllerPending(state) {
137
+ return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
138
+ }
139
+ function spawnTarget(targetPath, env) {
140
+ return spawn(targetPath, ['proxy'], {
141
+ env,
142
+ stdio: 'inherit',
143
+ windowsHide: true,
144
+ });
145
+ }
146
+ function waitForNativeExit(child) {
147
+ return new Promise((resolve) => {
148
+ child.once('error', (error) => { resolve({ code: null, signal: null, error }); });
149
+ child.once('exit', (code, signal) => { resolve({ code, signal }); });
150
+ });
151
+ }
152
+ async function stopNativeChild(child, exit, timeoutMs) {
153
+ if (child.exitCode !== null || child.signalCode !== null)
154
+ return;
155
+ child.kill('SIGTERM');
156
+ const stopped = await Promise.race([
157
+ exit.then(() => true),
158
+ delay(timeoutMs).then(() => false),
159
+ ]);
160
+ if (!stopped) {
161
+ child.kill('SIGKILL');
162
+ await exit;
163
+ }
164
+ }
165
+ function exitCode(exit) {
166
+ if (exit.error)
167
+ return 1;
168
+ if (exit.code !== null)
169
+ return exit.code;
170
+ return exit.signal === null ? 1 : 128 + signalNumber(exit.signal);
171
+ }
172
+ function signalNumber(signal) {
173
+ if (signal === 'SIGINT')
174
+ return 2;
175
+ if (signal === 'SIGTERM')
176
+ return 15;
177
+ if (signal === 'SIGKILL')
178
+ return 9;
179
+ return 1;
180
+ }
181
+ function positiveDuration(value, fallback) {
182
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
183
+ }
184
+ function delay(ms) {
185
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
186
+ }
@@ -1,2 +1,6 @@
1
- /** Resolve EVOLVER's own version. Walks up from this module to the nearest package.json named @evomap/evolver-proxy. */
2
- export declare function getCurrentVersion(): string;
1
+ export interface CurrentVersionOptions {
2
+ startDir?: string;
3
+ buildVersion?: string;
4
+ }
5
+ /** Resolve EVOLVER's version from package/git metadata, then the standalone build-time version. */
6
+ export declare function getCurrentVersion(options?: CurrentVersionOptions): string;
@@ -5,10 +5,12 @@
5
5
  import { dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { util } from '@evomap/evolver-core';
8
- /** Resolve EVOLVER's own version. Walks up from this module to the nearest package.json named @evomap/evolver-proxy. */
9
- export function getCurrentVersion() {
8
+ /** Resolve EVOLVER's version from package/git metadata, then the standalone build-time version. */
9
+ export function getCurrentVersion(options = {}) {
10
+ const buildVersion = (options.buildVersion ?? process.env.EVOLVER_CLI_VERSION)?.trim();
10
11
  return util.resolveRuntimeVersion({
11
- startDir: dirname(fileURLToPath(import.meta.url)),
12
+ startDir: options.startDir ?? dirname(fileURLToPath(import.meta.url)),
12
13
  isPackage: (pkg) => pkg.name === '@evomap/evolver-proxy' || pkg.name === '@evomap/evolver',
14
+ ...(buildVersion && buildVersion !== '0.0.0' ? { fallback: buildVersion } : {}),
13
15
  });
14
16
  }
@@ -0,0 +1,23 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { type SelfUpdateRecoveryOptions } from './transaction.js';
3
+ export declare const WINDOWS_RECOVERY_CONTROLLER_ARG = "--evolver-windows-recovery-controller";
4
+ 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>;
6
+ export interface WindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
7
+ argv?: readonly string[];
8
+ processExecPath?: string;
9
+ platform?: NodeJS.Platform;
10
+ confirmationTimeoutMs?: number;
11
+ pollIntervalMs?: number;
12
+ stopTimeoutMs?: number;
13
+ logger?: {
14
+ write(chunk: string): unknown;
15
+ };
16
+ /** Test-only process adapter; production always uses the fixed target plus the fixed `proxy` argv. */
17
+ spawnTarget?: (targetPath: string, env: NodeJS.ProcessEnv) => ChildProcess;
18
+ /** Test-only worker adapter; production executes the fixed updater path with the fixed worker argv. */
19
+ runUpdaterWorker?: RunUpdaterWorker;
20
+ }
21
+ export declare function maybeRunWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number | undefined>;
22
+ export declare function runWindowsRecoveryController(options?: WindowsRecoveryControllerOptions): Promise<number>;
23
+ export {};
@@ -0,0 +1,274 @@
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 { bindWindowsManagedExecutable, resolveWindowsUpdaterPaths, WINDOWS_UPDATER_WORKER_ARG, } from './windowsUpdater.js';
6
+ export const WINDOWS_RECOVERY_CONTROLLER_ARG = '--evolver-windows-recovery-controller';
7
+ export const WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG = '--evolver-windows-recovery-controller-provision';
8
+ const DEFAULT_CONFIRMATION_TIMEOUT_MS = 30_000;
9
+ const DEFAULT_POLL_INTERVAL_MS = 100;
10
+ const DEFAULT_STOP_TIMEOUT_MS = 2_000;
11
+ const PENDING_CONTROLLER_STAGES = new Set(['installed', 'restarted', 'health_check_pending']);
12
+ const RECOVER_BEFORE_START_STAGES = new Set([
13
+ 'preparing',
14
+ 'downloaded',
15
+ 'verified',
16
+ 'backed_up',
17
+ 'install_pending',
18
+ 'rolling_back',
19
+ 'rollback_pending',
20
+ ]);
21
+ const PENDING_SWAP_JOURNAL_STAGES = new Set([
22
+ 'install_pending',
23
+ 'restarted',
24
+ 'rolling_back',
25
+ 'rollback_pending',
26
+ ]);
27
+ export async function maybeRunWindowsRecoveryController(options = {}) {
28
+ const argv = options.argv ?? process.argv.slice(2);
29
+ const command = argv.length === 2 && argv[0] === 'proxy' ? argv[1] : undefined;
30
+ if (command !== WINDOWS_RECOVERY_CONTROLLER_ARG && command !== WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG) {
31
+ return undefined;
32
+ }
33
+ if ((options.platform ?? process.platform) !== 'win32')
34
+ return 64;
35
+ try {
36
+ if (command === WINDOWS_RECOVERY_CONTROLLER_PROVISION_ARG) {
37
+ await provisionStableWindowsRecoveryController(options, options.processExecPath ?? process.execPath);
38
+ return 0;
39
+ }
40
+ return await runWindowsRecoveryController(options);
41
+ }
42
+ catch {
43
+ return 1;
44
+ }
45
+ }
46
+ export async function runWindowsRecoveryController(options = {}) {
47
+ const env = options.env ?? process.env;
48
+ const platform = options.platform ?? process.platform;
49
+ const processExecPath = options.processExecPath ?? process.execPath;
50
+ const logger = options.logger ?? process.stderr;
51
+ const confirmationTimeoutMs = positiveDuration(options.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
52
+ const pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
53
+ const stopTimeoutMs = positiveDuration(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS);
54
+ const spawnBoundTarget = options.spawnTarget ?? spawnTarget;
55
+ const runBoundUpdaterWorker = options.runUpdaterWorker ?? runUpdaterWorker;
56
+ const bound = await bindStableWindowsRecoveryController({ ...options, env, platform }, processExecPath);
57
+ const transactionOptions = {
58
+ ...options,
59
+ env,
60
+ platform,
61
+ processExecPath,
62
+ stateDir: bound.stateDir,
63
+ targetPath: bound.targetPath,
64
+ beforeJournalMutation: async () => {
65
+ await bindStableWindowsRecoveryController({ ...options, env, platform }, processExecPath);
66
+ },
67
+ };
68
+ let initial = await inspectDurableSelfUpdate(transactionOptions);
69
+ if (initial.outcome === 'blocked') {
70
+ logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
71
+ return 1;
72
+ }
73
+ if (await pendingSwapExists(bound.stateDir)) {
74
+ if (initial.stage === undefined || !PENDING_SWAP_JOURNAL_STAGES.has(initial.stage)) {
75
+ logger.write('[evolver-windows-controller] pending_swap_blocked\n');
76
+ return 1;
77
+ }
78
+ const workerExitCode = await executeBoundUpdaterWorker(runBoundUpdaterWorker, bound.stateDir, env);
79
+ if (workerExitCode !== 0) {
80
+ let rollback;
81
+ try {
82
+ rollback = await rollbackDurableSelfUpdate(transactionOptions, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
83
+ rollback = await completePreparedRollback(rollback, transactionOptions, bound.stateDir, env, runBoundUpdaterWorker);
84
+ }
85
+ catch {
86
+ logger.write('[evolver-windows-controller] pending_swap_blocked\n');
87
+ return 1;
88
+ }
89
+ if (rollback.outcome !== 'rolled_back') {
90
+ logger.write('[evolver-windows-controller] pending_swap_blocked\n');
91
+ return 1;
92
+ }
93
+ return launchRestoredTarget(bound.targetPath, env, spawnBoundTarget);
94
+ }
95
+ initial = await inspectDurableSelfUpdate(transactionOptions);
96
+ }
97
+ if (initial.stage === 'install_pending') {
98
+ initial = await markWindowsInstallApplied(transactionOptions);
99
+ if (initial.outcome === 'blocked') {
100
+ logger.write('[evolver-windows-controller] install_not_applied\n');
101
+ return 1;
102
+ }
103
+ }
104
+ if (initial.outcome === 'blocked') {
105
+ logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
106
+ return 1;
107
+ }
108
+ if (initial.stage !== undefined && RECOVER_BEFORE_START_STAGES.has(initial.stage)) {
109
+ initial = await recoverAndApplyRollback(transactionOptions, bound.stateDir, env, runBoundUpdaterWorker);
110
+ if (initial.outcome === 'blocked' || initial.outcome === 'rollback_pending') {
111
+ logger.write('[evolver-windows-controller] recovery_blocked_before_start\n');
112
+ return 1;
113
+ }
114
+ }
115
+ const pendingAtLaunch = isControllerPending(initial);
116
+ const launched = spawnBoundTarget(bound.targetPath, env);
117
+ const childExit = waitForNativeExit(launched);
118
+ if (!pendingAtLaunch)
119
+ return exitCode(await childExit);
120
+ const deadline = Date.now() + confirmationTimeoutMs;
121
+ for (;;) {
122
+ const event = await Promise.race([
123
+ childExit.then((exit) => ({ type: 'exit', exit })),
124
+ delay(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())))
125
+ .then(() => ({ type: 'poll' })),
126
+ ]);
127
+ let state;
128
+ try {
129
+ state = await inspectDurableSelfUpdate(transactionOptions);
130
+ }
131
+ catch {
132
+ if (event.type === 'exit' || Date.now() >= deadline) {
133
+ logger.write('[evolver-windows-controller] recovery_state_unavailable\n');
134
+ }
135
+ else {
136
+ continue;
137
+ }
138
+ }
139
+ if (state?.outcome === 'confirmed')
140
+ return exitCode(await childExit);
141
+ if (state?.outcome === 'blocked') {
142
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
143
+ return 1;
144
+ }
145
+ if (state?.outcome === 'rolled_back') {
146
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
147
+ return launchRestoredTarget(bound.targetPath, env, spawnBoundTarget);
148
+ }
149
+ if (event.type === 'exit' || Date.now() >= deadline) {
150
+ await stopNativeChild(launched, childExit, stopTimeoutMs);
151
+ let rollback;
152
+ try {
153
+ rollback = await rollbackDurableSelfUpdate(transactionOptions, event.type === 'exit'
154
+ ? SELF_UPDATE_FAILURE_CODES.RESTART_FAILED
155
+ : 'health_confirmation_timeout');
156
+ rollback = await completePreparedRollback(rollback, transactionOptions, bound.stateDir, env, runBoundUpdaterWorker);
157
+ }
158
+ catch {
159
+ logger.write('[evolver-windows-controller] rollback_blocked\n');
160
+ return 1;
161
+ }
162
+ if (rollback.outcome === 'confirmed')
163
+ return exitCode(await childExit);
164
+ if (rollback.outcome !== 'rolled_back') {
165
+ logger.write('[evolver-windows-controller] rollback_blocked\n');
166
+ return 1;
167
+ }
168
+ return launchRestoredTarget(bound.targetPath, env, spawnBoundTarget);
169
+ }
170
+ }
171
+ }
172
+ async function recoverAndApplyRollback(options, stateDir, env, runBoundUpdaterWorker) {
173
+ const recovery = await recoverDurableSelfUpdate(options);
174
+ return completePreparedRollback(recovery, options, stateDir, env, runBoundUpdaterWorker);
175
+ }
176
+ async function completePreparedRollback(recovery, options, stateDir, env, runBoundUpdaterWorker) {
177
+ if (recovery.outcome !== 'rollback_pending')
178
+ return recovery;
179
+ const workerExitCode = await executeBoundUpdaterWorker(runBoundUpdaterWorker, stateDir, env);
180
+ if (workerExitCode !== 0) {
181
+ return { ...recovery, outcome: 'blocked', failureCode: 'rollback_apply_failed' };
182
+ }
183
+ return recoverDurableSelfUpdate(options);
184
+ }
185
+ async function pendingSwapExists(stateDir) {
186
+ const pendingPath = resolveWindowsUpdaterPaths(stateDir).pendingPath;
187
+ try {
188
+ const info = await lstat(pendingPath);
189
+ if (info.isSymbolicLink() || !info.isFile())
190
+ throw new Error('windows_controller_pending_unsafe');
191
+ return true;
192
+ }
193
+ catch (error) {
194
+ if (isErrno(error, 'ENOENT'))
195
+ return false;
196
+ throw error;
197
+ }
198
+ }
199
+ function isControllerPending(state) {
200
+ return state.stage !== undefined && PENDING_CONTROLLER_STAGES.has(state.stage);
201
+ }
202
+ function spawnTarget(targetPath, env) {
203
+ return spawn(targetPath, ['proxy'], {
204
+ env,
205
+ stdio: 'inherit',
206
+ windowsHide: true,
207
+ });
208
+ }
209
+ async function runUpdaterWorker(workerPath, _stateDir, env) {
210
+ return exitCode(await waitForNativeExit(spawn(workerPath, ['proxy', WINDOWS_UPDATER_WORKER_ARG], {
211
+ env,
212
+ stdio: 'inherit',
213
+ windowsHide: true,
214
+ })));
215
+ }
216
+ async function executeBoundUpdaterWorker(runner, stateDir, env) {
217
+ const paths = resolveWindowsUpdaterPaths(stateDir);
218
+ const bound = await bindWindowsManagedExecutable({
219
+ stateDir,
220
+ executablePath: paths.helperPath,
221
+ relativePath: ['windows-updater', 'updater.exe'],
222
+ label: 'controller_worker',
223
+ platform: 'win32',
224
+ });
225
+ const workerEnv = { ...env, EVOLVER_SELF_UPDATE_STATE_DIR: bound.stateDir };
226
+ return runner(bound.executablePath, bound.stateDir, workerEnv);
227
+ }
228
+ async function launchRestoredTarget(targetPath, env, spawnBoundTarget) {
229
+ return exitCode(await waitForNativeExit(spawnBoundTarget(targetPath, env)));
230
+ }
231
+ function waitForNativeExit(child) {
232
+ return new Promise((resolve) => {
233
+ child.once('error', (error) => { resolve({ code: null, signal: null, error }); });
234
+ child.once('exit', (code, signal) => { resolve({ code, signal }); });
235
+ });
236
+ }
237
+ async function stopNativeChild(child, exit, timeoutMs) {
238
+ if (child.exitCode !== null || child.signalCode !== null)
239
+ return;
240
+ child.kill('SIGTERM');
241
+ const stopped = await Promise.race([
242
+ exit.then(() => true),
243
+ delay(timeoutMs).then(() => false),
244
+ ]);
245
+ if (!stopped) {
246
+ child.kill('SIGKILL');
247
+ await exit;
248
+ }
249
+ }
250
+ function exitCode(exit) {
251
+ if (exit.error)
252
+ return 1;
253
+ if (exit.code !== null)
254
+ return exit.code;
255
+ return exit.signal === null ? 1 : 128 + signalNumber(exit.signal);
256
+ }
257
+ function signalNumber(signal) {
258
+ if (signal === 'SIGINT')
259
+ return 2;
260
+ if (signal === 'SIGTERM')
261
+ return 15;
262
+ if (signal === 'SIGKILL')
263
+ return 9;
264
+ return 1;
265
+ }
266
+ function positiveDuration(value, fallback) {
267
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
268
+ }
269
+ function delay(ms) {
270
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
271
+ }
272
+ function isErrno(error, code) {
273
+ return typeof error === 'object' && error !== null && error.code === code;
274
+ }
@@ -0,0 +1,79 @@
1
+ import { rename } from 'node:fs/promises';
2
+ export declare const WINDOWS_UPDATER_WORKER_ARG = "--evolver-windows-updater-worker";
3
+ export type WindowsUpdaterOperation = 'install' | 'rollback';
4
+ export interface WindowsUpdaterPaths {
5
+ directory: string;
6
+ helperPath: string;
7
+ pendingPath: string;
8
+ resultPath: string;
9
+ }
10
+ export interface PrepareWindowsUpdaterOptions {
11
+ operation: WindowsUpdaterOperation;
12
+ targetPath: string;
13
+ backupPath: string;
14
+ stateDir: string;
15
+ /** Required for install and bound to the signed-manifest digest. */
16
+ stagedPath?: string;
17
+ expectedStagedSha256?: string;
18
+ /** Rollback copies the currently running helper-capable binary by default. */
19
+ helperSourcePath?: string;
20
+ processExecPath?: string;
21
+ platform?: NodeJS.Platform;
22
+ }
23
+ export interface WindowsUpdaterDescriptor {
24
+ operation: WindowsUpdaterOperation;
25
+ operationId: string;
26
+ helperPath: string;
27
+ pendingPath: string;
28
+ resultPath: string;
29
+ }
30
+ export interface WindowsUpdaterResult {
31
+ schema_version: 1;
32
+ operation: WindowsUpdaterOperation;
33
+ status: 'completed' | 'failed';
34
+ failure_code?: string;
35
+ }
36
+ export interface ApplyWindowsUpdaterOptions {
37
+ stateDir?: string;
38
+ workerExecPath?: string;
39
+ platform?: NodeJS.Platform;
40
+ renameFn?: typeof rename;
41
+ }
42
+ export interface BindWindowsManagedExecutableOptions {
43
+ stateDir: string;
44
+ executablePath: string;
45
+ relativePath: readonly string[];
46
+ label: string;
47
+ mismatchLabel?: string;
48
+ platform?: NodeJS.Platform;
49
+ }
50
+ export interface BoundWindowsManagedExecutable {
51
+ stateDir: string;
52
+ executablePath: string;
53
+ }
54
+ /** Paths are fixed so the stable controller never consumes descriptor-supplied executable paths. */
55
+ export declare function resolveWindowsUpdaterPaths(stateDirInput: string): WindowsUpdaterPaths;
56
+ /** Bind a fixed executable below the private state root without following directory links. */
57
+ export declare function bindWindowsManagedExecutable(options: BindWindowsManagedExecutableOptions): Promise<BoundWindowsManagedExecutable>;
58
+ /**
59
+ * Prepare a launcher-consumed update descriptor. This function never mutates
60
+ * the live executable and never spawns a competing relaunch process.
61
+ *
62
+ * The stable lifecycle controller runs updater.exe before it starts target.
63
+ * The worker applies pending.json while no target process exists, then removes
64
+ * pending.json only after an idempotently durable success result is written.
65
+ */
66
+ export declare function prepareWindowsExecutableSwap(options: PrepareWindowsUpdaterOptions): Promise<WindowsUpdaterDescriptor>;
67
+ /**
68
+ * Apply the pending operation before the stable controller starts target.
69
+ * A successful swap is idempotent across crashes after rename: if target
70
+ * already has source's content, the helper only finalizes result/pending state.
71
+ */
72
+ export declare function applyPendingWindowsExecutableSwap(options?: ApplyWindowsUpdaterOptions): Promise<WindowsUpdaterResult>;
73
+ /** Return undefined for normal execution, otherwise the launcher helper exit code. */
74
+ export declare function maybeRunWindowsUpdaterWorkerFromArgv(options?: {
75
+ argv?: readonly string[];
76
+ env?: NodeJS.ProcessEnv;
77
+ platform?: NodeJS.Platform;
78
+ processExecPath?: string;
79
+ }): Promise<number | undefined>;