@camstack/server 1.1.25 → 1.1.27
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/core/hwaccel.router.js +63 -40
- package/dist/api/trpc/generated-cap-mounts.js +2 -1
- package/dist/api/trpc/generated-cap-routers.js +687 -582
- package/dist/api/trpc/trpc.router.js +5 -4
- package/dist/core/addon/addon-registry.service.js +26 -7
- package/dist/core/agent/agent-registry.service.js +82 -8
- package/dist/core/moleculer/moleculer.service.js +8 -6
- package/package.json +1 -1
|
@@ -143,10 +143,11 @@ function buildCapabilityRouters(services) {
|
|
|
143
143
|
live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
|
|
144
144
|
// stream-probe — fixed core API (ffprobe wrapper), not a cap.
|
|
145
145
|
streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
|
|
146
|
-
// hwaccel — fixed core API,
|
|
147
|
-
//
|
|
148
|
-
// which hardware backend each
|
|
149
|
-
|
|
146
|
+
// hwaccel — fixed core API, repointed onto the `platform-probe` cap
|
|
147
|
+
// (the sole cross-node-pinnable hwaccel surface). UI pipeline /
|
|
148
|
+
// NodeDetail pages query per-node to show which hardware backend each
|
|
149
|
+
// agent will use.
|
|
150
|
+
hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.capabilityRegistry, services.moleculer),
|
|
150
151
|
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
|
|
151
152
|
// ── Cap overrides: `mount: { kind: 'custom' }` ──────────────────
|
|
152
153
|
// `snapshot-provider.supportsDevice` is an OR across providers;
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AddonRegistryService = void 0;
|
|
37
|
+
exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
|
|
37
38
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
|
|
38
39
|
const os = __importStar(require("node:os"));
|
|
39
40
|
const addon_row_manifest_1 = require("./addon-row-manifest");
|
|
@@ -159,6 +160,19 @@ function parseDataPlaneEndpoints(raw) {
|
|
|
159
160
|
// shim that consumed it were deleted. All settings flow through the
|
|
160
161
|
// new `getAddonSettings / getGlobalSettings / getDeviceSettings`
|
|
161
162
|
// endpoints on the `addon-settings` singleton capability.
|
|
163
|
+
/**
|
|
164
|
+
* Whether the `capability:provider-registered` hook may emit `ready` for a
|
|
165
|
+
* capability. `platform-probe` OWNS its readiness protocol: its provider
|
|
166
|
+
* registers synchronously from `onInitialize` while the REAL probe
|
|
167
|
+
* (hardware + embedded-Python EP scoring) is still running async — the
|
|
168
|
+
* registration-time emit would flip it `ready` prematurely and probe-gated
|
|
169
|
+
* consumers (detection-pipeline engine auto-pick) would read
|
|
170
|
+
* accelerator-blind results. The addon emits `starting`/`ready`/`down`
|
|
171
|
+
* itself (`autoEmitReadiness=false`, ready on probe-done).
|
|
172
|
+
*/
|
|
173
|
+
function shouldEmitProviderRegisteredReady(capability) {
|
|
174
|
+
return capability !== 'platform-probe';
|
|
175
|
+
}
|
|
162
176
|
class AddonRegistryService {
|
|
163
177
|
loggingService;
|
|
164
178
|
eventBusService;
|
|
@@ -2048,13 +2062,18 @@ class AddonRegistryService {
|
|
|
2048
2062
|
// loops). The node-scoped emit is sufficient: the hydrate
|
|
2049
2063
|
// path on subprocess brokers replays whichever records the
|
|
2050
2064
|
// hub's `$readiness.getSnapshot` returns, scope and all.
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
}
|
|
2065
|
+
// Caps that OWN their readiness (platform-probe: registration
|
|
2066
|
+
// happens while its async probe still runs) are excluded —
|
|
2067
|
+
// this emit would flip them `ready` prematurely.
|
|
2068
|
+
if (shouldEmitProviderRegisteredReady(capability)) {
|
|
2069
|
+
try {
|
|
2070
|
+
this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
|
|
2071
|
+
}
|
|
2072
|
+
catch (err) {
|
|
2073
|
+
this.logger.warn('emitReady failed', {
|
|
2074
|
+
meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2058
2077
|
}
|
|
2059
2078
|
switch (capability) {
|
|
2060
2079
|
case 'storage': {
|
|
@@ -243,6 +243,14 @@ class AgentRegistryService {
|
|
|
243
243
|
* match — a single package can ship multiple addons with distinct ids
|
|
244
244
|
* and placements.
|
|
245
245
|
*
|
|
246
|
+
* Version-skew exception: a decl-id the hub has never heard of is NOT
|
|
247
|
+
* stale when the hub already ships that addon's PACKAGE. A newer agent
|
|
248
|
+
* bundle can add a decl-id within a package the hub also ships (e.g. a
|
|
249
|
+
* new `decoder-nodeav` inside `@camstack/addon-pipeline`); undeploying
|
|
250
|
+
* it would delete the whole shared bundle dir on the agent and crash
|
|
251
|
+
* every sibling runner. Only a decl-id whose PACKAGE is absent from the
|
|
252
|
+
* hub is treated as genuinely stale.
|
|
253
|
+
*
|
|
246
254
|
* All errors are caught and logged so a single bad agent never breaks
|
|
247
255
|
* the caller (connect handler or boot pass).
|
|
248
256
|
*/
|
|
@@ -268,14 +276,20 @@ class AgentRegistryService {
|
|
|
268
276
|
if (agentAddons.length === 0)
|
|
269
277
|
return;
|
|
270
278
|
// Build the hub's placement map: decl id → placement. Absence from
|
|
271
|
-
// this map means "not installed on the hub".
|
|
279
|
+
// this map means "not installed on the hub". Also collect the set of
|
|
280
|
+
// PACKAGE names the hub ships — used by the version-skew guard below.
|
|
272
281
|
const hubPlacements = new Map();
|
|
282
|
+
const hubPackages = new Set();
|
|
273
283
|
for (const row of this.addonRegistry.listAddons()) {
|
|
274
284
|
const declId = row.manifest.id;
|
|
275
285
|
if (typeof declId !== 'string')
|
|
276
286
|
continue;
|
|
277
287
|
const decl = row.declaration ?? row.manifest;
|
|
278
288
|
hubPlacements.set(declId, (0, types_1.resolveAddonPlacement)(decl));
|
|
289
|
+
const packageName = row.manifest.packageName;
|
|
290
|
+
if (typeof packageName === 'string' && packageName.length > 0) {
|
|
291
|
+
hubPackages.add(packageName);
|
|
292
|
+
}
|
|
279
293
|
}
|
|
280
294
|
const stale = agentAddons.filter((addon) => {
|
|
281
295
|
// Agent bootstrap infrastructure (storage/settings/metrics/logging from
|
|
@@ -289,9 +303,16 @@ class AgentRegistryService {
|
|
|
289
303
|
return false;
|
|
290
304
|
}
|
|
291
305
|
const placement = hubPlacements.get(addon.id);
|
|
292
|
-
|
|
293
|
-
|
|
306
|
+
if (placement === undefined) {
|
|
307
|
+
// decl-id unknown to the hub. If the hub ships this PACKAGE, this is a
|
|
308
|
+
// version-skew sibling (a newer agent bundle added a decl-id within a
|
|
309
|
+
// package the hub already ships) — undeploying it would delete the whole
|
|
310
|
+
// shared bundle on the agent. Only undeploy when the PACKAGE itself is
|
|
311
|
+
// absent from the hub.
|
|
312
|
+
if (addon.packageName !== undefined && hubPackages.has(addon.packageName))
|
|
313
|
+
return false;
|
|
294
314
|
return true;
|
|
315
|
+
}
|
|
295
316
|
// Installed but pinned to the hub → must not run on an agent.
|
|
296
317
|
return placement === 'hub-only';
|
|
297
318
|
});
|
|
@@ -386,12 +407,27 @@ class AgentRegistryService {
|
|
|
386
407
|
const hubEntry = await this.buildHubEntry(hubProcesses);
|
|
387
408
|
const remoteEntries = [];
|
|
388
409
|
const registry = this.moleculer.broker.registry;
|
|
389
|
-
|
|
410
|
+
// `onlyAvailable: false` intentionally includes nodes Moleculer still
|
|
411
|
+
// remembers as disconnected (`available: false`) — the same registry
|
|
412
|
+
// read `listNodeLiveness()` uses for `/health`. This surfaces recently
|
|
413
|
+
// offline agents inline instead of silently omitting them; it is NOT a
|
|
414
|
+
// new shadow map (no persistence, no timestamp field added anywhere) —
|
|
415
|
+
// once Moleculer's own `cleanOfflineNodesTimeout` purges the entry, the
|
|
416
|
+
// row disappears again. A hub-restart-surviving offline history is a
|
|
417
|
+
// separate, deliberately deferred piece of work.
|
|
418
|
+
const nodes = registry?.getNodeList?.({ onlyAvailable: false }) ?? [];
|
|
390
419
|
for (const node of nodes) {
|
|
391
420
|
const nodeId = node.id;
|
|
392
421
|
// Skip hub (already included) and child processes (contain '/')
|
|
393
422
|
if (nodeId === 'hub' || nodeId.includes('/'))
|
|
394
423
|
continue;
|
|
424
|
+
if (!node.available) {
|
|
425
|
+
// Offline: skip the `$agent.status`/`$process.list` RPC fan-out
|
|
426
|
+
// entirely (the node is unreachable — those calls would just time
|
|
427
|
+
// out) and surface a minimal, honestly-degraded row instead.
|
|
428
|
+
remoteEntries.push(this.buildOfflineEntry(node));
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
395
431
|
try {
|
|
396
432
|
const status = (await this.broker.call('$agent.status', {}, {
|
|
397
433
|
nodeID: nodeId,
|
|
@@ -465,12 +501,50 @@ class AgentRegistryService {
|
|
|
465
501
|
// Skip nodes without $agent service
|
|
466
502
|
}
|
|
467
503
|
}
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
504
|
+
// Offline agents ARE included above (see the `!node.available` branch)
|
|
505
|
+
// as long as Moleculer's own registry still remembers them. That window
|
|
506
|
+
// is transient — it does not survive a hub restart and disappears once
|
|
507
|
+
// `cleanOfflineNodesTimeout` purges the entry. A durable, restart-proof
|
|
508
|
+
// offline-node history is a separate, deliberately deferred piece of
|
|
509
|
+
// work (would need its own routing-blind store — never a `knownAgents`-
|
|
510
|
+
// style addition to `HubNodeRegistry`).
|
|
472
511
|
return [hubEntry, ...remoteEntries];
|
|
473
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Minimal, honestly-degraded entry for a node Moleculer's registry still
|
|
515
|
+
* remembers but marks `available: false`. No RPC round-trip is attempted
|
|
516
|
+
* (the node is unreachable by definition) so every live-only field is
|
|
517
|
+
* zeroed/defaulted rather than guessed. `connectedSince: Date.now()`
|
|
518
|
+
* keeps the downstream `uptime = Date.now() - connectedSince` topology
|
|
519
|
+
* projection at ~0 instead of asserting a stale/undefined value.
|
|
520
|
+
*/
|
|
521
|
+
buildOfflineEntry(node) {
|
|
522
|
+
return {
|
|
523
|
+
info: {
|
|
524
|
+
id: node.id,
|
|
525
|
+
name: node.id,
|
|
526
|
+
hostname: node.hostname ?? node.id,
|
|
527
|
+
capabilities: [],
|
|
528
|
+
platform: 'unknown',
|
|
529
|
+
arch: 'unknown',
|
|
530
|
+
cpuCores: 0,
|
|
531
|
+
memoryMB: 0,
|
|
532
|
+
},
|
|
533
|
+
localIps: [],
|
|
534
|
+
status: {
|
|
535
|
+
activeCameras: 0,
|
|
536
|
+
cpuPercent: 0,
|
|
537
|
+
memoryPercent: 0,
|
|
538
|
+
fps: {},
|
|
539
|
+
errors: [],
|
|
540
|
+
},
|
|
541
|
+
connectedSince: Date.now(),
|
|
542
|
+
isHub: false,
|
|
543
|
+
isOnline: false,
|
|
544
|
+
subProcesses: [],
|
|
545
|
+
agentAddons: [],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
474
548
|
async buildHubEntry(subProcesses = []) {
|
|
475
549
|
const cpus = os.cpus();
|
|
476
550
|
// Get live metrics from the metrics-provider capability (NativeMetricsProvider).
|
|
@@ -251,6 +251,14 @@ class MoleculerService {
|
|
|
251
251
|
// handler retries the hub-local route then throws a precise error rather
|
|
252
252
|
// than the unroutable broker fallback (`switch.switch.getStatus`).
|
|
253
253
|
isDeviceNativeCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.deviceNative === true,
|
|
254
|
+
// `nodeIdMode: 'data'` signal: for these caps (`addon-settings`,
|
|
255
|
+
// `addons`, `nodes`, `pipeline-orchestrator`) an inline `args.nodeId`
|
|
256
|
+
// is provider DATA — the hub singleton dispatches internally — never a
|
|
257
|
+
// routing pin. Without this a forked child's call carrying `nodeId`
|
|
258
|
+
// gets pinned to a node with no provider (dead broker fallback →
|
|
259
|
+
// ServiceNotFoundError). The kernel layer has no cap registry, so feed
|
|
260
|
+
// it the flag from the cap definition.
|
|
261
|
+
isDataNodeIdCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.nodeIdMode === 'data',
|
|
254
262
|
// Empty-collection fast-path: a `collection` cap's array-output method
|
|
255
263
|
// with ZERO providers registered cluster-wide IS the empty aggregate —
|
|
256
264
|
// return [] instead of routing (resolver miss → broker fallback → ~30s
|
|
@@ -388,12 +396,6 @@ class MoleculerService {
|
|
|
388
396
|
probe: (url, options) => this.streamProbe.probe(url, options),
|
|
389
397
|
probeField: (key, value) => this.streamProbe.probeField(key, value),
|
|
390
398
|
}));
|
|
391
|
-
// Register `$hwaccel` on hub — every node in the cluster does the
|
|
392
|
-
// same so `broker.call('$hwaccel.resolve', params, { nodeID })`
|
|
393
|
-
// returns the backend list for whichever host the caller targets.
|
|
394
|
-
// Admin UI uses this to show per-agent hwaccel info on the
|
|
395
|
-
// pipeline / NodeDetail pages.
|
|
396
|
-
this.brokerSafe.createService((0, system_1.createHwAccelService)((0, system_1.createKernelHwAccel)()));
|
|
397
399
|
await this.brokerSafe.start();
|
|
398
400
|
logger.info('Moleculer broker started (TCP transport)');
|
|
399
401
|
// Construct the CapRouteResolver now that both the broker and
|