@camstack/server 1.2.94 → 1.2.95
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/agent/addon-scan.js +131 -0
- package/dist/agent/main.js +35 -40
- package/dist/api/core/cap-providers.js +53 -9
- package/dist/core/addon/addon-package.service.js +17 -2
- package/package.json +14 -14
- package/dist/agent/builtins-seed.js +0 -89
- package/dist/api/addons-custom.router.js +0 -99
- package/dist/api/core/bulk-update-coordinator.js +0 -229
- package/dist/api/core/settings-backend.router.js +0 -121
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/core/addon/framework-live-sync.js +0 -344
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
- package/dist/server-root/boot-plan.js +0 -110
- package/dist/server-root/semver-compare.js +0 -45
- package/dist/server-root/server-root-state.js +0 -220
- package/dist/server-root/workspace-detect.js +0 -73
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.writePendingFrameworkSwap = writePendingFrameworkSwap;
|
|
37
|
-
exports.applyPendingFrameworkSwap = applyPendingFrameworkSwap;
|
|
38
|
-
exports.rollbackUnconfirmedFrameworkSwap = rollbackUnconfirmedFrameworkSwap;
|
|
39
|
-
exports.confirmFrameworkSwapHealthy = confirmFrameworkSwapHealthy;
|
|
40
|
-
/**
|
|
41
|
-
* Launcher zero-dep framework swap/rollback helpers.
|
|
42
|
-
*
|
|
43
|
-
* These run before @camstack/system is imported — zero @camstack imports.
|
|
44
|
-
* Only node:fs + node:path. All writes are atomic (tmpfile+rename).
|
|
45
|
-
* All reads tolerate missing/corrupt files (no-op result).
|
|
46
|
-
*/
|
|
47
|
-
const fs = __importStar(require("node:fs"));
|
|
48
|
-
const path = __importStar(require("node:path"));
|
|
49
|
-
const lifecycle_journal_path_js_1 = require("./lifecycle-journal-path.js");
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Marker file names
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
const PENDING_SWAP_MARKER = '.pending-framework-swap.json';
|
|
54
|
-
const SWAP_CONFIRM_MARKER = '.framework-swap-confirm.json';
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Helpers
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
/** Write a JSON file atomically via a tmp sibling + rename. */
|
|
59
|
-
function writeJsonAtomic(filePath, value) {
|
|
60
|
-
const tmp = `${filePath}.tmp`;
|
|
61
|
-
fs.writeFileSync(tmp, JSON.stringify(value), 'utf-8');
|
|
62
|
-
fs.renameSync(tmp, filePath);
|
|
63
|
-
}
|
|
64
|
-
/** Read + parse JSON, returning null on any error (missing / corrupt). */
|
|
65
|
-
function readJsonSafe(filePath) {
|
|
66
|
-
try {
|
|
67
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** Validate that a parsed value matches the PendingSwapMarker shape. */
|
|
74
|
-
function isPendingSwapMarker(value) {
|
|
75
|
-
if (typeof value !== 'object' || value === null)
|
|
76
|
-
return false;
|
|
77
|
-
const v = value;
|
|
78
|
-
if (typeof v['jobId'] !== 'string')
|
|
79
|
-
return false;
|
|
80
|
-
if (typeof v['taskId'] !== 'string')
|
|
81
|
-
return false;
|
|
82
|
-
if (!Array.isArray(v['packages']))
|
|
83
|
-
return false;
|
|
84
|
-
for (const pkg of v['packages']) {
|
|
85
|
-
if (typeof pkg !== 'object' || pkg === null)
|
|
86
|
-
return false;
|
|
87
|
-
const p = pkg;
|
|
88
|
-
if (typeof p['name'] !== 'string')
|
|
89
|
-
return false;
|
|
90
|
-
if (typeof p['stagedPath'] !== 'string')
|
|
91
|
-
return false;
|
|
92
|
-
if (typeof p['backupPath'] !== 'string')
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
if (typeof v['requestedAtMs'] !== 'number')
|
|
96
|
-
return false;
|
|
97
|
-
if (v['schemaVersion'] !== 1)
|
|
98
|
-
return false;
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
/** Validate that a parsed value matches the SwapConfirmMarker shape. */
|
|
102
|
-
function isSwapConfirmMarker(value) {
|
|
103
|
-
if (typeof value !== 'object' || value === null)
|
|
104
|
-
return false;
|
|
105
|
-
const v = value;
|
|
106
|
-
if (typeof v['jobId'] !== 'string')
|
|
107
|
-
return false;
|
|
108
|
-
if (typeof v['taskId'] !== 'string')
|
|
109
|
-
return false;
|
|
110
|
-
if (!Array.isArray(v['backups']))
|
|
111
|
-
return false;
|
|
112
|
-
for (const b of v['backups']) {
|
|
113
|
-
if (typeof b !== 'object' || b === null)
|
|
114
|
-
return false;
|
|
115
|
-
const entry = b;
|
|
116
|
-
if (typeof entry['name'] !== 'string')
|
|
117
|
-
return false;
|
|
118
|
-
if (typeof entry['backupPath'] !== 'string')
|
|
119
|
-
return false;
|
|
120
|
-
if (typeof entry['livePath'] !== 'string')
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
if (typeof v['appliedAtMs'] !== 'number')
|
|
124
|
-
return false;
|
|
125
|
-
if (typeof v['bootAttempts'] !== 'number')
|
|
126
|
-
return false;
|
|
127
|
-
if (v['schemaVersion'] !== 1)
|
|
128
|
-
return false;
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Patch a specific task inside a journal job file atomically.
|
|
133
|
-
* Reads `<dataDir>/lifecycle/jobs/<jobId>.json`, updates tasks[taskId],
|
|
134
|
-
* and writes back atomically. Silently no-ops on any read/write error.
|
|
135
|
-
*/
|
|
136
|
-
function patchJournalTask(dataDir, jobId, taskId, patch) {
|
|
137
|
-
const jobFile = path.join((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir), `${jobId}.json`);
|
|
138
|
-
const raw = readJsonSafe(jobFile);
|
|
139
|
-
if (typeof raw !== 'object' || raw === null)
|
|
140
|
-
return;
|
|
141
|
-
const job = raw;
|
|
142
|
-
const tasks = job['tasks'];
|
|
143
|
-
if (!Array.isArray(tasks))
|
|
144
|
-
return;
|
|
145
|
-
const updatedTasks = tasks.map((task) => {
|
|
146
|
-
if (typeof task !== 'object' || task === null)
|
|
147
|
-
return task;
|
|
148
|
-
const t = task;
|
|
149
|
-
if (t['taskId'] !== taskId)
|
|
150
|
-
return task;
|
|
151
|
-
const updated = { ...t, phase: patch.phase };
|
|
152
|
-
if (patch.error !== undefined)
|
|
153
|
-
updated['error'] = patch.error;
|
|
154
|
-
if (patch.finishedAtMs !== undefined)
|
|
155
|
-
updated['finishedAtMs'] = patch.finishedAtMs;
|
|
156
|
-
return updated;
|
|
157
|
-
});
|
|
158
|
-
try {
|
|
159
|
-
writeJsonAtomic(jobFile, { ...job, tasks: updatedTasks });
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
// journal patch is best-effort — never crash the launcher
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/** Copy a directory tree cross-device (fallback for EXDEV rename errors). */
|
|
166
|
-
function moveDirCrossDevice(src, dest) {
|
|
167
|
-
fs.cpSync(src, dest, { recursive: true });
|
|
168
|
-
fs.rmSync(src, { recursive: true, force: true });
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Write the `.pending-framework-swap.json` marker atomically.
|
|
172
|
-
* Co-located with the reader (`applyPendingFrameworkSwap`) for symmetry.
|
|
173
|
-
* Zero @camstack deps — this file runs before @camstack/system is imported.
|
|
174
|
-
*/
|
|
175
|
-
function writePendingFrameworkSwap(dataDir, marker) {
|
|
176
|
-
const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
|
|
177
|
-
writeJsonAtomic(markerPath, marker);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Apply a pending framework swap if a `.pending-framework-swap.json` marker
|
|
181
|
-
* exists in `dataDir`. Swaps each listed package atomically:
|
|
182
|
-
* 1. Backup: rename live → backupPath
|
|
183
|
-
* 2. Swap: rename staged → live (EXDEV fallback: cpSync + rmSync)
|
|
184
|
-
*
|
|
185
|
-
* On any mid-loop failure: restores already-swapped packages, deletes the
|
|
186
|
-
* marker, returns `{ applied: false }`.
|
|
187
|
-
* On success: writes `.framework-swap-confirm.json`, patches the journal
|
|
188
|
-
* task to `applied`, deletes the swap marker, returns `{ applied: true, jobId }`.
|
|
189
|
-
*/
|
|
190
|
-
function applyPendingFrameworkSwap(dataDir, frameworkDir) {
|
|
191
|
-
if (!frameworkDir)
|
|
192
|
-
return { applied: false };
|
|
193
|
-
const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
|
|
194
|
-
if (!fs.existsSync(markerPath))
|
|
195
|
-
return { applied: false };
|
|
196
|
-
const raw = readJsonSafe(markerPath);
|
|
197
|
-
if (!isPendingSwapMarker(raw)) {
|
|
198
|
-
// malformed — delete to prevent blocking future updates
|
|
199
|
-
try {
|
|
200
|
-
fs.rmSync(markerPath, { force: true });
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
// ignore deletion error
|
|
204
|
-
}
|
|
205
|
-
return { applied: false };
|
|
206
|
-
}
|
|
207
|
-
const marker = raw;
|
|
208
|
-
const swapped = [];
|
|
209
|
-
for (const pkg of marker.packages) {
|
|
210
|
-
const livePath = path.join(frameworkDir, 'node_modules', pkg.name);
|
|
211
|
-
try {
|
|
212
|
-
// Step 1: backup live dir if it exists
|
|
213
|
-
if (fs.existsSync(livePath)) {
|
|
214
|
-
fs.mkdirSync(path.dirname(pkg.backupPath), { recursive: true });
|
|
215
|
-
fs.renameSync(livePath, pkg.backupPath);
|
|
216
|
-
}
|
|
217
|
-
// Step 2: move staged dir to live
|
|
218
|
-
try {
|
|
219
|
-
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
|
220
|
-
fs.renameSync(pkg.stagedPath, livePath);
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
if (err.code === 'EXDEV') {
|
|
224
|
-
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
|
225
|
-
moveDirCrossDevice(pkg.stagedPath, livePath);
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
throw err;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
swapped.push({ livePath, backupPath: pkg.backupPath });
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// Restore all already-swapped packages
|
|
235
|
-
for (const done of swapped) {
|
|
236
|
-
try {
|
|
237
|
-
if (fs.existsSync(done.livePath)) {
|
|
238
|
-
fs.rmSync(done.livePath, { recursive: true, force: true });
|
|
239
|
-
}
|
|
240
|
-
if (fs.existsSync(done.backupPath)) {
|
|
241
|
-
fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
|
|
242
|
-
fs.renameSync(done.backupPath, done.livePath);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
// best-effort restore
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Delete the pending marker so the next boot doesn't retry a broken swap
|
|
250
|
-
try {
|
|
251
|
-
fs.rmSync(markerPath, { force: true });
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
// ignore
|
|
255
|
-
}
|
|
256
|
-
return { applied: false };
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// All packages swapped successfully — write confirm marker
|
|
260
|
-
const backups = marker.packages.map((pkg) => ({
|
|
261
|
-
name: pkg.name,
|
|
262
|
-
backupPath: pkg.backupPath,
|
|
263
|
-
livePath: path.join(frameworkDir, 'node_modules', pkg.name),
|
|
264
|
-
}));
|
|
265
|
-
const confirmMarker = {
|
|
266
|
-
jobId: marker.jobId,
|
|
267
|
-
taskId: marker.taskId,
|
|
268
|
-
backups,
|
|
269
|
-
appliedAtMs: Date.now(),
|
|
270
|
-
bootAttempts: 0,
|
|
271
|
-
schemaVersion: 1,
|
|
272
|
-
};
|
|
273
|
-
// Write confirm marker — if this fails, restore everything and fail the apply
|
|
274
|
-
try {
|
|
275
|
-
writeJsonAtomic(path.join(dataDir, SWAP_CONFIRM_MARKER), confirmMarker);
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
// Confirm marker write failed — restore all swapped packages and fail the apply
|
|
279
|
-
for (const done of swapped) {
|
|
280
|
-
try {
|
|
281
|
-
if (fs.existsSync(done.livePath)) {
|
|
282
|
-
fs.rmSync(done.livePath, { recursive: true, force: true });
|
|
283
|
-
}
|
|
284
|
-
if (fs.existsSync(done.backupPath)) {
|
|
285
|
-
fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
|
|
286
|
-
fs.renameSync(done.backupPath, done.livePath);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// best-effort restore
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// Delete the pending marker so the next boot doesn't retry
|
|
294
|
-
try {
|
|
295
|
-
fs.rmSync(markerPath, { force: true });
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
// ignore
|
|
299
|
-
}
|
|
300
|
-
return { applied: false };
|
|
301
|
-
}
|
|
302
|
-
// Patch journal task → applied (best-effort, doesn't fail the apply)
|
|
303
|
-
patchJournalTask(dataDir, marker.jobId, marker.taskId, { phase: 'applied' });
|
|
304
|
-
// Delete the swap marker
|
|
305
|
-
try {
|
|
306
|
-
fs.rmSync(markerPath, { force: true });
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// ignore
|
|
310
|
-
}
|
|
311
|
-
return { applied: true, jobId: marker.jobId };
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Check for an unconfirmed framework swap on boot.
|
|
315
|
-
*
|
|
316
|
-
* - `bootAttempts === 0`: this is the probation boot. Increment to 1,
|
|
317
|
-
* rewrite the marker, return `{ rolledBack: false }`.
|
|
318
|
-
* - `bootAttempts >= 1`: the previous probation boot crashed / didn't call
|
|
319
|
-
* `confirmFrameworkSwapHealthy`. Restore all backups, patch the journal
|
|
320
|
-
* task to `failed`, delete the confirm marker, return `{ rolledBack: true }`.
|
|
321
|
-
*/
|
|
322
|
-
function rollbackUnconfirmedFrameworkSwap(dataDir, frameworkDir) {
|
|
323
|
-
if (!frameworkDir)
|
|
324
|
-
return { rolledBack: false };
|
|
325
|
-
const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
|
|
326
|
-
if (!fs.existsSync(confirmPath))
|
|
327
|
-
return { rolledBack: false };
|
|
328
|
-
const raw = readJsonSafe(confirmPath);
|
|
329
|
-
if (!isSwapConfirmMarker(raw))
|
|
330
|
-
return { rolledBack: false };
|
|
331
|
-
const confirm = raw;
|
|
332
|
-
if (confirm.bootAttempts === 0) {
|
|
333
|
-
// Probation boot — increment attempts and let this boot proceed
|
|
334
|
-
const updated = { ...confirm, bootAttempts: 1 };
|
|
335
|
-
try {
|
|
336
|
-
writeJsonAtomic(confirmPath, updated);
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
// ignore write failure
|
|
340
|
-
}
|
|
341
|
-
return { rolledBack: false };
|
|
342
|
-
}
|
|
343
|
-
// bootAttempts >= 1: probation boot didn't become healthy — rollback
|
|
344
|
-
for (const backup of confirm.backups) {
|
|
345
|
-
try {
|
|
346
|
-
if (fs.existsSync(backup.livePath)) {
|
|
347
|
-
fs.rmSync(backup.livePath, { recursive: true, force: true });
|
|
348
|
-
}
|
|
349
|
-
if (fs.existsSync(backup.backupPath)) {
|
|
350
|
-
fs.mkdirSync(path.dirname(backup.livePath), { recursive: true });
|
|
351
|
-
fs.renameSync(backup.backupPath, backup.livePath);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
// best-effort restore
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
patchJournalTask(dataDir, confirm.jobId, confirm.taskId, {
|
|
359
|
-
phase: 'failed',
|
|
360
|
-
error: 'framework update rolled back — boot did not become healthy',
|
|
361
|
-
finishedAtMs: Date.now(),
|
|
362
|
-
});
|
|
363
|
-
try {
|
|
364
|
-
fs.rmSync(confirmPath, { force: true });
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
// ignore
|
|
368
|
-
}
|
|
369
|
-
return { rolledBack: true };
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Called by post-boot health check once the hub is confirmed healthy after
|
|
373
|
-
* a framework swap. Deletes all backups and the confirm marker so the old
|
|
374
|
-
* framework packages are cleaned up.
|
|
375
|
-
*/
|
|
376
|
-
function confirmFrameworkSwapHealthy(dataDir) {
|
|
377
|
-
const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
|
|
378
|
-
if (!fs.existsSync(confirmPath))
|
|
379
|
-
return;
|
|
380
|
-
const raw = readJsonSafe(confirmPath);
|
|
381
|
-
if (!isSwapConfirmMarker(raw)) {
|
|
382
|
-
// corrupt marker — still delete it
|
|
383
|
-
try {
|
|
384
|
-
fs.rmSync(confirmPath, { force: true });
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
// ignore
|
|
388
|
-
}
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const confirm = raw;
|
|
392
|
-
// Delete all backup dirs
|
|
393
|
-
for (const backup of confirm.backups) {
|
|
394
|
-
try {
|
|
395
|
-
fs.rmSync(backup.backupPath, { recursive: true, force: true });
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
// best-effort cleanup
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
// Delete the confirm marker
|
|
402
|
-
try {
|
|
403
|
-
fs.rmSync(confirmPath, { force: true });
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
// ignore
|
|
407
|
-
}
|
|
408
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requestFrameworkSwapAndRestart = requestFrameworkSwapAndRestart;
|
|
4
|
-
/**
|
|
5
|
-
* The single seam that joins the two halves of a framework-swap request:
|
|
6
|
-
* 1. write the durable `.pending-framework-swap.json` marker (consumed by the
|
|
7
|
-
* zero-dep launcher on the NEXT boot), and
|
|
8
|
-
* 2. schedule the self-restart that makes the launcher actually run.
|
|
9
|
-
*
|
|
10
|
-
* The launcher (`launcher-framework-swap.ts`) runs before `@camstack/system`
|
|
11
|
-
* is imported and is deliberately zero-dep, so it cannot own the restart — that
|
|
12
|
-
* lives in `@camstack/system`. This module is the ONLY owner of "stage a
|
|
13
|
-
* framework swap → reboot": every engine path that stages the framework (single
|
|
14
|
-
* `updateFrameworkPackage`, the "Update all" bulk job, and the auto-update
|
|
15
|
-
* scheduler) wires its `requestFrameworkSwap` dep to this helper and gets the
|
|
16
|
-
* reboot for free. The lifecycle engine assumes the process exits after
|
|
17
|
-
* `requestFrameworkSwap` returns; without the restart here the marker is written
|
|
18
|
-
* but never applied, silently stranding the update on disk.
|
|
19
|
-
*/
|
|
20
|
-
const system_1 = require("@camstack/system");
|
|
21
|
-
const launcher_framework_swap_js_1 = require("./launcher-framework-swap.js");
|
|
22
|
-
/** Delay before the hub exits — lets the in-flight startJob reply drain first. */
|
|
23
|
-
const FRAMEWORK_SWAP_RESTART_DELAY_MS = 500;
|
|
24
|
-
/**
|
|
25
|
-
* Write the pending-swap marker, then schedule the self-restart. The restart is
|
|
26
|
-
* UNCONDITIONAL: a staged framework swap is inert until the launcher applies it
|
|
27
|
-
* on reboot, so staging without restarting would silently lose the update.
|
|
28
|
-
*/
|
|
29
|
-
function requestFrameworkSwapAndRestart(dataDir, input, deps = {}) {
|
|
30
|
-
const writeMarker = deps.writeMarker ?? launcher_framework_swap_js_1.writePendingFrameworkSwap;
|
|
31
|
-
const scheduleRestart = deps.scheduleRestart ?? system_1.scheduleSelfRestart;
|
|
32
|
-
const now = deps.now ?? Date.now;
|
|
33
|
-
writeMarker(dataDir, {
|
|
34
|
-
jobId: input.jobId,
|
|
35
|
-
taskId: input.taskId,
|
|
36
|
-
packages: input.packages,
|
|
37
|
-
requestedAtMs: now(),
|
|
38
|
-
schemaVersion: 1,
|
|
39
|
-
});
|
|
40
|
-
scheduleRestart({ delayMs: FRAMEWORK_SWAP_RESTART_DELAY_MS });
|
|
41
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.planBoot = planBoot;
|
|
4
|
-
/**
|
|
5
|
-
* Decide the boot target. Never throws; never mutates `state`.
|
|
6
|
-
* `stateToWrite` is non-null whenever the durable state must change
|
|
7
|
-
* (probation counter bump, rollback, invalid-version eviction).
|
|
8
|
-
*/
|
|
9
|
-
function planBoot(state, isValidVersion, now) {
|
|
10
|
-
if (state === null) {
|
|
11
|
-
return { kind: 'baked', reason: 'no server-root state', stateToWrite: null };
|
|
12
|
-
}
|
|
13
|
-
let next = state;
|
|
14
|
-
let changed = false;
|
|
15
|
-
// ── Pending (freshly-staged) version ──────────────────────────────────
|
|
16
|
-
if (next.pendingBoot !== null) {
|
|
17
|
-
const pending = next.pendingBoot;
|
|
18
|
-
if (pending.bootAttempts >= 1) {
|
|
19
|
-
// Previous probation boot never confirmed healthy — roll back.
|
|
20
|
-
next = {
|
|
21
|
-
...next,
|
|
22
|
-
pendingBoot: null,
|
|
23
|
-
rolledBack: {
|
|
24
|
-
fromVersion: pending.version,
|
|
25
|
-
toVersion: next.currentVersion,
|
|
26
|
-
atMs: now(),
|
|
27
|
-
reason: 'probation boot did not reach ready — rolled back',
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
changed = true;
|
|
31
|
-
}
|
|
32
|
-
else if (!isValidVersion(pending.version)) {
|
|
33
|
-
next = {
|
|
34
|
-
...next,
|
|
35
|
-
pendingBoot: null,
|
|
36
|
-
rolledBack: {
|
|
37
|
-
fromVersion: pending.version,
|
|
38
|
-
toVersion: next.currentVersion,
|
|
39
|
-
atMs: now(),
|
|
40
|
-
reason: 'staged version failed validation',
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
changed = true;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
// Probation boot: bump the attempt counter durably, keep currentVersion
|
|
47
|
-
// untouched until the hub confirms the boot healthy.
|
|
48
|
-
return {
|
|
49
|
-
kind: 'data-root',
|
|
50
|
-
version: pending.version,
|
|
51
|
-
probation: true,
|
|
52
|
-
stateToWrite: {
|
|
53
|
-
...next,
|
|
54
|
-
pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 },
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// ── Confirmed current version ──────────────────────────────────────────
|
|
60
|
-
if (next.currentVersion !== null) {
|
|
61
|
-
if (isValidVersion(next.currentVersion)) {
|
|
62
|
-
return {
|
|
63
|
-
kind: 'data-root',
|
|
64
|
-
version: next.currentVersion,
|
|
65
|
-
probation: false,
|
|
66
|
-
stateToWrite: changed ? next : null,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// Current version dir is broken — try N-1, else baked.
|
|
70
|
-
const broken = next.currentVersion;
|
|
71
|
-
if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
|
|
72
|
-
const fallback = next.previousVersion;
|
|
73
|
-
return {
|
|
74
|
-
kind: 'data-root',
|
|
75
|
-
version: fallback,
|
|
76
|
-
probation: false,
|
|
77
|
-
stateToWrite: {
|
|
78
|
-
...next,
|
|
79
|
-
currentVersion: fallback,
|
|
80
|
-
previousVersion: null,
|
|
81
|
-
rolledBack: {
|
|
82
|
-
fromVersion: broken,
|
|
83
|
-
toVersion: fallback,
|
|
84
|
-
atMs: now(),
|
|
85
|
-
reason: 'active version failed validation',
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
kind: 'baked',
|
|
92
|
-
reason: `active version ${broken} failed validation and no valid previous version exists`,
|
|
93
|
-
stateToWrite: {
|
|
94
|
-
...next,
|
|
95
|
-
currentVersion: null,
|
|
96
|
-
rolledBack: {
|
|
97
|
-
fromVersion: broken,
|
|
98
|
-
toVersion: null,
|
|
99
|
-
atMs: now(),
|
|
100
|
-
reason: 'active version failed validation',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
kind: 'baked',
|
|
107
|
-
reason: 'no active data-root version',
|
|
108
|
-
stateToWrite: changed ? next : null,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Minimal semver comparison — zero-dep (used by the starter, which must never
|
|
4
|
-
* import from node_modules). Handles `MAJOR.MINOR.PATCH[-prerelease]`:
|
|
5
|
-
* numeric segment compare, missing segments = 0, prerelease < its release,
|
|
6
|
-
* prereleases of the same base compare lexically. Malformed segments compare
|
|
7
|
-
* as 0 (best effort — never throws).
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.compareSemver = compareSemver;
|
|
11
|
-
function parse(version) {
|
|
12
|
-
const dashIdx = version.indexOf('-');
|
|
13
|
-
const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
|
|
14
|
-
const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
|
|
15
|
-
const nums = base.split('.').map((seg) => {
|
|
16
|
-
const n = Number.parseInt(seg, 10);
|
|
17
|
-
return Number.isNaN(n) ? 0 : n;
|
|
18
|
-
});
|
|
19
|
-
return { nums, prerelease };
|
|
20
|
-
}
|
|
21
|
-
/** Returns -1 when a < b, 0 when equal, 1 when a > b. */
|
|
22
|
-
function compareSemver(a, b) {
|
|
23
|
-
const pa = parse(a);
|
|
24
|
-
const pb = parse(b);
|
|
25
|
-
const len = Math.max(pa.nums.length, pb.nums.length);
|
|
26
|
-
for (let i = 0; i < len; i++) {
|
|
27
|
-
const na = pa.nums[i] ?? 0;
|
|
28
|
-
const nb = pb.nums[i] ?? 0;
|
|
29
|
-
if (na < nb)
|
|
30
|
-
return -1;
|
|
31
|
-
if (na > nb)
|
|
32
|
-
return 1;
|
|
33
|
-
}
|
|
34
|
-
if (pa.prerelease === null && pb.prerelease === null)
|
|
35
|
-
return 0;
|
|
36
|
-
if (pa.prerelease === null)
|
|
37
|
-
return 1;
|
|
38
|
-
if (pb.prerelease === null)
|
|
39
|
-
return -1;
|
|
40
|
-
if (pa.prerelease < pb.prerelease)
|
|
41
|
-
return -1;
|
|
42
|
-
if (pa.prerelease > pb.prerelease)
|
|
43
|
-
return 1;
|
|
44
|
-
return 0;
|
|
45
|
-
}
|