@camstack/server 1.2.127 → 1.2.129
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/agent-update-service.js +14 -11
- package/dist/api/core/cap-providers.js +4 -15
- package/dist/core/addon/addon-package.service.js +182 -39
- package/dist/core/logging/logging.service.js +8 -5
- package/dist/core/server-update/server-update.service.js +3 -6
- package/dist/core/streaming/stream-probe.service.js +25 -5
- package/dist/core/streaming/ttl-cache.js +72 -0
- package/dist/core/update-availability-emitter.js +42 -1
- package/dist/core/update-availability-store.js +148 -0
- package/dist/core/updates/agent-installed-packages.js +30 -0
- package/dist/core/updates/update-check-scheduler.js +136 -0
- package/dist/manual-boot.js +50 -1
- package/package.json +7 -7
|
@@ -63,12 +63,13 @@ exports.agentRuntimeManifestEntry = agentRuntimeManifestEntry;
|
|
|
63
63
|
const node_crypto_1 = require("node:crypto");
|
|
64
64
|
const fs = __importStar(require("node:fs"));
|
|
65
65
|
const path = __importStar(require("node:path"));
|
|
66
|
-
const index_js_1 = require("../server-root/index.js");
|
|
67
|
-
const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
|
|
68
|
-
const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
|
|
69
66
|
const system_1 = require("@camstack/system");
|
|
70
67
|
const types_1 = require("@camstack/types");
|
|
68
|
+
const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
|
|
69
|
+
const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
|
|
71
70
|
const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
|
|
71
|
+
const update_availability_store_js_1 = require("../core/update-availability-store.js");
|
|
72
|
+
const index_js_1 = require("../server-root/index.js");
|
|
72
73
|
/**
|
|
73
74
|
* The synthetic addonId the agent's OWN runtime registers infra providers
|
|
74
75
|
* under (no addon owns them — the bootstrap does). Appears in the agent's
|
|
@@ -151,6 +152,8 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
151
152
|
eventBus;
|
|
152
153
|
nodeId;
|
|
153
154
|
agentDataDir;
|
|
155
|
+
/** The base class keeps `logger` private; hold our own for the store. */
|
|
156
|
+
agentLogger;
|
|
154
157
|
constructor(options) {
|
|
155
158
|
const restartAgent = options.restartAgent ??
|
|
156
159
|
((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir));
|
|
@@ -189,22 +192,22 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
189
192
|
});
|
|
190
193
|
this.nodeId = options.nodeId ?? process.env['CAMSTACK_NODE_ID'] ?? 'agent';
|
|
191
194
|
this.agentDataDir = options.dataDir;
|
|
195
|
+
this.agentLogger = options.logger;
|
|
192
196
|
this.eventBus = options.eventBus ?? null;
|
|
193
197
|
this.updateAvailability =
|
|
194
198
|
options.eventBus !== undefined
|
|
195
|
-
? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, {
|
|
196
|
-
type: 'core',
|
|
197
|
-
id: 'agent-update-service',
|
|
198
|
-
})
|
|
199
|
+
? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, { type: 'core', id: 'agent-update-service' }, options.updateAvailabilityStore)
|
|
199
200
|
: null;
|
|
200
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Bind the agent's real event bus once the mesh is up. The dedup state is
|
|
204
|
+
* persisted under the agent's data dir from here on — an agent restarts on
|
|
205
|
+
* every root update, which is exactly when its availability list is longest.
|
|
206
|
+
*/
|
|
201
207
|
bindAvailability(eventBus, nodeId) {
|
|
202
208
|
this.eventBus = eventBus;
|
|
203
209
|
this.nodeId = nodeId;
|
|
204
|
-
this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(eventBus, {
|
|
205
|
-
type: 'core',
|
|
206
|
-
id: 'agent-update-service',
|
|
207
|
-
});
|
|
210
|
+
this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(eventBus, { type: 'core', id: 'agent-update-service' }, new update_availability_store_js_1.FileUpdateAvailabilityStore(this.agentDataDir, 'agent-update', this.agentLogger));
|
|
208
211
|
}
|
|
209
212
|
async checkServerUpdate() {
|
|
210
213
|
const result = await super.checkServerUpdate();
|
|
@@ -76,6 +76,7 @@ const server_1 = require("@trpc/server");
|
|
|
76
76
|
const integration_id_backfill_1 = require("../../boot/integration-id-backfill");
|
|
77
77
|
const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
|
|
78
78
|
const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
|
|
79
|
+
const agent_installed_packages_js_1 = require("../../core/updates/agent-installed-packages.js");
|
|
79
80
|
const collection_preference_js_1 = require("./collection-preference.js");
|
|
80
81
|
const site_location_js_1 = require("./site-location.js");
|
|
81
82
|
// ── system ──────────────────────────────────────────────────────────
|
|
@@ -1220,18 +1221,6 @@ function isHubNode(nodeId) {
|
|
|
1220
1221
|
* filter it out to make the list look clean — the number reaching zero is the
|
|
1221
1222
|
* point of the number.
|
|
1222
1223
|
*/
|
|
1223
|
-
async function fetchAgentInstalledPackages(broker, nodeId) {
|
|
1224
|
-
const status = await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: 5_000 });
|
|
1225
|
-
const out = [];
|
|
1226
|
-
for (const a of status.addons ?? []) {
|
|
1227
|
-
if (typeof a.packageName !== 'string' || typeof a.version !== 'string')
|
|
1228
|
-
continue;
|
|
1229
|
-
if ((0, addon_package_service_js_1.isFrameworkPackage)(a.packageName))
|
|
1230
|
-
continue;
|
|
1231
|
-
out.push({ name: a.packageName, version: a.version });
|
|
1232
|
-
}
|
|
1233
|
-
return out;
|
|
1234
|
-
}
|
|
1235
1224
|
function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
1236
1225
|
const broker = moleculer.broker;
|
|
1237
1226
|
const frameworkAllowSet = new Set([addon_package_service_js_1.SYSTEM_PACKAGE]);
|
|
@@ -1277,7 +1266,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1277
1266
|
const nodeId = input.nodeId;
|
|
1278
1267
|
const updates = nodeId === undefined || isHubNode(nodeId)
|
|
1279
1268
|
? await ps.checkUpdates()
|
|
1280
|
-
: await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId), nodeId);
|
|
1269
|
+
: await ps.checkUpdatesForInstalled(await (0, agent_installed_packages_js_1.fetchAgentInstalledPackages)(broker, nodeId), nodeId);
|
|
1281
1270
|
return updates.map((u) => ({ ...u, isSystem: frameworkAllowSet.has(u.name) }));
|
|
1282
1271
|
},
|
|
1283
1272
|
updatePackage: async (input) => {
|
|
@@ -1331,7 +1320,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1331
1320
|
if (nodeId === undefined || isHubNode(nodeId))
|
|
1332
1321
|
return ps.checkUpdates(true);
|
|
1333
1322
|
// Agent rosters carry no hub-side cache — the diff is always live.
|
|
1334
|
-
const installed = await fetchAgentInstalledPackages(broker, nodeId);
|
|
1323
|
+
const installed = await (0, agent_installed_packages_js_1.fetchAgentInstalledPackages)(broker, nodeId);
|
|
1335
1324
|
return ps.checkUpdatesForInstalled(installed, nodeId);
|
|
1336
1325
|
},
|
|
1337
1326
|
restartServer: async () => ps.restartServer(ctx.user?.username ?? ctx.user?.id),
|
|
@@ -1399,7 +1388,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1399
1388
|
return { success: true };
|
|
1400
1389
|
},
|
|
1401
1390
|
getAutoUpdateSettings: async () => ps.getAutoUpdateSettings(),
|
|
1402
|
-
setAutoUpdateSettings: async (input) => ps.setAutoUpdateSettings(input.channel, input.intervalSeconds),
|
|
1391
|
+
setAutoUpdateSettings: async (input) => ps.setAutoUpdateSettings(input.channel, input.intervalSeconds, input.updateCheckIntervalSeconds),
|
|
1403
1392
|
getAddonAutoUpdate: async (input) => ps.getAddonAutoUpdate(input.addonId),
|
|
1404
1393
|
setAddonAutoUpdate: async (input) => ps.setAddonAutoUpdate(input.addonId, input.channel),
|
|
1405
1394
|
applyAutoUpdateToAll: async (input) => {
|
|
@@ -45,8 +45,9 @@ const path = __importStar(require("node:path"));
|
|
|
45
45
|
const node_util_1 = require("node:util");
|
|
46
46
|
const system_1 = require("@camstack/system");
|
|
47
47
|
const types_1 = require("@camstack/types");
|
|
48
|
-
const package_dir_utils_js_1 = require("./package-dir-utils.js");
|
|
49
48
|
const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
|
|
49
|
+
const update_check_scheduler_js_1 = require("../updates/update-check-scheduler.js");
|
|
50
|
+
const package_dir_utils_js_1 = require("./package-dir-utils.js");
|
|
50
51
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
51
52
|
/**
|
|
52
53
|
* The primary host-provided framework package (kernel + core). Kept as a named
|
|
@@ -193,10 +194,20 @@ class AddonPackageService {
|
|
|
193
194
|
updateAvailability;
|
|
194
195
|
// -- Auto-update state ----------------------------------------------------
|
|
195
196
|
autoUpdateConfig = {
|
|
196
|
-
global: {
|
|
197
|
+
global: {
|
|
198
|
+
channel: 'off',
|
|
199
|
+
intervalSeconds: 21600,
|
|
200
|
+
updateCheckIntervalSeconds: update_check_scheduler_js_1.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS,
|
|
201
|
+
},
|
|
197
202
|
overrides: {},
|
|
198
203
|
};
|
|
199
204
|
autoUpdateTimer = null;
|
|
205
|
+
/**
|
|
206
|
+
* Re-arm hook for the availability poller, wired at boot (manual-boot). Kept
|
|
207
|
+
* as a callback rather than a service reference so this module never imports
|
|
208
|
+
* the scheduler's dependencies back.
|
|
209
|
+
*/
|
|
210
|
+
updateCheckRescheduler = null;
|
|
200
211
|
// -- Timing constants -----------------------------------------------------
|
|
201
212
|
// Short TTL — operators expect to see freshly-published versions
|
|
202
213
|
// within minutes of `npm publish`, not hours. The Addons page kicks
|
|
@@ -207,7 +218,12 @@ class AddonPackageService {
|
|
|
207
218
|
static VERSION_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
208
219
|
static NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
209
220
|
static REGISTRY_TIMEOUT_MS = 10_000;
|
|
210
|
-
constructor(loggingService, eventBusService, configService, addonRegistry, notificationService, toastService
|
|
221
|
+
constructor(loggingService, eventBusService, configService, addonRegistry, notificationService, toastService,
|
|
222
|
+
/**
|
|
223
|
+
* Durable dedup state for `update.available`. Injected by boot wiring
|
|
224
|
+
* (manual-boot) — unit tests leave it out and get the RAM-only emitter.
|
|
225
|
+
*/
|
|
226
|
+
updateAvailabilityStore) {
|
|
211
227
|
this.loggingService = loggingService;
|
|
212
228
|
this.eventBusService = eventBusService;
|
|
213
229
|
this.configService = configService;
|
|
@@ -215,10 +231,7 @@ class AddonPackageService {
|
|
|
215
231
|
this.notificationService = notificationService;
|
|
216
232
|
this.toastService = toastService;
|
|
217
233
|
this.logger = this.loggingService.createLogger('AddonPackageService');
|
|
218
|
-
this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(this.eventBusService, {
|
|
219
|
-
type: 'core',
|
|
220
|
-
id: 'addon-package-service',
|
|
221
|
-
});
|
|
234
|
+
this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(this.eventBusService, { type: 'core', id: 'addon-package-service' }, updateAvailabilityStore);
|
|
222
235
|
// Initialize installer eagerly (no async needed).
|
|
223
236
|
// Ensures install/uninstall works before full module init completes.
|
|
224
237
|
try {
|
|
@@ -613,8 +626,8 @@ class AddonPackageService {
|
|
|
613
626
|
this.logger.info('Checking for package updates...');
|
|
614
627
|
const updates = [];
|
|
615
628
|
// Check installed addon packages in addons dir
|
|
616
|
-
const
|
|
617
|
-
updates.push(...
|
|
629
|
+
const sweep = await this.checkAddonPackageUpdates();
|
|
630
|
+
updates.push(...sweep.updates);
|
|
618
631
|
this.logger.info('Found package updates', {
|
|
619
632
|
meta: {
|
|
620
633
|
count: updates.length,
|
|
@@ -629,12 +642,7 @@ class AddonPackageService {
|
|
|
629
642
|
updates,
|
|
630
643
|
expiresAt: now + AddonPackageService.UPDATE_CACHE_TTL_MS,
|
|
631
644
|
};
|
|
632
|
-
this.
|
|
633
|
-
target: 'addon',
|
|
634
|
-
packageName: update.name,
|
|
635
|
-
currentVersion: update.currentVersion,
|
|
636
|
-
latestVersion: update.latestVersion,
|
|
637
|
-
})));
|
|
645
|
+
this.publishAddonAvailability(updates, sweep.failures);
|
|
638
646
|
return updates;
|
|
639
647
|
}
|
|
640
648
|
/** Clear the cached update check results */
|
|
@@ -659,6 +667,7 @@ class AddonPackageService {
|
|
|
659
667
|
seen.set(pkg.name, pkg.version);
|
|
660
668
|
}
|
|
661
669
|
const updates = [];
|
|
670
|
+
let failures = 0;
|
|
662
671
|
await Promise.all([...seen].map(async ([name, version]) => {
|
|
663
672
|
if (!this.isAllowedPackage(name))
|
|
664
673
|
return;
|
|
@@ -667,7 +676,16 @@ class AddonPackageService {
|
|
|
667
676
|
// keeps the UI honest (no dead "Update" button for @camstack/server).
|
|
668
677
|
if (exports.AUTO_UPDATE_EXCLUDED_PACKAGES.has(name))
|
|
669
678
|
return;
|
|
670
|
-
const
|
|
679
|
+
const lookup = await this.lookupLatestVersion(name);
|
|
680
|
+
if (!lookup.ok) {
|
|
681
|
+
failures += 1;
|
|
682
|
+
this.logger.warn('Registry lookup failed for installed package', {
|
|
683
|
+
tags: { nodeId: nodeId ?? 'hub' },
|
|
684
|
+
meta: { name, error: lookup.error },
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const latestVersion = lookup.latestVersion;
|
|
671
689
|
if (latestVersion === null || !isVersionNewer(latestVersion, version))
|
|
672
690
|
return;
|
|
673
691
|
const category = this.categorize(name);
|
|
@@ -679,13 +697,7 @@ class AddonPackageService {
|
|
|
679
697
|
requiresRestart: category === 'core',
|
|
680
698
|
});
|
|
681
699
|
}));
|
|
682
|
-
this.
|
|
683
|
-
target: 'addon',
|
|
684
|
-
packageName: update.name,
|
|
685
|
-
currentVersion: update.currentVersion,
|
|
686
|
-
latestVersion: update.latestVersion,
|
|
687
|
-
...(nodeId !== undefined ? { nodeId } : {}),
|
|
688
|
-
})), nodeId);
|
|
700
|
+
this.publishAddonAvailability(updates, failures, nodeId);
|
|
689
701
|
return updates;
|
|
690
702
|
}
|
|
691
703
|
/**
|
|
@@ -1165,6 +1177,7 @@ class AddonPackageService {
|
|
|
1165
1177
|
const hubPkgDir = (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(packageName);
|
|
1166
1178
|
const buildId = hubPkgDir !== null ? (0, package_dir_utils_js_1.computeDistBuildId)(path.join(hubPkgDir, 'dist')) : null;
|
|
1167
1179
|
let latestVersion = null;
|
|
1180
|
+
let lookupFailed = false;
|
|
1168
1181
|
try {
|
|
1169
1182
|
const args = [
|
|
1170
1183
|
'view',
|
|
@@ -1177,21 +1190,73 @@ class AddonPackageService {
|
|
|
1177
1190
|
latestVersion = trimmed.length > 0 ? trimmed : null;
|
|
1178
1191
|
}
|
|
1179
1192
|
catch (err) {
|
|
1193
|
+
lookupFailed = true;
|
|
1180
1194
|
this.logger.debug('listFrameworkPackages: npm view failed', {
|
|
1181
1195
|
meta: { packageName, error: (0, types_1.errMsg)(err) },
|
|
1182
1196
|
});
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
// STRICTLY newer, never merely different. The hub runs
|
|
1199
|
+
// `1.2.x-dev.<timestamp>` builds that are AHEAD of the npm tag, and
|
|
1200
|
+
// `latest !== current` lit an "update available" badge whose only
|
|
1201
|
+
// possible outcome was a downgrade. `publishFrameworkAvailability`
|
|
1202
|
+
// already gated on this; the row the UI reads did not, so the same
|
|
1203
|
+
// page disagreed with itself.
|
|
1204
|
+
const hasUpdate = latestVersion !== null &&
|
|
1205
|
+
currentVersion !== 'unknown' &&
|
|
1206
|
+
isVersionNewer(latestVersion, currentVersion);
|
|
1185
1207
|
return {
|
|
1186
1208
|
packageName,
|
|
1187
1209
|
currentVersion,
|
|
1188
1210
|
latestVersion,
|
|
1189
1211
|
hasUpdate,
|
|
1190
1212
|
buildId,
|
|
1213
|
+
lookupFailed,
|
|
1191
1214
|
...(description !== undefined ? { description } : {}),
|
|
1192
1215
|
};
|
|
1193
1216
|
}));
|
|
1194
|
-
|
|
1217
|
+
this.publishFrameworkAvailability(rows);
|
|
1218
|
+
return rows.map(({ lookupFailed: _lookupFailed, ...row }) => row);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Announce framework updates. This surface SHOWED `@camstack/system` had an
|
|
1222
|
+
* update and emitted nothing, so the operator was never told — it was the
|
|
1223
|
+
* only discovery path with no publish at all.
|
|
1224
|
+
*
|
|
1225
|
+
* Two rules it must not break:
|
|
1226
|
+
* - **Strictly newer only.** `hasUpdate` is `latest !== current`, which is
|
|
1227
|
+
* true for a hub running a `-dev.<timestamp>` build AHEAD of the npm tag.
|
|
1228
|
+
* Announcing that would be telling the operator to downgrade.
|
|
1229
|
+
* - **A failed lookup publishes nothing.** Not even an empty snapshot: an
|
|
1230
|
+
* empty snapshot means "up to date" and would clear the dedup state, so
|
|
1231
|
+
* the next successful poll would re-announce everything.
|
|
1232
|
+
*/
|
|
1233
|
+
publishFrameworkAvailability(rows) {
|
|
1234
|
+
const failed = rows.filter((row) => row.lookupFailed);
|
|
1235
|
+
if (failed.length > 0) {
|
|
1236
|
+
this.logger.warn('Framework update check incomplete — availability not published', {
|
|
1237
|
+
meta: { packages: failed.map((row) => row.packageName) },
|
|
1238
|
+
});
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const candidates = [];
|
|
1242
|
+
for (const row of rows) {
|
|
1243
|
+
if (row.latestVersion === null || row.currentVersion === 'unknown')
|
|
1244
|
+
continue;
|
|
1245
|
+
if (!isVersionNewer(row.latestVersion, row.currentVersion))
|
|
1246
|
+
continue;
|
|
1247
|
+
candidates.push({
|
|
1248
|
+
target: 'server',
|
|
1249
|
+
packageName: row.packageName,
|
|
1250
|
+
currentVersion: row.currentVersion,
|
|
1251
|
+
latestVersion: row.latestVersion,
|
|
1252
|
+
nodeId: this.resolveNodeId(),
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
this.updateAvailability.publishSnapshot('server', candidates, this.resolveNodeId());
|
|
1256
|
+
}
|
|
1257
|
+
/** Node id stamped on availability events emitted by THIS process. */
|
|
1258
|
+
resolveNodeId() {
|
|
1259
|
+
return process.env['CAMSTACK_NODE_ID'] ?? 'hub';
|
|
1195
1260
|
}
|
|
1196
1261
|
// =========================================================================
|
|
1197
1262
|
// Reload
|
|
@@ -1224,21 +1289,39 @@ class AddonPackageService {
|
|
|
1224
1289
|
// =========================================================================
|
|
1225
1290
|
// Auto-update settings
|
|
1226
1291
|
// =========================================================================
|
|
1292
|
+
/**
|
|
1293
|
+
* Register the availability poller's re-arm hook. Called once at boot so a
|
|
1294
|
+
* `setAutoUpdateSettings` write takes effect without a restart.
|
|
1295
|
+
*/
|
|
1296
|
+
setUpdateCheckRescheduler(reschedule) {
|
|
1297
|
+
this.updateCheckRescheduler = reschedule;
|
|
1298
|
+
}
|
|
1227
1299
|
/** Get global auto-update settings */
|
|
1228
1300
|
getAutoUpdateSettings() {
|
|
1229
1301
|
return { ...this.autoUpdateConfig.global };
|
|
1230
1302
|
}
|
|
1231
1303
|
/** Set global auto-update settings and restart the timer */
|
|
1232
|
-
async setAutoUpdateSettings(channel, intervalSeconds) {
|
|
1304
|
+
async setAutoUpdateSettings(channel, intervalSeconds, updateCheckIntervalSeconds) {
|
|
1305
|
+
const previousCheckInterval = this.autoUpdateConfig.global.updateCheckIntervalSeconds;
|
|
1306
|
+
const nextCheckInterval = updateCheckIntervalSeconds === undefined
|
|
1307
|
+
? previousCheckInterval
|
|
1308
|
+
: (0, update_check_scheduler_js_1.clampUpdateCheckInterval)(updateCheckIntervalSeconds);
|
|
1233
1309
|
this.autoUpdateConfig = {
|
|
1234
1310
|
...this.autoUpdateConfig,
|
|
1235
1311
|
global: {
|
|
1236
1312
|
channel,
|
|
1237
1313
|
intervalSeconds: intervalSeconds ?? this.autoUpdateConfig.global.intervalSeconds,
|
|
1314
|
+
updateCheckIntervalSeconds: nextCheckInterval,
|
|
1238
1315
|
},
|
|
1239
1316
|
};
|
|
1240
1317
|
this.saveAutoUpdateConfig();
|
|
1241
1318
|
this.scheduleAutoUpdate();
|
|
1319
|
+
if (updateCheckIntervalSeconds === undefined)
|
|
1320
|
+
return;
|
|
1321
|
+
this.logger.info('Update-check interval changed', {
|
|
1322
|
+
meta: { from: previousCheckInterval, to: nextCheckInterval },
|
|
1323
|
+
});
|
|
1324
|
+
this.updateCheckRescheduler?.();
|
|
1242
1325
|
}
|
|
1243
1326
|
/** Get per-addon auto-update override */
|
|
1244
1327
|
getAddonAutoUpdate(addonId) {
|
|
@@ -1395,10 +1478,14 @@ class AddonPackageService {
|
|
|
1395
1478
|
const global = asRecord(raw['global']);
|
|
1396
1479
|
const channel = asString(global['channel']);
|
|
1397
1480
|
const validChannel = channel === 'latest' || channel === 'beta' ? channel : 'off';
|
|
1481
|
+
const rawCheckInterval = global['updateCheckIntervalSeconds'];
|
|
1398
1482
|
return {
|
|
1399
1483
|
global: {
|
|
1400
1484
|
channel: validChannel,
|
|
1401
1485
|
intervalSeconds: typeof global['intervalSeconds'] === 'number' ? global['intervalSeconds'] : 3600,
|
|
1486
|
+
updateCheckIntervalSeconds: typeof rawCheckInterval === 'number'
|
|
1487
|
+
? (0, update_check_scheduler_js_1.clampUpdateCheckInterval)(rawCheckInterval)
|
|
1488
|
+
: update_check_scheduler_js_1.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS,
|
|
1402
1489
|
},
|
|
1403
1490
|
overrides: Object.fromEntries(Object.entries(asRecord(raw['overrides'])).map(([k, v]) => {
|
|
1404
1491
|
const s = asString(v);
|
|
@@ -1414,7 +1501,14 @@ class AddonPackageService {
|
|
|
1414
1501
|
meta: { error: (0, types_1.errMsg)(err) },
|
|
1415
1502
|
});
|
|
1416
1503
|
}
|
|
1417
|
-
return {
|
|
1504
|
+
return {
|
|
1505
|
+
global: {
|
|
1506
|
+
channel: 'off',
|
|
1507
|
+
intervalSeconds: 21600,
|
|
1508
|
+
updateCheckIntervalSeconds: update_check_scheduler_js_1.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS,
|
|
1509
|
+
},
|
|
1510
|
+
overrides: {},
|
|
1511
|
+
};
|
|
1418
1512
|
}
|
|
1419
1513
|
/** Save auto-update config to disk */
|
|
1420
1514
|
saveAutoUpdateConfig() {
|
|
@@ -1440,12 +1534,17 @@ class AddonPackageService {
|
|
|
1440
1534
|
/**
|
|
1441
1535
|
* Check addon packages for updates by reading installed versions from
|
|
1442
1536
|
* data/addons/{name}/package.json and comparing against npm registry.
|
|
1537
|
+
*
|
|
1538
|
+
* Reports `failures` alongside the diff: a registry lookup that could not be
|
|
1539
|
+
* MADE is not evidence that a package is up to date, and the availability
|
|
1540
|
+
* publish branches on it.
|
|
1443
1541
|
*/
|
|
1444
1542
|
async checkAddonPackageUpdates() {
|
|
1445
1543
|
const addonsDir = this.resolveAddonsDir();
|
|
1446
1544
|
const updates = [];
|
|
1545
|
+
let failures = 0;
|
|
1447
1546
|
if (!fs.existsSync(addonsDir))
|
|
1448
|
-
return updates;
|
|
1547
|
+
return { updates, failures };
|
|
1449
1548
|
// Collect all package.json paths -- handles both flat and scoped layouts
|
|
1450
1549
|
const pkgJsonPaths = [];
|
|
1451
1550
|
const topDirs = fs
|
|
@@ -1491,7 +1590,16 @@ class AddonPackageService {
|
|
|
1491
1590
|
if (source === 'workspace')
|
|
1492
1591
|
continue;
|
|
1493
1592
|
}
|
|
1494
|
-
const
|
|
1593
|
+
const lookup = await this.lookupLatestVersion(name);
|
|
1594
|
+
if (!lookup.ok) {
|
|
1595
|
+
failures += 1;
|
|
1596
|
+
this.logger.warn('Registry lookup failed for installed addon package', {
|
|
1597
|
+
tags: { nodeId: this.resolveNodeId() },
|
|
1598
|
+
meta: { name, error: lookup.error },
|
|
1599
|
+
});
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
const latestVersion = lookup.latestVersion;
|
|
1495
1603
|
if (!latestVersion)
|
|
1496
1604
|
continue;
|
|
1497
1605
|
if (isVersionNewer(latestVersion, version)) {
|
|
@@ -1505,13 +1613,16 @@ class AddonPackageService {
|
|
|
1505
1613
|
}
|
|
1506
1614
|
}
|
|
1507
1615
|
catch (error) {
|
|
1616
|
+
// The package could not be evaluated at all (unreadable manifest, …).
|
|
1617
|
+
// Counted as a failure for the same reason a registry miss is.
|
|
1618
|
+
failures += 1;
|
|
1508
1619
|
const msg = (0, types_1.errMsg)(error);
|
|
1509
|
-
this.logger.
|
|
1620
|
+
this.logger.warn('Failed to check updates for addon', {
|
|
1510
1621
|
meta: { pkgJsonPath, error: msg },
|
|
1511
1622
|
});
|
|
1512
1623
|
}
|
|
1513
1624
|
}
|
|
1514
|
-
return updates;
|
|
1625
|
+
return { updates, failures };
|
|
1515
1626
|
}
|
|
1516
1627
|
// =========================================================================
|
|
1517
1628
|
// Private: npm registry helpers
|
|
@@ -1522,7 +1633,7 @@ class AddonPackageService {
|
|
|
1522
1633
|
* Honours `CAMSTACK_NPM_REGISTRY` so update checks resolve against
|
|
1523
1634
|
* the same registry the installer/pack paths use. Without this, a
|
|
1524
1635
|
* per-node `listUpdates` (which diffs an agent's roster via
|
|
1525
|
-
* `checkUpdatesForInstalled` → `
|
|
1636
|
+
* `checkUpdatesForInstalled` → `lookupLatestVersion`) would bypass a
|
|
1526
1637
|
* private registry — including the e2e harness's verdaccio — and
|
|
1527
1638
|
* silently report "no update" for packages that only exist there.
|
|
1528
1639
|
* Trailing slashes are stripped so the `${base}/${name}` join is clean.
|
|
@@ -1532,29 +1643,61 @@ class AddonPackageService {
|
|
|
1532
1643
|
const base = override && override.length > 0 ? override : AddonPackageService.NPM_REGISTRY;
|
|
1533
1644
|
return base.replace(/\/+$/, '');
|
|
1534
1645
|
}
|
|
1535
|
-
/**
|
|
1536
|
-
|
|
1646
|
+
/**
|
|
1647
|
+
* Ask the registry for a package's latest version, distinguishing "there is
|
|
1648
|
+
* no such package" (a definitive answer) from "I could not ask" (a failure).
|
|
1649
|
+
* The difference decides whether an availability SNAPSHOT may be published —
|
|
1650
|
+
* see {@link RegistryLookup}.
|
|
1651
|
+
*/
|
|
1652
|
+
async lookupLatestVersion(packageName) {
|
|
1537
1653
|
try {
|
|
1538
1654
|
const encodedName = packageName.replace('/', '%2F');
|
|
1539
1655
|
const url = `${this.resolveRegistryBase()}/${encodedName}/latest`;
|
|
1540
1656
|
const response = await fetch(url, {
|
|
1541
1657
|
signal: AbortSignal.timeout(AddonPackageService.REGISTRY_TIMEOUT_MS),
|
|
1542
1658
|
});
|
|
1659
|
+
if (response.status === 404)
|
|
1660
|
+
return { ok: true, latestVersion: null };
|
|
1543
1661
|
if (!response.ok) {
|
|
1544
1662
|
this.logger.debug('Registry returned non-ok status', {
|
|
1545
1663
|
meta: { packageName, status: response.status },
|
|
1546
1664
|
});
|
|
1547
|
-
return
|
|
1665
|
+
return { ok: false, error: `registry status ${response.status}` };
|
|
1548
1666
|
}
|
|
1549
1667
|
const data = await fetchJsonObject(response);
|
|
1550
1668
|
const version = asString(data['version']);
|
|
1551
|
-
return version || null;
|
|
1669
|
+
return { ok: true, latestVersion: version || null };
|
|
1552
1670
|
}
|
|
1553
1671
|
catch (error) {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1672
|
+
return { ok: false, error: (0, types_1.errMsg)(error) };
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Announce the addon-update set for one scope (hub or a node).
|
|
1677
|
+
*
|
|
1678
|
+
* `failures > 0` means at least one registry lookup could not be made, so
|
|
1679
|
+
* the set is INCOMPLETE — publish it as candidates (additive) rather than a
|
|
1680
|
+
* snapshot (authoritative). A snapshot built from a partial sweep silently
|
|
1681
|
+
* marks the unreachable packages "up to date", clears their dedup state and
|
|
1682
|
+
* re-announces them on the next good poll.
|
|
1683
|
+
*/
|
|
1684
|
+
publishAddonAvailability(updates, failures, nodeId) {
|
|
1685
|
+
const candidates = updates.map((update) => ({
|
|
1686
|
+
target: 'addon',
|
|
1687
|
+
packageName: update.name,
|
|
1688
|
+
currentVersion: update.currentVersion,
|
|
1689
|
+
latestVersion: update.latestVersion,
|
|
1690
|
+
...(nodeId !== undefined ? { nodeId } : {}),
|
|
1691
|
+
}));
|
|
1692
|
+
if (failures > 0) {
|
|
1693
|
+
this.logger.warn('Addon update check incomplete — availability published as partial', {
|
|
1694
|
+
tags: { nodeId: nodeId ?? 'hub' },
|
|
1695
|
+
meta: { failures, published: candidates.length },
|
|
1696
|
+
});
|
|
1697
|
+
this.updateAvailability.publishCandidates(candidates);
|
|
1698
|
+
return;
|
|
1557
1699
|
}
|
|
1700
|
+
this.updateAvailability.publishSnapshot('addon', candidates, nodeId);
|
|
1558
1701
|
}
|
|
1559
1702
|
/** Fetch npm search results for camstack addon packages (cached 5 min) */
|
|
1560
1703
|
async fetchSearchFromNpm() {
|
|
@@ -28,11 +28,14 @@ class LoggingService extends system_1.LogManager {
|
|
|
28
28
|
// keep their sparse history. `eventBus.ringBufferSize` still sizes the
|
|
29
29
|
// separate system-event ring; logs get their own per-addon cap.
|
|
30
30
|
const perAddonCapacity = configService.get('eventBus.perAddonLogBufferSize') ?? 5000;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
31
|
+
// Total ceiling across all buckets. Left UNSET here on purpose so the buffer
|
|
32
|
+
// applies its own default (`DEFAULT_MAX_TOTAL_LOG_ENTRIES`): the per-addon
|
|
33
|
+
// rings are a hard bound only per bucket, and their product grows with the
|
|
34
|
+
// roster — hub-main ingests from every local runner AND every agent, so
|
|
35
|
+
// "unbounded by default" meant ~300k retained entries (~92MB) on the one
|
|
36
|
+
// process that must fit inside the cgroup. An operator who genuinely wants
|
|
37
|
+
// no aggregate bound writes an explicit `null`, which is preserved.
|
|
38
|
+
const maxTotalEntries = configService.get('eventBus.maxTotalLogBufferSize');
|
|
36
39
|
// Only entries at or below this level are ever discarded to meet the total.
|
|
37
40
|
const pruneLevel = configService.get('eventBus.logBufferPruneLevel') ?? 'debug';
|
|
38
41
|
super(perAddonCapacity, { maxTotalEntries, pruneLevel });
|
|
@@ -53,9 +53,9 @@ exports.ServerUpdateService = void 0;
|
|
|
53
53
|
*/
|
|
54
54
|
const path = __importStar(require("node:path"));
|
|
55
55
|
const index_js_1 = require("../../server-root/index.js");
|
|
56
|
-
const system_exec_npm_js_1 = require("./system-exec-npm.js");
|
|
57
|
-
const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
|
|
58
56
|
const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
|
|
57
|
+
const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
|
|
58
|
+
const system_exec_npm_js_1 = require("./system-exec-npm.js");
|
|
59
59
|
class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
60
60
|
updateAvailability;
|
|
61
61
|
nodeId;
|
|
@@ -95,10 +95,7 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
|
95
95
|
});
|
|
96
96
|
this.updateAvailability =
|
|
97
97
|
options.eventBus !== undefined
|
|
98
|
-
? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, {
|
|
99
|
-
type: 'core',
|
|
100
|
-
id: 'server-update-service',
|
|
101
|
-
})
|
|
98
|
+
? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, { type: 'core', id: 'server-update-service' }, options.updateAvailabilityStore)
|
|
102
99
|
: null;
|
|
103
100
|
this.nodeId = options.nodeId ?? 'hub';
|
|
104
101
|
}
|
|
@@ -3,17 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.StreamProbeService = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const util_1 = require("util");
|
|
6
|
+
const ttl_cache_1 = require("./ttl-cache");
|
|
6
7
|
const types_1 = require("@camstack/types");
|
|
7
8
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
8
9
|
const CACHE_TTL_MS = 3_600_000; // 1 hour
|
|
9
10
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
11
|
+
/**
|
|
12
|
+
* Ceiling on cached probes.
|
|
13
|
+
*
|
|
14
|
+
* The cache is keyed by stream URL, and a URL is not a bounded quantity: every
|
|
15
|
+
* credential rotation, every edited field, every per-camera probe of a value
|
|
16
|
+
* that was later changed mints a new key. 512 is far above any real camera
|
|
17
|
+
* roster (the live cluster runs 27), so a working deployment never evicts;
|
|
18
|
+
* the ceiling exists so the key space cannot be walked into the OOM.
|
|
19
|
+
*/
|
|
20
|
+
const CACHE_MAX_ENTRIES = 512;
|
|
10
21
|
/** Codec aliases normalised to canonical names. */
|
|
11
22
|
const CODEC_ALIASES = {
|
|
12
23
|
hevc: 'h265',
|
|
13
24
|
};
|
|
14
25
|
class StreamProbeService {
|
|
15
26
|
logger;
|
|
16
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Probe results, bounded by BOTH a swept TTL and an entry ceiling.
|
|
29
|
+
*
|
|
30
|
+
* It used to be a plain `Map` whose age was consulted only when the same URL
|
|
31
|
+
* was probed again — so an entry nobody asked about a second time was never
|
|
32
|
+
* found expired and never removed. See {@link TtlCache}.
|
|
33
|
+
*/
|
|
34
|
+
cache = new ttl_cache_1.TtlCache({
|
|
35
|
+
ttlMs: CACHE_TTL_MS,
|
|
36
|
+
maxEntries: CACHE_MAX_ENTRIES,
|
|
37
|
+
});
|
|
17
38
|
constructor(loggingService) {
|
|
18
39
|
this.logger = loggingService.createLogger('StreamProbeService');
|
|
19
40
|
}
|
|
@@ -25,12 +46,11 @@ class StreamProbeService {
|
|
|
25
46
|
const force = options?.force ?? false;
|
|
26
47
|
if (!force) {
|
|
27
48
|
const cached = this.cache.get(url);
|
|
28
|
-
if (cached
|
|
29
|
-
return cached
|
|
30
|
-
}
|
|
49
|
+
if (cached)
|
|
50
|
+
return cached;
|
|
31
51
|
}
|
|
32
52
|
const metadata = await this.runProbe(url);
|
|
33
|
-
this.cache.set(url,
|
|
53
|
+
this.cache.set(url, metadata);
|
|
34
54
|
return metadata;
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TtlCache = void 0;
|
|
4
|
+
class TtlCache {
|
|
5
|
+
entries = new Map();
|
|
6
|
+
ttlMs;
|
|
7
|
+
maxEntries;
|
|
8
|
+
now;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.ttlMs = options.ttlMs;
|
|
11
|
+
this.maxEntries = options.maxEntries;
|
|
12
|
+
this.now = options.now ?? (() => Date.now());
|
|
13
|
+
}
|
|
14
|
+
/** The live value, or undefined when absent or expired. An expired entry is
|
|
15
|
+
* DELETED here, not merely reported missing. */
|
|
16
|
+
get(key) {
|
|
17
|
+
const slot = this.entries.get(key);
|
|
18
|
+
if (slot === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const at = this.now();
|
|
21
|
+
if (at - slot.storedAt >= this.ttlMs) {
|
|
22
|
+
this.entries.delete(key);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
slot.lastAccessAt = at;
|
|
26
|
+
return slot.value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store a value, then bring the map back inside both bounds.
|
|
30
|
+
*
|
|
31
|
+
* The sweep runs on WRITE and not on a timer on purpose: a timer would have
|
|
32
|
+
* to be owned, unref'd and stopped by every holder of a cache, and a cache
|
|
33
|
+
* that is never written to is a cache that is not growing.
|
|
34
|
+
*/
|
|
35
|
+
set(key, value) {
|
|
36
|
+
const at = this.now();
|
|
37
|
+
this.entries.set(key, { value, storedAt: at, lastAccessAt: at });
|
|
38
|
+
this.sweepExpired(at);
|
|
39
|
+
this.enforceMaxEntries(key);
|
|
40
|
+
}
|
|
41
|
+
delete(key) {
|
|
42
|
+
this.entries.delete(key);
|
|
43
|
+
}
|
|
44
|
+
clear() {
|
|
45
|
+
this.entries.clear();
|
|
46
|
+
}
|
|
47
|
+
/** Entries currently retained. The number that used to only go up. */
|
|
48
|
+
size() {
|
|
49
|
+
return this.entries.size;
|
|
50
|
+
}
|
|
51
|
+
sweepExpired(at) {
|
|
52
|
+
for (const [key, slot] of this.entries) {
|
|
53
|
+
if (at - slot.storedAt >= this.ttlMs)
|
|
54
|
+
this.entries.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Evict least-recently-USED first. `protectedKey` is the entry just written —
|
|
58
|
+
* evicting it would make `set` a no-op. */
|
|
59
|
+
enforceMaxEntries(protectedKey) {
|
|
60
|
+
if (this.entries.size <= this.maxEntries)
|
|
61
|
+
return;
|
|
62
|
+
const coldestFirst = [...this.entries.entries()]
|
|
63
|
+
.filter(([key]) => key !== protectedKey)
|
|
64
|
+
.toSorted((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
65
|
+
for (const [key] of coldestFirst) {
|
|
66
|
+
if (this.entries.size <= this.maxEntries)
|
|
67
|
+
return;
|
|
68
|
+
this.entries.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.TtlCache = TtlCache;
|
|
@@ -14,15 +14,33 @@ const types_1 = require("@camstack/types");
|
|
|
14
14
|
*
|
|
15
15
|
* A successful empty snapshot clears prior state for that scope; failed checks
|
|
16
16
|
* should not call this method.
|
|
17
|
+
*
|
|
18
|
+
* When a {@link IUpdateAvailabilityStore} is injected the dedup state is
|
|
19
|
+
* DURABLE: a restart does not re-announce what the operator already saw, and a
|
|
20
|
+
* genuinely newer version still announces. Without one the emitter stays
|
|
21
|
+
* RAM-only (the shape every unit test uses).
|
|
17
22
|
*/
|
|
18
23
|
class UpdateAvailabilityEmitter {
|
|
19
24
|
eventBus;
|
|
20
25
|
source;
|
|
26
|
+
store;
|
|
21
27
|
latestByKey = new Map();
|
|
22
28
|
held = new Map();
|
|
23
|
-
|
|
29
|
+
/** Set when in-memory state diverged from what the store last saw. */
|
|
30
|
+
dirty = false;
|
|
31
|
+
constructor(eventBus, source, store) {
|
|
24
32
|
this.eventBus = eventBus;
|
|
25
33
|
this.source = source;
|
|
34
|
+
this.store = store;
|
|
35
|
+
const persisted = store?.load() ?? null;
|
|
36
|
+
if (persisted === null)
|
|
37
|
+
return;
|
|
38
|
+
for (const [key, version] of Object.entries(persisted.latestByKey)) {
|
|
39
|
+
this.latestByKey.set(key, version);
|
|
40
|
+
}
|
|
41
|
+
for (const [key, candidate] of Object.entries(persisted.held)) {
|
|
42
|
+
this.held.set(key, candidate);
|
|
43
|
+
}
|
|
26
44
|
}
|
|
27
45
|
publishSnapshot(target, candidates, nodeId) {
|
|
28
46
|
const scope = nodeId ?? 'hub';
|
|
@@ -44,7 +62,9 @@ class UpdateAvailabilityEmitter {
|
|
|
44
62
|
continue;
|
|
45
63
|
this.held.delete(key);
|
|
46
64
|
this.latestByKey.delete(key);
|
|
65
|
+
this.dirty = true;
|
|
47
66
|
}
|
|
67
|
+
this.persistIfDirty();
|
|
48
68
|
if (!latestChanged)
|
|
49
69
|
return;
|
|
50
70
|
this.emitList(target);
|
|
@@ -58,6 +78,7 @@ class UpdateAvailabilityEmitter {
|
|
|
58
78
|
if (this.note(candidate))
|
|
59
79
|
latestChanged = true;
|
|
60
80
|
}
|
|
81
|
+
this.persistIfDirty();
|
|
61
82
|
if (!latestChanged)
|
|
62
83
|
return;
|
|
63
84
|
for (const target of targets)
|
|
@@ -66,10 +87,30 @@ class UpdateAvailabilityEmitter {
|
|
|
66
87
|
note(candidate) {
|
|
67
88
|
const key = this.key(candidate);
|
|
68
89
|
const previous = this.latestByKey.get(key);
|
|
90
|
+
const previousHeld = this.held.get(key);
|
|
91
|
+
if (previousHeld === undefined ||
|
|
92
|
+
previousHeld.currentVersion !== candidate.currentVersion ||
|
|
93
|
+
previousHeld.latestVersion !== candidate.latestVersion ||
|
|
94
|
+
previousHeld.nodeId !== candidate.nodeId) {
|
|
95
|
+
this.dirty = true;
|
|
96
|
+
}
|
|
69
97
|
this.held.set(key, candidate);
|
|
70
98
|
this.latestByKey.set(key, candidate.latestVersion);
|
|
71
99
|
return previous !== candidate.latestVersion;
|
|
72
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Flush to the durable store. Called BEFORE the emit so a crash between the
|
|
103
|
+
* two costs a duplicate announcement, never a silent one.
|
|
104
|
+
*/
|
|
105
|
+
persistIfDirty() {
|
|
106
|
+
if (!this.dirty)
|
|
107
|
+
return;
|
|
108
|
+
this.dirty = false;
|
|
109
|
+
this.store?.save({
|
|
110
|
+
latestByKey: Object.fromEntries(this.latestByKey),
|
|
111
|
+
held: Object.fromEntries(this.held),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
73
114
|
emitList(target) {
|
|
74
115
|
const list = [...this.held.values()].filter((candidate) => candidate.target === target);
|
|
75
116
|
if (list.length === 0)
|
|
@@ -0,0 +1,148 @@
|
|
|
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.FileUpdateAvailabilityStore = exports.UPDATE_AVAILABILITY_DIR = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Durable backing for {@link UpdateAvailabilityEmitter}'s dedup state.
|
|
39
|
+
*
|
|
40
|
+
* The emitter's whole job is "announce a (package, version) ONCE". Keeping
|
|
41
|
+
* that in RAM meant every hub restart re-announced everything the operator had
|
|
42
|
+
* already seen — and hubs restart on every framework update, which is exactly
|
|
43
|
+
* the moment the list is longest.
|
|
44
|
+
*
|
|
45
|
+
* The mechanism is deliberately the one the neighbouring update code already
|
|
46
|
+
* uses: a small JSON file under the node's data dir, next to
|
|
47
|
+
* `auto-update.json`, `runtime-state.json` and `.restart-pending`. No new
|
|
48
|
+
* database, no settings-store dependency (the emitter runs on the AGENT too,
|
|
49
|
+
* where the hub's SQLite settings store does not exist).
|
|
50
|
+
*/
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const types_1 = require("@camstack/types");
|
|
54
|
+
/** Directory under the data dir that holds one file per emitter scope. */
|
|
55
|
+
exports.UPDATE_AVAILABILITY_DIR = 'update-availability';
|
|
56
|
+
/**
|
|
57
|
+
* One file per emitter scope (`addon-packages`, `server-update`,
|
|
58
|
+
* `agent-update`) so two emitters in the same process never overwrite each
|
|
59
|
+
* other's dedup state.
|
|
60
|
+
*/
|
|
61
|
+
class FileUpdateAvailabilityStore {
|
|
62
|
+
scopeId;
|
|
63
|
+
logger;
|
|
64
|
+
filePath;
|
|
65
|
+
constructor(dataDir, scopeId, logger) {
|
|
66
|
+
this.scopeId = scopeId;
|
|
67
|
+
this.logger = logger;
|
|
68
|
+
this.filePath = path.join(dataDir, exports.UPDATE_AVAILABILITY_DIR, `${scopeId}.json`);
|
|
69
|
+
}
|
|
70
|
+
load() {
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(this.filePath))
|
|
73
|
+
return null;
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
|
|
75
|
+
return parseState(parsed);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// A corrupt file must not look like "nothing was ever announced" AND
|
|
79
|
+
// must not crash boot. It re-announces once, then heals on the next save.
|
|
80
|
+
this.logger.warn('Update-availability state unreadable; starting from empty', {
|
|
81
|
+
meta: { scopeId: this.scopeId, path: this.filePath, error: (0, types_1.errMsg)(error) },
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
save(state) {
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
89
|
+
const tmp = `${this.filePath}.tmp`;
|
|
90
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
91
|
+
fs.renameSync(tmp, this.filePath);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Losing a write degrades to the old RAM-only behaviour (a re-announce
|
|
95
|
+
// after restart) — never a failed update check.
|
|
96
|
+
this.logger.warn('Failed to persist update-availability state', {
|
|
97
|
+
meta: { scopeId: this.scopeId, path: this.filePath, error: (0, types_1.errMsg)(error) },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.FileUpdateAvailabilityStore = FileUpdateAvailabilityStore;
|
|
103
|
+
function parseState(raw) {
|
|
104
|
+
if (raw === null || typeof raw !== 'object')
|
|
105
|
+
return null;
|
|
106
|
+
const latestRaw = Reflect.get(raw, 'latestByKey');
|
|
107
|
+
const heldRaw = Reflect.get(raw, 'held');
|
|
108
|
+
if (latestRaw === null || typeof latestRaw !== 'object')
|
|
109
|
+
return null;
|
|
110
|
+
if (heldRaw === null || typeof heldRaw !== 'object')
|
|
111
|
+
return null;
|
|
112
|
+
const latestByKey = {};
|
|
113
|
+
for (const [key, value] of Object.entries(latestRaw)) {
|
|
114
|
+
if (typeof value === 'string')
|
|
115
|
+
latestByKey[key] = value;
|
|
116
|
+
}
|
|
117
|
+
const held = {};
|
|
118
|
+
for (const [key, value] of Object.entries(heldRaw)) {
|
|
119
|
+
const candidate = parseCandidate(value);
|
|
120
|
+
if (candidate !== null)
|
|
121
|
+
held[key] = candidate;
|
|
122
|
+
}
|
|
123
|
+
return { latestByKey, held };
|
|
124
|
+
}
|
|
125
|
+
function parseCandidate(raw) {
|
|
126
|
+
if (raw === null || typeof raw !== 'object')
|
|
127
|
+
return null;
|
|
128
|
+
const target = Reflect.get(raw, 'target');
|
|
129
|
+
const packageName = Reflect.get(raw, 'packageName');
|
|
130
|
+
const currentVersion = Reflect.get(raw, 'currentVersion');
|
|
131
|
+
const latestVersion = Reflect.get(raw, 'latestVersion');
|
|
132
|
+
const nodeId = Reflect.get(raw, 'nodeId');
|
|
133
|
+
if (target !== 'addon' && target !== 'server')
|
|
134
|
+
return null;
|
|
135
|
+
if (typeof packageName !== 'string')
|
|
136
|
+
return null;
|
|
137
|
+
if (typeof currentVersion !== 'string')
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof latestVersion !== 'string')
|
|
140
|
+
return null;
|
|
141
|
+
return {
|
|
142
|
+
target,
|
|
143
|
+
packageName,
|
|
144
|
+
currentVersion,
|
|
145
|
+
latestVersion,
|
|
146
|
+
...(typeof nodeId === 'string' ? { nodeId } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_STATUS_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.fetchAgentInstalledPackages = fetchAgentInstalledPackages;
|
|
5
|
+
/**
|
|
6
|
+
* The agent addon roster, as the hub sees it.
|
|
7
|
+
*
|
|
8
|
+
* An agent has no npm runtime: it reports what it has installed via
|
|
9
|
+
* `$agent.status` and the hub does the registry diff
|
|
10
|
+
* (`AddonPackageService.checkUpdatesForInstalled`). Extracted from
|
|
11
|
+
* `cap-providers.ts` so the availability poller and the `addons.listUpdates`
|
|
12
|
+
* cap route read the roster through ONE function — a second copy would drift
|
|
13
|
+
* the framework filter and make the two surfaces disagree about what is
|
|
14
|
+
* installed on a node.
|
|
15
|
+
*/
|
|
16
|
+
const addon_package_service_js_1 = require("../addon/addon-package.service.js");
|
|
17
|
+
/** Timeout for `$agent.status`; a node that cannot answer this fast is unwell. */
|
|
18
|
+
exports.AGENT_STATUS_TIMEOUT_MS = 5_000;
|
|
19
|
+
async function fetchAgentInstalledPackages(broker, nodeId) {
|
|
20
|
+
const status = await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: exports.AGENT_STATUS_TIMEOUT_MS });
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const addon of status.addons ?? []) {
|
|
23
|
+
if (typeof addon.packageName !== 'string' || typeof addon.version !== 'string')
|
|
24
|
+
continue;
|
|
25
|
+
if ((0, addon_package_service_js_1.isFrameworkPackage)(addon.packageName))
|
|
26
|
+
continue;
|
|
27
|
+
out.push({ name: addon.packageName, version: addon.version });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UpdateCheckScheduler = exports.MAX_UPDATE_CHECK_INTERVAL_SECONDS = exports.MIN_UPDATE_CHECK_INTERVAL_SECONDS = exports.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS = void 0;
|
|
4
|
+
exports.clampUpdateCheckInterval = clampUpdateCheckInterval;
|
|
5
|
+
const types_1 = require("@camstack/types");
|
|
6
|
+
/**
|
|
7
|
+
* Default poll cadence: 6 hours. Registry lookups are cheap but not free, and
|
|
8
|
+
* an operator who is told about a publish within a quarter-day is told in time.
|
|
9
|
+
*/
|
|
10
|
+
exports.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS = 6 * 60 * 60;
|
|
11
|
+
/**
|
|
12
|
+
* Floor: 15 minutes. Below this the sweep starts overlapping its own npm
|
|
13
|
+
* lookups on a cluster of any size, and the notification stops being a
|
|
14
|
+
* notification. `setAutoUpdateSettings` clamps to this rather than refusing,
|
|
15
|
+
* so a bad value degrades to "poll briskly" instead of "poll never".
|
|
16
|
+
*/
|
|
17
|
+
exports.MIN_UPDATE_CHECK_INTERVAL_SECONDS = 15 * 60;
|
|
18
|
+
/** Ceiling: 7 days. Anything longer is indistinguishable from disabled. */
|
|
19
|
+
exports.MAX_UPDATE_CHECK_INTERVAL_SECONDS = 7 * 24 * 60 * 60;
|
|
20
|
+
/** Grace after boot before the first sweep, so registry I/O never races boot. */
|
|
21
|
+
const DEFAULT_INITIAL_DELAY_MS = 90_000;
|
|
22
|
+
/** Clamp an operator-supplied interval into the supported window. */
|
|
23
|
+
function clampUpdateCheckInterval(seconds) {
|
|
24
|
+
if (!Number.isFinite(seconds))
|
|
25
|
+
return exports.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS;
|
|
26
|
+
const whole = Math.floor(seconds);
|
|
27
|
+
if (whole < exports.MIN_UPDATE_CHECK_INTERVAL_SECONDS)
|
|
28
|
+
return exports.MIN_UPDATE_CHECK_INTERVAL_SECONDS;
|
|
29
|
+
if (whole > exports.MAX_UPDATE_CHECK_INTERVAL_SECONDS)
|
|
30
|
+
return exports.MAX_UPDATE_CHECK_INTERVAL_SECONDS;
|
|
31
|
+
return whole;
|
|
32
|
+
}
|
|
33
|
+
class UpdateCheckScheduler {
|
|
34
|
+
logger;
|
|
35
|
+
targets;
|
|
36
|
+
getIntervalSeconds;
|
|
37
|
+
initialDelayMs;
|
|
38
|
+
timer = null;
|
|
39
|
+
bootTimer = null;
|
|
40
|
+
intervalSeconds = exports.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS;
|
|
41
|
+
sweeping = false;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.logger = options.logger;
|
|
44
|
+
this.targets = options.targets;
|
|
45
|
+
this.getIntervalSeconds = options.getIntervalSeconds;
|
|
46
|
+
this.initialDelayMs = options.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
47
|
+
}
|
|
48
|
+
/** Interval currently armed, after clamping. Exposed for assertions + logs. */
|
|
49
|
+
currentIntervalSeconds() {
|
|
50
|
+
return this.intervalSeconds;
|
|
51
|
+
}
|
|
52
|
+
/** Arm the poller. Safe to call twice — the previous timers are replaced. */
|
|
53
|
+
start() {
|
|
54
|
+
this.arm();
|
|
55
|
+
if (this.bootTimer !== null)
|
|
56
|
+
clearTimeout(this.bootTimer);
|
|
57
|
+
this.bootTimer = setTimeout(() => {
|
|
58
|
+
this.bootTimer = null;
|
|
59
|
+
void this.runSweep();
|
|
60
|
+
}, this.initialDelayMs);
|
|
61
|
+
this.bootTimer.unref?.();
|
|
62
|
+
}
|
|
63
|
+
/** Re-read the interval from settings and re-arm if it moved. */
|
|
64
|
+
reschedule() {
|
|
65
|
+
const next = clampUpdateCheckInterval(this.getIntervalSeconds());
|
|
66
|
+
if (this.timer !== null && next === this.intervalSeconds)
|
|
67
|
+
return;
|
|
68
|
+
this.arm();
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
if (this.timer !== null)
|
|
72
|
+
clearInterval(this.timer);
|
|
73
|
+
if (this.bootTimer !== null)
|
|
74
|
+
clearTimeout(this.bootTimer);
|
|
75
|
+
this.timer = null;
|
|
76
|
+
this.bootTimer = null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One full sweep. Never throws and never runs concurrently with itself — an
|
|
80
|
+
* overlapping tick (slow registry, long agent fan-out) is dropped WITH a log
|
|
81
|
+
* rather than doubling the npm traffic.
|
|
82
|
+
*/
|
|
83
|
+
async runSweep() {
|
|
84
|
+
if (this.sweeping) {
|
|
85
|
+
this.logger.warn('Update check skipped — previous sweep still running');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.sweeping = true;
|
|
89
|
+
try {
|
|
90
|
+
const nodes = this.targets.listNodes();
|
|
91
|
+
await this.attempt('hub addon packages', 'hub', () => this.targets.checkHubAddons());
|
|
92
|
+
await this.attempt('framework packages', 'hub', () => this.targets.checkFrameworkPackages());
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
if (!node.isOnline) {
|
|
95
|
+
// D58: never dispatch at a node the registry already called offline —
|
|
96
|
+
// and a dropped branch is never silent.
|
|
97
|
+
this.logger.warn('Update check skipped — node offline', { tags: { nodeId: node.id } });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
await this.attempt('node server package', node.id, () => this.targets.checkNodeServerUpdate(node.id, node.isHub));
|
|
101
|
+
if (node.isHub)
|
|
102
|
+
continue;
|
|
103
|
+
await this.attempt('agent addon packages', node.id, () => this.targets.checkAgentAddons(node.id));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
this.sweeping = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
arm() {
|
|
111
|
+
if (this.timer !== null)
|
|
112
|
+
clearInterval(this.timer);
|
|
113
|
+
this.intervalSeconds = clampUpdateCheckInterval(this.getIntervalSeconds());
|
|
114
|
+
this.logger.info('Update check scheduled', {
|
|
115
|
+
meta: { intervalSeconds: this.intervalSeconds },
|
|
116
|
+
});
|
|
117
|
+
this.timer = setInterval(() => {
|
|
118
|
+
void this.runSweep();
|
|
119
|
+
}, this.intervalSeconds * 1_000);
|
|
120
|
+
this.timer.unref?.();
|
|
121
|
+
}
|
|
122
|
+
async attempt(what, nodeId, run) {
|
|
123
|
+
try {
|
|
124
|
+
await run();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// Logged, not published: an empty snapshot would read as "up to date"
|
|
128
|
+
// and destroy the dedup state the operator's notifications rely on.
|
|
129
|
+
this.logger.warn(`Update check failed — ${what}`, {
|
|
130
|
+
tags: { nodeId },
|
|
131
|
+
meta: { error: (0, types_1.errMsg)(error) },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.UpdateCheckScheduler = UpdateCheckScheduler;
|
package/dist/manual-boot.js
CHANGED
|
@@ -77,6 +77,9 @@ 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_store_js_1 = require("./core/update-availability-store.js");
|
|
81
|
+
const agent_installed_packages_js_1 = require("./core/updates/agent-installed-packages.js");
|
|
82
|
+
const update_check_scheduler_js_1 = require("./core/updates/update-check-scheduler.js");
|
|
80
83
|
// ---------------------------------------------------------------------------
|
|
81
84
|
// Service container — narrowing via `instanceof`, no casts.
|
|
82
85
|
// ---------------------------------------------------------------------------
|
|
@@ -157,7 +160,10 @@ async function bootManual(opts) {
|
|
|
157
160
|
// AddonWidgetsService — needs AddonRegistryService for the bundled-addon
|
|
158
161
|
// dist sub-folder lookup (see service docstring).
|
|
159
162
|
const addonWidgetsService = new addon_widgets_service_1.AddonWidgetsService(loggingService, capabilityService, addonRegistryService);
|
|
160
|
-
|
|
163
|
+
// Durable `update.available` dedup lives next to `auto-update.json`, so a
|
|
164
|
+
// hub restart never re-announces what the operator already saw.
|
|
165
|
+
const availabilityDataDir = path.resolve(process.env['CAMSTACK_DATA'] ?? 'camstack-data');
|
|
166
|
+
const addonPackageService = new addon_package_service_1.AddonPackageService(loggingService, eventBusService, configService, addonRegistryService, notificationWrapper, toastWrapper, new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'addon-packages', loggingService.createLogger('UpdateAvailability')));
|
|
161
167
|
// Addon back-fill delivery seam. Wired here (not in the AgentRegistryService
|
|
162
168
|
// constructor) because it needs AddonPackageService, which is a later layer.
|
|
163
169
|
// Bytes come from THE HUB'S OWN INSTALLED COPY whenever it has one, and only
|
|
@@ -234,9 +240,45 @@ async function bootManual(opts) {
|
|
|
234
240
|
logger: loggingService.createLogger('ServerUpdate'),
|
|
235
241
|
restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
|
|
236
242
|
eventBus: eventBusService,
|
|
243
|
+
updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'server-update', loggingService.createLogger('UpdateAvailability')),
|
|
237
244
|
});
|
|
238
245
|
const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService, (0, cap_providers_1.createNodeRootPackageLookup)(moleculerService, serverUpdateService));
|
|
239
246
|
const postBootService = new post_boot_service_1.PostBootService(addonRegistryService, eventBusService, loggingService);
|
|
247
|
+
// ---- Update availability poller ---------------------------------------
|
|
248
|
+
// "Tell me, don't install." Deliberately NOT gated on the auto-update
|
|
249
|
+
// channel — that gate is why the only periodic publisher was dead on every
|
|
250
|
+
// live hub (`{"channel":"off"}`). See UpdateCheckScheduler.
|
|
251
|
+
const updateCheckLogger = loggingService.createLogger('UpdateCheck');
|
|
252
|
+
const updateCheckScheduler = new update_check_scheduler_js_1.UpdateCheckScheduler({
|
|
253
|
+
logger: updateCheckLogger,
|
|
254
|
+
getIntervalSeconds: () => addonPackageService.getAutoUpdateSettings().updateCheckIntervalSeconds,
|
|
255
|
+
targets: {
|
|
256
|
+
listNodes: () => agentRegistryService.listNodeLiveness(),
|
|
257
|
+
checkHubAddons: () => addonPackageService.checkUpdates(true),
|
|
258
|
+
checkFrameworkPackages: () => addonPackageService.listFrameworkPackages(),
|
|
259
|
+
checkAgentAddons: async (nodeId) => {
|
|
260
|
+
const broker = moleculerService.broker;
|
|
261
|
+
const installed = await (0, agent_installed_packages_js_1.fetchAgentInstalledPackages)(broker, nodeId);
|
|
262
|
+
return addonPackageService.checkUpdatesForInstalled(installed, nodeId);
|
|
263
|
+
},
|
|
264
|
+
checkNodeServerUpdate: async (nodeId, isHub) => {
|
|
265
|
+
if (isHub)
|
|
266
|
+
return serverUpdateService.checkServerUpdate();
|
|
267
|
+
const proxy = moleculerService.createCapabilityProxy('server-management', nodeId);
|
|
268
|
+
if (proxy === null) {
|
|
269
|
+
// A node with no reachable `server-management` cannot be checked —
|
|
270
|
+
// say so rather than counting it as up to date.
|
|
271
|
+
updateCheckLogger.warn('Update check skipped — server-management unreachable', {
|
|
272
|
+
tags: { nodeId },
|
|
273
|
+
});
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return proxy['checkServerUpdate']?.({});
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
addonPackageService.setUpdateCheckRescheduler(() => updateCheckScheduler.reschedule());
|
|
281
|
+
updateCheckScheduler.start();
|
|
240
282
|
// ---- Container ---------------------------------------------------------
|
|
241
283
|
const container = new ServiceContainer();
|
|
242
284
|
container.register(config_service_1.ConfigService, configService);
|
|
@@ -262,6 +304,7 @@ async function bootManual(opts) {
|
|
|
262
304
|
container.register(server_update_service_1.ServerUpdateService, serverUpdateService);
|
|
263
305
|
container.register(topology_emitter_service_1.TopologyEmitterService, topologyEmitterService);
|
|
264
306
|
container.register(post_boot_service_1.PostBootService, postBootService);
|
|
307
|
+
container.register(update_check_scheduler_js_1.UpdateCheckScheduler, updateCheckScheduler);
|
|
265
308
|
// ---- Fastify instance --------------------------------------------------
|
|
266
309
|
const fastify = (0, fastify_1.default)(fastifyOpts);
|
|
267
310
|
// /health and /health/* — registered in `main.ts` via
|
|
@@ -278,6 +321,12 @@ async function bootManual(opts) {
|
|
|
278
321
|
const logErr = (label, err) => {
|
|
279
322
|
console.error(`[manual-boot] ${label} destroy failed:`, err);
|
|
280
323
|
};
|
|
324
|
+
try {
|
|
325
|
+
updateCheckScheduler.stop();
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
logErr('UpdateCheckScheduler', err);
|
|
329
|
+
}
|
|
281
330
|
try {
|
|
282
331
|
topologyEmitterService.onModuleDestroy();
|
|
283
332
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.129",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
36
|
+
"@camstack/addon-admin-ui": "1.2.77",
|
|
37
37
|
"@camstack/addon-agent-ui": "1.2.20",
|
|
38
38
|
"@camstack/addon-auth": "1.2.22",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.19",
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.24",
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.94",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.77",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.92",
|
|
44
44
|
"@camstack/sdk": "1.2.22",
|
|
45
45
|
"@camstack/shm-ring": "1.1.19",
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
46
|
+
"@camstack/system": "1.2.103",
|
|
47
|
+
"@camstack/types": "1.2.86",
|
|
48
48
|
"@camstack/ui-library": "1.2.58",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|