@camstack/server 1.2.268 → 1.2.270
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/api/backup-download-pump.js +139 -0
- package/dist/api/backup-download.js +109 -0
- package/dist/api/trpc/ws-slow-consumer-guard.js +166 -0
- package/dist/core/addon/addon-registry.service.js +76 -136
- package/dist/core/addon/forked-custom-actions.js +63 -0
- package/dist/core/moleculer/moleculer.service.js +31 -0
- package/dist/core/update-availability-emitter.js +6 -0
- package/dist/core/updates/update-check-scheduler.js +3 -0
- package/dist/core/updates/wrapper-update-checker.js +63 -0
- package/dist/main.js +48 -84
- package/dist/manual-boot.js +22 -0
- package/dist/server-root/index.js +131 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -58,6 +58,7 @@ const ws_1 = require("@trpc/server/adapters/ws");
|
|
|
58
58
|
const ws_2 = require("ws");
|
|
59
59
|
const addon_upload_1 = require("./api/addon-upload");
|
|
60
60
|
const auth_whoami_1 = require("./api/auth-whoami");
|
|
61
|
+
const backup_download_js_1 = require("./api/backup-download.js");
|
|
61
62
|
const health_routes_1 = require("./api/health/health.routes");
|
|
62
63
|
const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
|
|
63
64
|
const model_distributor_js_1 = require("./api/model-distributor.js");
|
|
@@ -79,6 +80,7 @@ const trpc_router_1 = require("./api/trpc/trpc.router");
|
|
|
79
80
|
const trpc_error_principal_1 = require("./api/trpc/trpc-error-principal");
|
|
80
81
|
const trpc_error_device_tags_1 = require("./api/trpc/trpc-error-device-tags");
|
|
81
82
|
const ws_request_census_1 = require("./api/trpc/ws-request-census");
|
|
83
|
+
const ws_slow_consumer_guard_js_1 = require("./api/trpc/ws-slow-consumer-guard.js");
|
|
82
84
|
const addon_route_jwt_gate_js_1 = require("./auth/addon-route-jwt-gate.js");
|
|
83
85
|
const addon_route_share_gate_js_1 = require("./auth/addon-route-share-gate.js");
|
|
84
86
|
const session_cookie_js_1 = require("./auth/session-cookie.js");
|
|
@@ -302,6 +304,22 @@ async function bootstrap() {
|
|
|
302
304
|
socketPlane: (0, system_1.createSocketPlaneReader)({
|
|
303
305
|
registry: () => moleculerForEventPlane?.childRegistry ?? null,
|
|
304
306
|
}),
|
|
307
|
+
// The write queue to every peer NODE, from Moleculer's own writer
|
|
308
|
+
// sockets. Those are unref'd (`TcpWriter.connect`), so the 2026-09-10
|
|
309
|
+
// exclusion of "every socket queue" — a sum over `_getActiveHandles()` —
|
|
310
|
+
// never saw them. With the UDS queues above, every outbound plane this
|
|
311
|
+
// process writes to is now on the line and judged on every probe (D446).
|
|
312
|
+
meshQueue: (0, system_1.createMeshQueueReader)(() => moleculerForEventPlane?.broker ?? null),
|
|
313
|
+
// The ArrayBuffer census, armed by the probe itself. Twice on 2026-09-10
|
|
314
|
+
// this process's `arrayBuffers` climbed linearly for an hour (to 1.3 GB
|
|
315
|
+
// and to 5 GB, both LIVE across a forced compaction) and released in one
|
|
316
|
+
// instant, with no line at either boundary; the measurement that names
|
|
317
|
+
// the shape existed and was never run because it needed a human awake
|
|
318
|
+
// during the episode. Now the heartbeat takes it: one forced full GC —
|
|
319
|
+
// the pause a reclaim pass already costs — walked in memory, nothing
|
|
320
|
+
// written, on a threshold held for a minute, at most twice an hour.
|
|
321
|
+
// It is NOT a heap snapshot; see `array-buffer-census.ts`.
|
|
322
|
+
arrayBufferCensus: { census: (0, system_1.createInspectorArrayBufferCensus)() },
|
|
305
323
|
});
|
|
306
324
|
// Clean up orphaned processes from previous crashes before starting
|
|
307
325
|
cleanupOrphanProcesses();
|
|
@@ -810,89 +828,14 @@ async function bootstrap() {
|
|
|
810
828
|
});
|
|
811
829
|
}
|
|
812
830
|
});
|
|
813
|
-
// ── Backup archive download
|
|
814
|
-
// Streams an archive
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (!authHeader) {
|
|
822
|
-
return reply.status(401).send({ error: 'Unauthorized' });
|
|
823
|
-
}
|
|
824
|
-
try {
|
|
825
|
-
const token = authHeader.replace('Bearer ', '');
|
|
826
|
-
const downloadAuth = app.get(auth_service_1.AuthService);
|
|
827
|
-
const payload = downloadAuth.verifyToken(token);
|
|
828
|
-
if (!payload.isAdmin) {
|
|
829
|
-
return reply.status(403).send({ error: 'Admin required' });
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
catch {
|
|
833
|
-
return reply.status(401).send({ error: 'Invalid token' });
|
|
834
|
-
}
|
|
835
|
-
const { locationId, archiveId } = request.params;
|
|
836
|
-
const backupSingleton = capabilityRegistry.getSingleton('backup');
|
|
837
|
-
if (!backupSingleton?.listArchives) {
|
|
838
|
-
return reply.status(503).send({ error: 'Backup orchestrator unavailable' });
|
|
839
|
-
}
|
|
840
|
-
const archives = await backupSingleton.listArchives({ destinationId: locationId });
|
|
841
|
-
const archive = archives.find((a) => a.id === archiveId);
|
|
842
|
-
if (!archive) {
|
|
843
|
-
return reply.status(404).send({ error: 'Archive not found' });
|
|
844
|
-
}
|
|
845
|
-
const storage = capabilityRegistry.getSingleton('storage');
|
|
846
|
-
if (!storage?.beginDownload) {
|
|
847
|
-
return reply.status(503).send({ error: 'Storage cap unavailable' });
|
|
848
|
-
}
|
|
849
|
-
const downloadName = `${archive.label ?? archive.id}.tar.gz`;
|
|
850
|
-
// Sanitize: strip newlines and double-quotes which would break
|
|
851
|
-
// the Content-Disposition header. Whitespace is fine.
|
|
852
|
-
const safeName = downloadName.replace(/[\r\n"]/g, '_');
|
|
853
|
-
reply.header('content-type', 'application/gzip');
|
|
854
|
-
reply.header('content-length', String(archive.sizeBytes));
|
|
855
|
-
reply.header('content-disposition', `attachment; filename="${safeName}"`);
|
|
856
|
-
const { downloadId, sizeBytes } = await storage.beginDownload({
|
|
857
|
-
location: locationId,
|
|
858
|
-
relativePath: archive.filename,
|
|
859
|
-
});
|
|
860
|
-
const CHUNK = 8 * 1024 * 1024;
|
|
861
|
-
try {
|
|
862
|
-
let offset = 0;
|
|
863
|
-
// Stream the chunked response. Fastify's `reply.raw` is the
|
|
864
|
-
// Node.js writable; we write each chunk and `end()` once
|
|
865
|
-
// we've drained the source. Per-write back-pressure is
|
|
866
|
-
// handled inside the runtime — buffered writes pile up but
|
|
867
|
-
// never beyond the OS socket buffer.
|
|
868
|
-
while (offset < sizeBytes) {
|
|
869
|
-
const len = Math.min(CHUNK, sizeBytes - offset);
|
|
870
|
-
const chunk = await storage.readChunk({ downloadId, offset, length: len });
|
|
871
|
-
if (chunk.byteLength === 0) {
|
|
872
|
-
throw new Error(`backup download: empty chunk at offset ${offset}/${sizeBytes}`);
|
|
873
|
-
}
|
|
874
|
-
reply.raw.write(chunk);
|
|
875
|
-
offset += chunk.byteLength;
|
|
876
|
-
}
|
|
877
|
-
reply.raw.end();
|
|
878
|
-
}
|
|
879
|
-
catch (err) {
|
|
880
|
-
console.error('[backup-download] stream failed:', err);
|
|
881
|
-
// Headers may already be flushed — abort the socket if so.
|
|
882
|
-
if (!reply.raw.headersSent) {
|
|
883
|
-
return reply.status(500).send({ error: 'Download failed' });
|
|
884
|
-
}
|
|
885
|
-
reply.raw.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
886
|
-
}
|
|
887
|
-
finally {
|
|
888
|
-
try {
|
|
889
|
-
await storage.endDownload({ downloadId });
|
|
890
|
-
}
|
|
891
|
-
catch {
|
|
892
|
-
/* best-effort */
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return reply;
|
|
831
|
+
// ── Backup archive download ─────────────────────────────────────
|
|
832
|
+
// Streams an archive through the storage cap's chunked-download
|
|
833
|
+
// protocol at the pace the client reads it (D447). The route, its
|
|
834
|
+
// auth chain and the drain-respecting pump live in `api/backup-download*`.
|
|
835
|
+
(0, backup_download_js_1.registerBackupDownloadRoute)(fastify, {
|
|
836
|
+
verifier: app.get(auth_service_1.AuthService),
|
|
837
|
+
capabilities: capabilityRegistry,
|
|
838
|
+
logger: app.get(logging_service_1.LoggingService).createLogger('backup-download'),
|
|
896
839
|
});
|
|
897
840
|
// POST /api/auth/session — upgrade a tRPC-issued JWT to a browser cookie.
|
|
898
841
|
fastify.post('/api/auth/session', async (request, reply) => {
|
|
@@ -1223,18 +1166,39 @@ async function bootstrap() {
|
|
|
1223
1166
|
// call. The frame is the only place an operation is observable from
|
|
1224
1167
|
// outside the adapter — see `ws-request-census.ts`. Registered before
|
|
1225
1168
|
// `applyWSSHandler` so no socket can be accepted without a session.
|
|
1169
|
+
// A subscriber that stops READING is disconnected, not buffered until the
|
|
1170
|
+
// hub dies. On 2026-09-10 the only WS client (the operator's phone, viewer
|
|
1171
|
+
// in the background, JS suspended, TCP open) queued 88 MB of JSON strings
|
|
1172
|
+
// in hub-main's old_space with no bound and no line. See
|
|
1173
|
+
// `ws-slow-consumer-guard.ts` for the policy and the alternatives it beat.
|
|
1174
|
+
const wsSlowConsumerGuard = new ws_slow_consumer_guard_js_1.WsSlowConsumerGuard({
|
|
1175
|
+
logger: app.get(logging_service_1.LoggingService).createLogger('tRPC:ws'),
|
|
1176
|
+
});
|
|
1177
|
+
wsSlowConsumerGuard.start();
|
|
1226
1178
|
wss.on('connection', (client, req) => {
|
|
1227
1179
|
(0, ws_request_census_1.attachWsCensusSession)(httpRequestCensus, client, req, app.get(logging_service_1.LoggingService).createLogger('tRPC:ws'));
|
|
1180
|
+
wsSlowConsumerGuard.attach(client, (0, ws_slow_consumer_guard_js_1.wsConnectionFacts)(req));
|
|
1228
1181
|
});
|
|
1229
1182
|
(0, ws_1.applyWSSHandler)({
|
|
1230
1183
|
wss,
|
|
1231
1184
|
router: appRouter,
|
|
1185
|
+
// The complement of the bound above, for the SUSPENDED client: a PING is
|
|
1186
|
+
// a text frame the client's JS must answer, and a backgrounded app's JS
|
|
1187
|
+
// does not run — so the socket is terminated ~40 s into the background
|
|
1188
|
+
// instead of accumulating for the night, and the app reconnects on
|
|
1189
|
+
// return (every WS client of this hub is `@trpc/client` >= 11, which
|
|
1190
|
+
// answers PING with PONG; the viewer pins ^11.16). The bound stays: it
|
|
1191
|
+
// covers a client whose JS runs but whose link cannot drain the events.
|
|
1192
|
+
// A termination here closes with 1006; the guard's close line names it.
|
|
1193
|
+
keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 },
|
|
1232
1194
|
createContext: async (opts) => {
|
|
1233
1195
|
const wsCtx = await (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService, app.get(logging_service_1.LoggingService).createLogger('tRPC:ws'));
|
|
1234
1196
|
// The connection's identity, derived ONCE and reused for every
|
|
1235
1197
|
// operation on this socket. Frames that arrived while the bearer was
|
|
1236
1198
|
// still resolving are held by the session and released here.
|
|
1237
|
-
|
|
1199
|
+
const principal = (0, trpc_error_principal_1.describeTrpcPrincipal)(wsCtx.user);
|
|
1200
|
+
(0, ws_request_census_1.identifyWsCensusSession)(opts.res, principal);
|
|
1201
|
+
wsSlowConsumerGuard.identify(opts.res, principal);
|
|
1238
1202
|
return wsCtx;
|
|
1239
1203
|
},
|
|
1240
1204
|
onError: ({ path: trpcPath, error, ctx, input, }) => {
|
package/dist/manual-boot.js
CHANGED
|
@@ -77,9 +77,11 @@ const server_update_service_1 = require("./core/server-update/server-update.serv
|
|
|
77
77
|
const storage_service_1 = require("./core/storage/storage.service");
|
|
78
78
|
const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
|
|
79
79
|
const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
|
|
80
|
+
const update_availability_emitter_js_1 = require("./core/update-availability-emitter.js");
|
|
80
81
|
const update_availability_store_js_1 = require("./core/update-availability-store.js");
|
|
81
82
|
const agent_installed_packages_js_1 = require("./core/updates/agent-installed-packages.js");
|
|
82
83
|
const update_check_scheduler_js_1 = require("./core/updates/update-check-scheduler.js");
|
|
84
|
+
const wrapper_update_checker_js_1 = require("./core/updates/wrapper-update-checker.js");
|
|
83
85
|
// ---------------------------------------------------------------------------
|
|
84
86
|
// Service container — narrowing via `instanceof`, no casts.
|
|
85
87
|
// ---------------------------------------------------------------------------
|
|
@@ -252,6 +254,25 @@ async function bootManual(opts) {
|
|
|
252
254
|
// channel — that gate is why the only periodic publisher was dead on every
|
|
253
255
|
// live hub (`{"channel":"off"}`). See UpdateCheckScheduler.
|
|
254
256
|
const updateCheckLogger = loggingService.createLogger('UpdateCheck');
|
|
257
|
+
// The fifth target: the SHELL each node runs inside. Its own emitter scope,
|
|
258
|
+
// so a shell announcement and a code announcement never overwrite each
|
|
259
|
+
// other's dedup state — they are different facts about the same node (D437).
|
|
260
|
+
const wrapperUpdateChecker = new wrapper_update_checker_js_1.WrapperUpdateChecker({
|
|
261
|
+
logger: updateCheckLogger,
|
|
262
|
+
emitter: new update_availability_emitter_js_1.UpdateAvailabilityEmitter(eventBusService, { type: 'core', id: 'wrapper-update-check' }, new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'wrapper-update', loggingService.createLogger('UpdateAvailability'))),
|
|
263
|
+
readStatus: async (nodeId, isHub) => {
|
|
264
|
+
if (isHub)
|
|
265
|
+
return serverUpdateService.getServerPackageStatus();
|
|
266
|
+
const proxy = moleculerService.createCapabilityProxy('server-management', nodeId);
|
|
267
|
+
if (proxy === null) {
|
|
268
|
+
// Unreachable is not "the image is fine". Reject so the scheduler
|
|
269
|
+
// records a failed check instead of a clean bill.
|
|
270
|
+
throw new Error(`server-management unreachable on ${nodeId}`);
|
|
271
|
+
}
|
|
272
|
+
const status = await proxy['getServerPackageStatus']?.({});
|
|
273
|
+
return types_1.ServerPackageStatusSchema.parse(status);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
255
276
|
const updateCheckScheduler = new update_check_scheduler_js_1.UpdateCheckScheduler({
|
|
256
277
|
logger: updateCheckLogger,
|
|
257
278
|
getIntervalSeconds: () => addonPackageService.getAutoUpdateSettings().updateCheckIntervalSeconds,
|
|
@@ -278,6 +299,7 @@ async function bootManual(opts) {
|
|
|
278
299
|
}
|
|
279
300
|
return proxy['checkServerUpdate']?.({});
|
|
280
301
|
},
|
|
302
|
+
checkWrapperUpdate: (nodeId, isHub) => wrapperUpdateChecker.check(nodeId, isHub),
|
|
281
303
|
},
|
|
282
304
|
});
|
|
283
305
|
addonPackageService.setUpdateCheckRescheduler(() => updateCheckScheduler.reschedule());
|
|
@@ -77,6 +77,7 @@ var require_dist = __commonJS({
|
|
|
77
77
|
DEV_UPLOADS_DIRNAME: /* @__PURE__ */ __name(() => DEV_UPLOADS_DIRNAME, "DEV_UPLOADS_DIRNAME"),
|
|
78
78
|
DEV_UPLOADS_KEEP_COUNT: /* @__PURE__ */ __name(() => DEV_UPLOADS_KEEP_COUNT, "DEV_UPLOADS_KEEP_COUNT"),
|
|
79
79
|
DEV_UPLOAD_MANIFEST_FILE: /* @__PURE__ */ __name(() => DEV_UPLOAD_MANIFEST_FILE, "DEV_UPLOAD_MANIFEST_FILE"),
|
|
80
|
+
ELECTRON_APP_VERSION_ENV: /* @__PURE__ */ __name(() => ELECTRON_APP_VERSION_ENV, "ELECTRON_APP_VERSION_ENV"),
|
|
80
81
|
HOST_EXTERNAL_SPECIFIERS: /* @__PURE__ */ __name(() => HOST_EXTERNAL_SPECIFIERS, "HOST_EXTERNAL_SPECIFIERS"),
|
|
81
82
|
HUB_ROOT_SPEC: /* @__PURE__ */ __name(() => HUB_ROOT_SPEC2, "HUB_ROOT_SPEC"),
|
|
82
83
|
PENDING_ROOT_SWAP_FILE: /* @__PURE__ */ __name(() => PENDING_ROOT_SWAP_FILE, "PENDING_ROOT_SWAP_FILE"),
|
|
@@ -93,6 +94,7 @@ var require_dist = __commonJS({
|
|
|
93
94
|
currentDir: /* @__PURE__ */ __name(() => currentDir, "currentDir"),
|
|
94
95
|
currentEntryPath: /* @__PURE__ */ __name(() => currentEntryPath, "currentEntryPath"),
|
|
95
96
|
detectWorkspaceRoot: /* @__PURE__ */ __name(() => detectWorkspaceRoot, "detectWorkspaceRoot"),
|
|
97
|
+
detectWrapperIdentity: /* @__PURE__ */ __name(() => detectWrapperIdentity, "detectWrapperIdentity"),
|
|
96
98
|
devChannelEpoch: /* @__PURE__ */ __name(() => devChannelEpoch, "devChannelEpoch"),
|
|
97
99
|
devUploadManifestPath: /* @__PURE__ */ __name(() => devUploadManifestPath, "devUploadManifestPath"),
|
|
98
100
|
devUploadVersionDir: /* @__PURE__ */ __name(() => devUploadVersionDir, "devUploadVersionDir"),
|
|
@@ -110,6 +112,7 @@ var require_dist = __commonJS({
|
|
|
110
112
|
readPendingRootSwap: /* @__PURE__ */ __name(() => readPendingRootSwap, "readPendingRootSwap"),
|
|
111
113
|
readRestartIntentMarker: /* @__PURE__ */ __name(() => readRestartIntentMarker, "readRestartIntentMarker"),
|
|
112
114
|
readServerRootState: /* @__PURE__ */ __name(() => readServerRootState, "readServerRootState"),
|
|
115
|
+
readWrapperProbes: /* @__PURE__ */ __name(() => readWrapperProbes, "readWrapperProbes"),
|
|
113
116
|
registerActiveRootResolver: /* @__PURE__ */ __name(() => registerActiveRootResolver, "registerActiveRootResolver"),
|
|
114
117
|
restartIntentMarkerPath: /* @__PURE__ */ __name(() => restartIntentMarkerPath, "restartIntentMarkerPath"),
|
|
115
118
|
rootEntryPath: /* @__PURE__ */ __name(() => rootEntryPath2, "rootEntryPath"),
|
|
@@ -789,6 +792,93 @@ var require_dist = __commonJS({
|
|
|
789
792
|
};
|
|
790
793
|
}
|
|
791
794
|
__name(assessImageContract, "assessImageContract");
|
|
795
|
+
var ELECTRON_APP_VERSION_ENV = "CAMSTACK_AGENT_APP_VERSION";
|
|
796
|
+
var CONTAINER_CGROUP_MARKERS = [
|
|
797
|
+
"docker",
|
|
798
|
+
"containerd",
|
|
799
|
+
"kubepods",
|
|
800
|
+
"podman"
|
|
801
|
+
];
|
|
802
|
+
function cgroupNamesAContainer(contents) {
|
|
803
|
+
const lower = contents.toLowerCase();
|
|
804
|
+
return CONTAINER_CGROUP_MARKERS.some((marker) => lower.includes(marker));
|
|
805
|
+
}
|
|
806
|
+
__name(cgroupNamesAContainer, "cgroupNamesAContainer");
|
|
807
|
+
function cgroupDetail(contents) {
|
|
808
|
+
const lines = contents.split("\n").filter((line) => line.trim().length > 0);
|
|
809
|
+
const named = lines.find((line) => cgroupNamesAContainer(line));
|
|
810
|
+
return (named ?? lines[0] ?? "").trim().slice(0, 200);
|
|
811
|
+
}
|
|
812
|
+
__name(cgroupDetail, "cgroupDetail");
|
|
813
|
+
function detectWrapperIdentity(inputs) {
|
|
814
|
+
const { platform, dockerEnvFileExists, proc1Cgroup, electronAppVersion, seedVersion } = inputs;
|
|
815
|
+
const linux = platform === "linux";
|
|
816
|
+
const cgroupAnswered = proc1Cgroup !== null;
|
|
817
|
+
const cgroupSaysContainer = proc1Cgroup !== null && cgroupNamesAContainer(proc1Cgroup);
|
|
818
|
+
const containerObserved = dockerEnvFileExists || cgroupSaysContainer;
|
|
819
|
+
const electronDeclared = electronAppVersion !== void 0 && electronAppVersion.length > 0;
|
|
820
|
+
const containerRuledOut = !containerObserved && (cgroupAnswered || !linux);
|
|
821
|
+
const evidence = [
|
|
822
|
+
{
|
|
823
|
+
fact: "platform",
|
|
824
|
+
mode: "observed",
|
|
825
|
+
holds: linux,
|
|
826
|
+
detail: linux ? `process.platform=${platform} \u2014 a container is possible here` : `process.platform=${platform} \u2014 a linux container cannot run on this platform`
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
fact: "dockerenv-file",
|
|
830
|
+
mode: "observed",
|
|
831
|
+
holds: dockerEnvFileExists,
|
|
832
|
+
detail: dockerEnvFileExists ? "/.dockerenv exists" : "/.dockerenv not present"
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
fact: "proc-1-cgroup",
|
|
836
|
+
mode: "observed",
|
|
837
|
+
holds: cgroupSaysContainer,
|
|
838
|
+
detail: proc1Cgroup === null ? "/proc/1/cgroup unreadable \u2014 this probe did NOT answer" : cgroupDetail(proc1Cgroup)
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
fact: "electron-app-version-env",
|
|
842
|
+
mode: "declared",
|
|
843
|
+
holds: electronDeclared,
|
|
844
|
+
detail: electronDeclared ? `CAMSTACK_AGENT_APP_VERSION=${String(electronAppVersion)}` : "CAMSTACK_AGENT_APP_VERSION not set"
|
|
845
|
+
}
|
|
846
|
+
];
|
|
847
|
+
if (containerObserved && electronDeclared) {
|
|
848
|
+
return {
|
|
849
|
+
kind: "contradictory",
|
|
850
|
+
evidence,
|
|
851
|
+
currentVersion: null
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (containerObserved) {
|
|
855
|
+
return {
|
|
856
|
+
kind: "docker",
|
|
857
|
+
evidence,
|
|
858
|
+
currentVersion: seedVersion
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
if (electronDeclared) {
|
|
862
|
+
return {
|
|
863
|
+
kind: "electron",
|
|
864
|
+
evidence,
|
|
865
|
+
currentVersion: electronAppVersion ?? null
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if (containerRuledOut) {
|
|
869
|
+
return {
|
|
870
|
+
kind: "native",
|
|
871
|
+
evidence,
|
|
872
|
+
currentVersion: seedVersion
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
kind: "unknown",
|
|
877
|
+
evidence,
|
|
878
|
+
currentVersion: null
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
__name(detectWrapperIdentity, "detectWrapperIdentity");
|
|
792
882
|
var fs4 = __toESM2(require("fs"));
|
|
793
883
|
var path4 = __toESM2(require("path"));
|
|
794
884
|
function detectWorkspaceRoot(fromDir) {
|
|
@@ -930,6 +1020,26 @@ var require_dist = __commonJS({
|
|
|
930
1020
|
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
931
1021
|
var RESTART_REASON_PREFIX = "server-update";
|
|
932
1022
|
var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
1023
|
+
function readWrapperProbes() {
|
|
1024
|
+
let proc1Cgroup = null;
|
|
1025
|
+
try {
|
|
1026
|
+
proc1Cgroup = fs6.readFileSync("/proc/1/cgroup", "utf-8");
|
|
1027
|
+
} catch {
|
|
1028
|
+
proc1Cgroup = null;
|
|
1029
|
+
}
|
|
1030
|
+
let dockerEnvFileExists = false;
|
|
1031
|
+
try {
|
|
1032
|
+
dockerEnvFileExists = fs6.existsSync("/.dockerenv");
|
|
1033
|
+
} catch {
|
|
1034
|
+
dockerEnvFileExists = false;
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
platform: process.platform,
|
|
1038
|
+
dockerEnvFileExists,
|
|
1039
|
+
proc1Cgroup
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
__name(readWrapperProbes, "readWrapperProbes");
|
|
933
1043
|
function readPackageVersion(pkgJsonPath) {
|
|
934
1044
|
try {
|
|
935
1045
|
const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
|
|
@@ -956,6 +1066,7 @@ var require_dist = __commonJS({
|
|
|
956
1066
|
execNpm;
|
|
957
1067
|
ensureNativePrebuildsFn;
|
|
958
1068
|
env;
|
|
1069
|
+
readWrapperProbesFn;
|
|
959
1070
|
now;
|
|
960
1071
|
runningPackageJsonPath;
|
|
961
1072
|
workspaceProbeDir;
|
|
@@ -983,6 +1094,7 @@ var require_dist = __commonJS({
|
|
|
983
1094
|
});
|
|
984
1095
|
this.ensureNativePrebuildsFn = options.ensureNativePrebuilds ?? (async () => void 0);
|
|
985
1096
|
this.env = options.env ?? process.env;
|
|
1097
|
+
this.readWrapperProbesFn = options.readWrapperProbes ?? readWrapperProbes;
|
|
986
1098
|
this.now = options.now ?? Date.now;
|
|
987
1099
|
this.runningPackageJsonPath = options.runningPackageJsonPath;
|
|
988
1100
|
this.workspaceProbeDir = options.workspaceProbeDir;
|
|
@@ -1008,6 +1120,20 @@ var require_dist = __commonJS({
|
|
|
1008
1120
|
if (seedDir === void 0 || seedDir.length === 0) return null;
|
|
1009
1121
|
return readPackageVersion(path6.join(seedDir, "package.json"));
|
|
1010
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* What this node runs INSIDE. Observed where it can be (the container
|
|
1125
|
+
* probes), declared only where it cannot (the Electron app version, which a
|
|
1126
|
+
* child process has no other way to learn). A contradiction between the two
|
|
1127
|
+
* is reported as `contradictory`, never resolved — see `wrapper-identity.ts`
|
|
1128
|
+
* and D437.
|
|
1129
|
+
*/
|
|
1130
|
+
wrapperIdentity() {
|
|
1131
|
+
return detectWrapperIdentity({
|
|
1132
|
+
...this.readWrapperProbesFn(),
|
|
1133
|
+
electronAppVersion: this.env[ELECTRON_APP_VERSION_ENV],
|
|
1134
|
+
seedVersion: this.seedVersion()
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1011
1137
|
rootDir() {
|
|
1012
1138
|
return serverRootDir(this.dataDir);
|
|
1013
1139
|
}
|
|
@@ -1046,7 +1172,11 @@ var require_dist = __commonJS({
|
|
|
1046
1172
|
seedVersion: this.seedVersion(),
|
|
1047
1173
|
latestVersion,
|
|
1048
1174
|
runningVersion
|
|
1049
|
-
})
|
|
1175
|
+
}),
|
|
1176
|
+
// The other half of the same question. `imageContract` says the shell is
|
|
1177
|
+
// BEHIND; this says WHAT the shell is, which is the only thing that can
|
|
1178
|
+
// turn that verdict into an instruction the operator can act on.
|
|
1179
|
+
wrapper: this.wrapperIdentity()
|
|
1050
1180
|
};
|
|
1051
1181
|
}
|
|
1052
1182
|
// ── Check ─────────────────────────────────────────────────────────────
|