@camstack/server 1.2.36 → 1.2.38
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/launcher.js +59 -0
- package/dist/package-inventory.js +180 -0
- package/package.json +5 -5
package/dist/launcher.js
CHANGED
|
@@ -56,6 +56,7 @@ const path = __importStar(require("node:path"));
|
|
|
56
56
|
const tar = __importStar(require("tar"));
|
|
57
57
|
const yaml = __importStar(require("js-yaml"));
|
|
58
58
|
const framework_nodepath_js_1 = require("./framework-nodepath.js");
|
|
59
|
+
const package_inventory_js_1 = require("./package-inventory.js");
|
|
59
60
|
const bootstrap_packages_js_1 = require("./bootstrap-packages.js");
|
|
60
61
|
/** Path of the manifest file embedded inside every archive. */
|
|
61
62
|
const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
|
|
@@ -435,6 +436,64 @@ async function launch() {
|
|
|
435
436
|
console.warn(`[launcher] could NOT resolve ${pkg}: ${err instanceof Error ? err.message : String(err)}`);
|
|
436
437
|
}
|
|
437
438
|
}
|
|
439
|
+
// WHAT ELSE could have run? (D45 enforcement step 2)
|
|
440
|
+
//
|
|
441
|
+
// The line above says which copy this boot resolved. It cannot say what else
|
|
442
|
+
// is lying around, and that is the half that produced three wrong diagnoses:
|
|
443
|
+
// one agent held FOUR copies of @camstack/system (1.2.28 / 1.2.28 / 1.2.0 /
|
|
444
|
+
// 1.2.30), so every version the node reported was true of a copy nobody was
|
|
445
|
+
// running.
|
|
446
|
+
//
|
|
447
|
+
// Reported, not fatal. Every node in the fleet currently HAS extra copies —
|
|
448
|
+
// the image seeds under /opt and the legacy /data/framework — and D45 records
|
|
449
|
+
// their removal as a deliberate, scheduled migration with a container
|
|
450
|
+
// recreate as the escape hatch, not something a routine boot should enact by
|
|
451
|
+
// refusing to start. `CAMSTACK_INVENTORY_STRICT=1` makes it fatal, which is
|
|
452
|
+
// how a layout that HAS been cleaned keeps itself clean.
|
|
453
|
+
try {
|
|
454
|
+
const inventory = (0, package_inventory_js_1.inventoryHostProvided)((0, package_inventory_js_1.hostProvidedSearchRoots)({
|
|
455
|
+
nodePath: process.env['NODE_PATH'],
|
|
456
|
+
pathSeparator: process.platform === 'win32' ? ';' : ':',
|
|
457
|
+
dataDir: DATA_DIR,
|
|
458
|
+
serverNodeModules: nodeModulesDir,
|
|
459
|
+
seedRoots: (0, package_inventory_js_1.seedNodeModulesRoots)(fs),
|
|
460
|
+
}), {
|
|
461
|
+
exists: (dir) => fs.existsSync(path.join(dir, 'package.json')),
|
|
462
|
+
readVersion: (dir) => {
|
|
463
|
+
try {
|
|
464
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
465
|
+
const version = typeof raw === 'object' && raw !== null
|
|
466
|
+
? raw.version
|
|
467
|
+
: undefined;
|
|
468
|
+
return typeof version === 'string' ? version : null;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
realPath: (dir) => {
|
|
475
|
+
try {
|
|
476
|
+
return fs.realpathSync(dir);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return dir;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
const report = (0, package_inventory_js_1.formatInventoryReport)(inventory);
|
|
484
|
+
if (report !== '') {
|
|
485
|
+
console.error(`[launcher] ${report}`);
|
|
486
|
+
if (process.env['CAMSTACK_INVENTORY_STRICT'] === '1') {
|
|
487
|
+
console.error('[launcher] CAMSTACK_INVENTORY_STRICT=1 — refusing to boot on this layout');
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
// The inventory is diagnostics. It must never be the reason a node fails
|
|
494
|
+
// to boot when it is the thing that was supposed to explain failures.
|
|
495
|
+
console.warn(`[launcher] package inventory failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
496
|
+
}
|
|
438
497
|
// Now safe to load the role's runtime entry (both have static imports from
|
|
439
498
|
// @camstack/system, resolved only after the framework dir + NODE_PATH setup
|
|
440
499
|
// above). The agent boots from THIS same @camstack/server closure — there is
|
|
@@ -0,0 +1,180 @@
|
|
|
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.HOST_PROVIDED_PACKAGES = void 0;
|
|
37
|
+
exports.hostProvidedSearchRoots = hostProvidedSearchRoots;
|
|
38
|
+
exports.seedNodeModulesRoots = seedNodeModulesRoots;
|
|
39
|
+
exports.inventoryHostProvided = inventoryHostProvided;
|
|
40
|
+
exports.formatInventoryReport = formatInventoryReport;
|
|
41
|
+
/**
|
|
42
|
+
* Boot-time inventory of host-provided packages — enforcement step 2 of
|
|
43
|
+
* [D45](../../../docs/decisions/adr-0045.md).
|
|
44
|
+
*
|
|
45
|
+
* D45 requires exactly ONE copy of every host-provided package per node, inside
|
|
46
|
+
* the active server closure. The record exists because that was not true: one
|
|
47
|
+
* agent held four copies of `@camstack/system` — 1.2.28 under the image seed,
|
|
48
|
+
* 1.2.28 under a second seed tree, 1.2.0 in the legacy `/data/framework`, and
|
|
49
|
+
* 1.2.30 in the closure — and each could be truthfully quoted as "the version".
|
|
50
|
+
* Three consecutive diagnoses of which one actually ran were each disproved by
|
|
51
|
+
* the next measurement.
|
|
52
|
+
*
|
|
53
|
+
* Step 1 (the launcher naming the copy it RESOLVED) answers "which one runs".
|
|
54
|
+
* This answers the other half — "what else is lying around that could have
|
|
55
|
+
* run" — which is the question a resolved path alone cannot.
|
|
56
|
+
*
|
|
57
|
+
* Everything here is pure: the caller injects the three filesystem operations,
|
|
58
|
+
* so the whole inventory is testable against a described layout rather than a
|
|
59
|
+
* real disk.
|
|
60
|
+
*/
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
/**
|
|
63
|
+
* Packages the closure provides to everything on the node. Addons never bundle
|
|
64
|
+
* these ([D15](../../../docs/decisions/adr-0015.md)); a second copy on disk is
|
|
65
|
+
* a resolution race waiting for a boot order to change.
|
|
66
|
+
*/
|
|
67
|
+
exports.HOST_PROVIDED_PACKAGES = [
|
|
68
|
+
'@camstack/system',
|
|
69
|
+
'@camstack/types',
|
|
70
|
+
'@camstack/sdk',
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Every directory under which a host-provided package could sit as
|
|
74
|
+
* `<root>/<pkgName>`.
|
|
75
|
+
*
|
|
76
|
+
* The list is explicit rather than a filesystem walk: these are the places a
|
|
77
|
+
* copy has actually turned up, each named in D45, and a bounded list keeps this
|
|
78
|
+
* to a handful of stat calls at boot. `/data/addons` is a root in its own right
|
|
79
|
+
* because the bootstrap-install layout puts the package DIRECTLY there
|
|
80
|
+
* (`/data/addons/@camstack/system`), not under a `node_modules`.
|
|
81
|
+
*/
|
|
82
|
+
function hostProvidedSearchRoots(input) {
|
|
83
|
+
const fromNodePath = (input.nodePath ?? '')
|
|
84
|
+
.split(input.pathSeparator)
|
|
85
|
+
.map((p) => p.trim())
|
|
86
|
+
.filter((p) => p.length > 0);
|
|
87
|
+
const roots = [
|
|
88
|
+
input.serverNodeModules,
|
|
89
|
+
...fromNodePath,
|
|
90
|
+
path.join(input.dataDir, 'framework', 'node_modules'),
|
|
91
|
+
path.join(input.dataDir, 'addons'),
|
|
92
|
+
...input.seedRoots,
|
|
93
|
+
];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
return roots.filter((r) => {
|
|
96
|
+
if (seen.has(r))
|
|
97
|
+
return false;
|
|
98
|
+
seen.add(r);
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The image seed trees: `/opt/<name>/node_modules`.
|
|
104
|
+
*
|
|
105
|
+
* Enumerated rather than hardcoded because their names have changed with the
|
|
106
|
+
* image layout (`camstack-hub`, `camstack-seed-framework`, …) and a hardcoded
|
|
107
|
+
* list would quietly stop finding the copy it was written for. Returns empty
|
|
108
|
+
* when `/opt` does not exist, which is every dev machine.
|
|
109
|
+
*/
|
|
110
|
+
function seedNodeModulesRoots(fs, optDir = '/opt') {
|
|
111
|
+
if (!fs.existsSync(optDir))
|
|
112
|
+
return [];
|
|
113
|
+
try {
|
|
114
|
+
return fs
|
|
115
|
+
.readdirSync(optDir)
|
|
116
|
+
.map((name) => path.join(optDir, name, 'node_modules'))
|
|
117
|
+
.filter((dir) => fs.existsSync(dir));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Find every copy of every host-provided package under `roots`.
|
|
125
|
+
*
|
|
126
|
+
* A copy whose package.json will not parse is still RECORDED, with a null
|
|
127
|
+
* version. Dropping it would under-report the very thing this exists to
|
|
128
|
+
* count — and an unreadable copy is exactly the kind that has been sitting
|
|
129
|
+
* somewhere since an interrupted install.
|
|
130
|
+
*/
|
|
131
|
+
function inventoryHostProvided(roots, fs, packages = exports.HOST_PROVIDED_PACKAGES) {
|
|
132
|
+
const copies = [];
|
|
133
|
+
for (const pkg of packages) {
|
|
134
|
+
// Keyed by canonical path: two roots that are the same directory (a
|
|
135
|
+
// symlinked closure, a repeated NODE_PATH entry) are ONE copy, and calling
|
|
136
|
+
// them two would cry wolf on a node that is actually compliant.
|
|
137
|
+
const seenReal = new Set();
|
|
138
|
+
for (const root of roots) {
|
|
139
|
+
const dir = path.join(root, pkg);
|
|
140
|
+
if (!fs.exists(dir))
|
|
141
|
+
continue;
|
|
142
|
+
const real = fs.realPath(dir);
|
|
143
|
+
if (seenReal.has(real))
|
|
144
|
+
continue;
|
|
145
|
+
seenReal.add(real);
|
|
146
|
+
copies.push({ pkg, path: dir, version: fs.readVersion(dir) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const countByPkg = new Map();
|
|
150
|
+
for (const c of copies)
|
|
151
|
+
countByPkg.set(c.pkg, (countByPkg.get(c.pkg) ?? 0) + 1);
|
|
152
|
+
const duplicated = [...countByPkg.entries()]
|
|
153
|
+
.filter(([, n]) => n > 1)
|
|
154
|
+
.map(([pkg]) => pkg)
|
|
155
|
+
.sort();
|
|
156
|
+
return { copies, duplicated, rootsScanned: roots };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The loud report. Empty string when the node is clean — a compliant boot says
|
|
160
|
+
* nothing, so the one that isn't stands out instead of scrolling past.
|
|
161
|
+
*/
|
|
162
|
+
function formatInventoryReport(result) {
|
|
163
|
+
if (result.duplicated.length === 0)
|
|
164
|
+
return '';
|
|
165
|
+
const lines = [
|
|
166
|
+
'D45 violation — a host-provided package exists more than once on this node.',
|
|
167
|
+
'Which copy runs is a module-resolution outcome, and every version this node',
|
|
168
|
+
'reports is a claim about one of them rather than about the node.',
|
|
169
|
+
'',
|
|
170
|
+
];
|
|
171
|
+
for (const pkg of result.duplicated) {
|
|
172
|
+
const found = result.copies.filter((c) => c.pkg === pkg);
|
|
173
|
+
lines.push(` ${pkg} — ${found.length} copies:`);
|
|
174
|
+
for (const c of found) {
|
|
175
|
+
lines.push(` ${c.version ?? '<unreadable package.json>'} ${c.path}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
lines.push('', ` roots scanned: ${result.rootsScanned.join(', ')}`);
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.38",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@camstack/addon-admin-ui": "1.2.22",
|
|
37
37
|
"@camstack/addon-agent-ui": "1.2.10",
|
|
38
|
-
"@camstack/addon-auth": "1.2.
|
|
38
|
+
"@camstack/addon-auth": "1.2.11",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.9",
|
|
40
|
-
"@camstack/addon-notifiers": "1.2.
|
|
40
|
+
"@camstack/addon-notifiers": "1.2.12",
|
|
41
41
|
"@camstack/addon-pipeline": "1.2.39",
|
|
42
42
|
"@camstack/addon-pipeline-orchestrator": "1.2.19",
|
|
43
43
|
"@camstack/addon-post-analysis": "1.2.24",
|
|
44
44
|
"@camstack/sdk": "1.2.9",
|
|
45
45
|
"@camstack/shm-ring": "1.1.9",
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
46
|
+
"@camstack/system": "1.2.32",
|
|
47
|
+
"@camstack/types": "1.2.24",
|
|
48
48
|
"@camstack/ui-library": "1.2.18",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|