@camstack/server 1.2.477 → 1.2.479
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-service.js +27 -71
- package/dist/agent/agent-undeploy.js +255 -0
- package/dist/agent/main.js +13 -0
- package/dist/api/core/cap-providers.js +91 -3
- package/dist/api/trpc/generated-cap-routers.js +8 -133
- package/dist/api/trpc/trpc.router.js +22 -2
- package/dist/core/addon/addon-uninstall.js +259 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +5 -5
- package/dist/core/agent/agent-addon-backfill.js +20 -18
- package/dist/core/agent/agent-registry.service.js +35 -7
- package/dist/server-root/index.js +0 -1
- package/dist/single-copy-cleanup.js +0 -1
- package/package.json +13 -13
- package/dist/api/bridge-addons.router.js +0 -109
|
@@ -42,6 +42,7 @@ const path = __importStar(require("node:path"));
|
|
|
42
42
|
const system_1 = require("@camstack/system");
|
|
43
43
|
const moleculer_1 = require("moleculer");
|
|
44
44
|
const agent_deploy_swap_js_1 = require("./agent-deploy-swap.js");
|
|
45
|
+
const agent_undeploy_js_1 = require("./agent-undeploy.js");
|
|
45
46
|
const apply_model_distribution_js_1 = require("./apply-model-distribution.js");
|
|
46
47
|
const fetch_bundle_from_hub_js_1 = require("./fetch-bundle-from-hub.js");
|
|
47
48
|
/**
|
|
@@ -57,6 +58,14 @@ const deploySwapLogger = {
|
|
|
57
58
|
child: () => deploySwapLogger,
|
|
58
59
|
withTags: () => deploySwapLogger,
|
|
59
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Packages an agent bootstraps itself when the bootstrap does not say
|
|
63
|
+
* otherwise. Mirrors the hub reconcile's `AGENT_BOOTSTRAP_PACKAGES`.
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_AGENT_BOOTSTRAP_PACKAGES = new Set([
|
|
66
|
+
'@camstack/system',
|
|
67
|
+
'@camstack/addon-agent-ui',
|
|
68
|
+
]);
|
|
60
69
|
/** Timeout for the agent-local `$process.restart` delegation. */
|
|
61
70
|
const AGENT_PROCESS_RESTART_TIMEOUT_MS = 30_000;
|
|
62
71
|
function getLocalIps() {
|
|
@@ -455,81 +464,28 @@ function createAgentService(deps) {
|
|
|
455
464
|
},
|
|
456
465
|
},
|
|
457
466
|
/**
|
|
458
|
-
* Genuinely drop a deployed addon from this agent.
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* the addon
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* To truly undeploy we must, in order:
|
|
465
|
-
* 1. `shutdown()` the running instance (releases timers, sockets,
|
|
466
|
-
* native handles) — same hook the agent's SIGTERM path uses.
|
|
467
|
-
* 2. `broker.destroyService(addonId)` — the deployed-addon Moleculer
|
|
468
|
-
* service is named after the addon DECLARATION id (see
|
|
469
|
-
* `createAddonService` → `name: declaration.id`). Destroying it
|
|
470
|
-
* unregisters every capability action so the cluster stops
|
|
471
|
-
* routing to / advertising this agent's provider.
|
|
472
|
-
* 3. Drop the `loadedAddons` entry and delete the on-disk folder.
|
|
473
|
-
*
|
|
474
|
-
* `addonId` here is the addon DECLARATION id — the same key
|
|
475
|
-
* `loadedAddons` uses and the same value `$agent.status` reports, so
|
|
476
|
-
* the hub reconciler can match it directly.
|
|
467
|
+
* Genuinely drop a deployed addon — or a whole package — from this agent.
|
|
468
|
+
* `{ packageName }` is the operator's uninstall (`addons.uninstallPackage
|
|
469
|
+
* ({ nodeId })`); `{ addonId }` is the stale-addon reconcile. Both stop the
|
|
470
|
+
* runner(s) hosting the addon, which the old body never did: on an agent
|
|
471
|
+
* nearly every addon lives in a group runner, so deleting its folder left
|
|
472
|
+
* the process running and advertising its caps. See `agent-undeploy.ts`.
|
|
477
473
|
*/
|
|
478
474
|
undeploy: {
|
|
479
475
|
handler: async (ctx) => {
|
|
480
476
|
const { params, broker } = ctx;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
// 2. Destroy the Moleculer service so the cluster stops seeing
|
|
496
|
-
// this agent's capability providers for the addon.
|
|
497
|
-
try {
|
|
498
|
-
await broker.destroyService(addonId);
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
// Service may not exist (group-spawned addons have no per-addon
|
|
502
|
-
// service on the agent broker, or it was never created).
|
|
503
|
-
}
|
|
504
|
-
// 3. Forget the entry and remove the deployed folder. The deploy
|
|
505
|
-
// handler keys the on-disk dir by the PACKAGE name, but $agent
|
|
506
|
-
// redeploys for a single addon use addonId === packageName for
|
|
507
|
-
// unscoped packages; remove whichever folder matches.
|
|
508
|
-
deps.loadedAddons.delete(addonId);
|
|
509
|
-
const addonDir = path.join(deps.addonsDir, addonId);
|
|
510
|
-
if (fs.existsSync(addonDir)) {
|
|
511
|
-
fs.rmSync(addonDir, { recursive: true, force: true });
|
|
512
|
-
}
|
|
513
|
-
// Defense-in-depth: one package dir can host MANY addons (e.g.
|
|
514
|
-
// `@camstack/addon-pipeline` ships decoder-ffmpeg, recorder,
|
|
515
|
-
// motion-wasm, …). Removing the whole bundle because ONE member was
|
|
516
|
-
// undeployed is destructive — every sibling runner would crash-loop
|
|
517
|
-
// on the missing package.json. Only remove the package dir when no
|
|
518
|
-
// OTHER loaded addon still lives in it (this addon's own entry was
|
|
519
|
-
// already deleted from `loadedAddons` above, so the remaining
|
|
520
|
-
// values are exactly the siblings).
|
|
521
|
-
const pkgName = entry?.packageName;
|
|
522
|
-
if (pkgName && pkgName !== addonId) {
|
|
523
|
-
const pkgShared = [...deps.loadedAddons.values()].some((e) => e.packageName === pkgName);
|
|
524
|
-
if (!pkgShared) {
|
|
525
|
-
const pkgDir = path.join(deps.addonsDir, pkgName);
|
|
526
|
-
if (fs.existsSync(pkgDir)) {
|
|
527
|
-
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
broker.logger.info(`$agent.undeploy: ${addonId} disposed (instance + service + folder)`);
|
|
532
|
-
return { success: true, addonId };
|
|
477
|
+
return (0, agent_undeploy_js_1.undeployFromAgent)({
|
|
478
|
+
addonsDir: deps.addonsDir,
|
|
479
|
+
loadedAddons: deps.loadedAddons,
|
|
480
|
+
bootstrapPackages: deps.bootstrapPackages ?? DEFAULT_AGENT_BOOTSTRAP_PACKAGES,
|
|
481
|
+
listRunnersByAddon: (addonId) => broker.call('$process.listByAddon', { addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
|
|
482
|
+
stopRunner: (name) => broker.call('$process.stop', { name }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
|
|
483
|
+
destroyService: (addonId) => broker.destroyService(addonId),
|
|
484
|
+
unregisterProvider: (capName, addonId) => deps.unregisterProvider?.(capName, addonId),
|
|
485
|
+
respawnSurvivors: deps.reloadDeployedAddons,
|
|
486
|
+
reconcileNodeRegistration: deps.reconcileNodeRegistration,
|
|
487
|
+
logger: deploySwapLogger,
|
|
488
|
+
}, { addonId: params.addonId, packageName: params.packageName });
|
|
533
489
|
},
|
|
534
490
|
},
|
|
535
491
|
/**
|
|
@@ -0,0 +1,255 @@
|
|
|
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.undeployFromAgent = undeployFromAgent;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const agent_group_runner_js_1 = require("./agent-group-runner.js");
|
|
40
|
+
function capName(cap) {
|
|
41
|
+
return typeof cap === 'string' ? cap : cap.name;
|
|
42
|
+
}
|
|
43
|
+
function packageDirFor(addonsDir, packageName) {
|
|
44
|
+
const resolved = path.resolve(addonsDir, packageName);
|
|
45
|
+
const root = path.resolve(addonsDir);
|
|
46
|
+
// A package name is data from the network. It must name a dir INSIDE the
|
|
47
|
+
// addon root and never the root itself.
|
|
48
|
+
if (resolved === root || !resolved.startsWith(root + path.sep))
|
|
49
|
+
return null;
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
function resolveTarget(seam, request) {
|
|
53
|
+
const packageName = request.packageName ??
|
|
54
|
+
(request.addonId !== undefined
|
|
55
|
+
? seam.loadedAddons.get(request.addonId)?.packageName
|
|
56
|
+
: undefined);
|
|
57
|
+
const packageDir = packageName !== undefined ? packageDirFor(seam.addonsDir, packageName) : null;
|
|
58
|
+
const manifest = packageDir !== null ? (0, agent_group_runner_js_1.readPackageManifestAddons)(packageDir) : null;
|
|
59
|
+
const capsByAddon = new Map();
|
|
60
|
+
let protectedBy = null;
|
|
61
|
+
for (const decl of manifest?.declarations ?? []) {
|
|
62
|
+
capsByAddon.set(decl.id, (decl.capabilities ?? []).map(capName));
|
|
63
|
+
if (decl.protected === true)
|
|
64
|
+
protectedBy = decl.id;
|
|
65
|
+
}
|
|
66
|
+
if (request.packageName === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
packageName,
|
|
69
|
+
addonIds: request.addonId !== undefined ? [request.addonId] : [],
|
|
70
|
+
capsByAddon,
|
|
71
|
+
packageDir,
|
|
72
|
+
protectedBy,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const ids = new Set(capsByAddon.keys());
|
|
76
|
+
for (const entry of seam.loadedAddons.values()) {
|
|
77
|
+
if (entry.packageName === request.packageName)
|
|
78
|
+
ids.add(entry.id);
|
|
79
|
+
}
|
|
80
|
+
return { packageName, addonIds: [...ids], capsByAddon, packageDir, protectedBy };
|
|
81
|
+
}
|
|
82
|
+
function refusalReason(seam, target) {
|
|
83
|
+
if (target.packageName !== undefined && seam.bootstrapPackages.has(target.packageName)) {
|
|
84
|
+
return `${target.packageName} is this node's bootstrap infrastructure and cannot be uninstalled`;
|
|
85
|
+
}
|
|
86
|
+
if (target.protectedBy !== null) {
|
|
87
|
+
return `${target.packageName ?? 'package'} is protected (addon "${target.protectedBy}" declares protected: true)`;
|
|
88
|
+
}
|
|
89
|
+
if (target.packageName === undefined && target.addonIds.length === 0) {
|
|
90
|
+
return 'neither addonId nor packageName was given';
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async function shutdownInProcess(seam, addonId) {
|
|
95
|
+
const entry = seam.loadedAddons.get(addonId);
|
|
96
|
+
if (entry?.addon?.shutdown) {
|
|
97
|
+
try {
|
|
98
|
+
await entry.addon.shutdown();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
seam.logger.warn(`undeploy: ${addonId} shutdown() threw`, {
|
|
102
|
+
tags: { addonId },
|
|
103
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await seam.destroyService(addonId);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Group-runner addons have no per-addon service on the agent broker.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function stopHostingRunners(seam, addonIds) {
|
|
115
|
+
const removing = new Set(addonIds);
|
|
116
|
+
const runners = new Map();
|
|
117
|
+
const warnings = [];
|
|
118
|
+
for (const id of addonIds) {
|
|
119
|
+
try {
|
|
120
|
+
for (const row of await seam.listRunnersByAddon(id))
|
|
121
|
+
runners.set(row.name, row);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
warnings.push(`could not list runners for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const stopped = [];
|
|
128
|
+
const survivors = [];
|
|
129
|
+
for (const runner of runners.values()) {
|
|
130
|
+
const result = await seam.stopRunner(runner.name).catch((err) => ({
|
|
131
|
+
success: false,
|
|
132
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
133
|
+
}));
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
warnings.push(`runner "${runner.name}" did not stop: ${result.reason ?? 'unknown'}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
stopped.push(runner.name);
|
|
139
|
+
for (const id of runner.addonIds) {
|
|
140
|
+
if (!removing.has(id))
|
|
141
|
+
survivors.push(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { stopped, survivors, warnings };
|
|
145
|
+
}
|
|
146
|
+
function removeDirs(seam, request, target) {
|
|
147
|
+
const removed = [];
|
|
148
|
+
const rm = (dir) => {
|
|
149
|
+
if (dir === null || !fs.existsSync(dir))
|
|
150
|
+
return;
|
|
151
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
removed.push(dir);
|
|
153
|
+
};
|
|
154
|
+
if (request.packageName !== undefined) {
|
|
155
|
+
rm(target.packageDir);
|
|
156
|
+
return removed;
|
|
157
|
+
}
|
|
158
|
+
// Addon mode — the reconcile's per-id removal. One package dir can host MANY
|
|
159
|
+
// addons; only remove it when no OTHER loaded addon still lives in it (this
|
|
160
|
+
// addon's own row is already gone from `loadedAddons`).
|
|
161
|
+
const addonId = request.addonId;
|
|
162
|
+
if (addonId !== undefined)
|
|
163
|
+
rm(packageDirFor(seam.addonsDir, addonId));
|
|
164
|
+
const pkg = target.packageName;
|
|
165
|
+
if (pkg !== undefined && pkg !== addonId) {
|
|
166
|
+
const shared = [...seam.loadedAddons.values()].some((e) => e.packageName === pkg);
|
|
167
|
+
if (!shared)
|
|
168
|
+
rm(target.packageDir);
|
|
169
|
+
}
|
|
170
|
+
return removed;
|
|
171
|
+
}
|
|
172
|
+
/** Run one `$agent.undeploy`. Never throws for a refusal — it answers `refused`. */
|
|
173
|
+
async function undeployFromAgent(seam, request) {
|
|
174
|
+
const target = resolveTarget(seam, request);
|
|
175
|
+
const scope = request.packageName !== undefined ? 'package' : 'addon';
|
|
176
|
+
const base = {
|
|
177
|
+
scope,
|
|
178
|
+
addonId: request.addonId,
|
|
179
|
+
packageName: target.packageName,
|
|
180
|
+
addonIds: target.addonIds,
|
|
181
|
+
};
|
|
182
|
+
const refused = refusalReason(seam, target);
|
|
183
|
+
if (refused !== null) {
|
|
184
|
+
seam.logger.warn(`undeploy refused: ${refused}`, { meta: { request } });
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
success: false,
|
|
188
|
+
outcome: 'refused',
|
|
189
|
+
reason: refused,
|
|
190
|
+
runnersStopped: [],
|
|
191
|
+
runnersRespawned: [],
|
|
192
|
+
removedDirs: [],
|
|
193
|
+
warnings: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const present = target.addonIds.some((id) => seam.loadedAddons.has(id)) ||
|
|
197
|
+
(target.packageDir !== null && fs.existsSync(target.packageDir));
|
|
198
|
+
if (scope === 'package' && !present) {
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
success: true,
|
|
202
|
+
outcome: 'not-installed',
|
|
203
|
+
runnersStopped: [],
|
|
204
|
+
runnersRespawned: [],
|
|
205
|
+
removedDirs: [],
|
|
206
|
+
warnings: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
for (const id of target.addonIds)
|
|
210
|
+
await shutdownInProcess(seam, id);
|
|
211
|
+
const runners = await stopHostingRunners(seam, target.addonIds);
|
|
212
|
+
for (const id of target.addonIds) {
|
|
213
|
+
for (const cap of target.capsByAddon.get(id) ?? [])
|
|
214
|
+
seam.unregisterProvider(cap, id);
|
|
215
|
+
seam.loadedAddons.delete(id);
|
|
216
|
+
}
|
|
217
|
+
// A survivor's row is dropped too, so the discovery pass below sees it as
|
|
218
|
+
// not-loaded and respawns its runner — without the removed addons, whose
|
|
219
|
+
// code is gone by then.
|
|
220
|
+
for (const id of runners.survivors) {
|
|
221
|
+
for (const cap of target.capsByAddon.get(id) ?? [])
|
|
222
|
+
seam.unregisterProvider(cap, id);
|
|
223
|
+
seam.loadedAddons.delete(id);
|
|
224
|
+
}
|
|
225
|
+
const removedDirs = removeDirs(seam, request, target);
|
|
226
|
+
const warnings = [...runners.warnings];
|
|
227
|
+
let runnersRespawned = [];
|
|
228
|
+
if (runners.survivors.length > 0) {
|
|
229
|
+
if (seam.respawnSurvivors) {
|
|
230
|
+
try {
|
|
231
|
+
await seam.respawnSurvivors();
|
|
232
|
+
runnersRespawned = runners.stopped;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
warnings.push(`survivors ${runners.survivors.join(', ')} were not respawned: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
warnings.push(`survivors ${runners.survivors.join(', ')} were stopped and not respawned`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
seam.reconcileNodeRegistration?.();
|
|
243
|
+
seam.logger.info(`undeploy: ${target.packageName ?? request.addonId ?? '?'} removed ` +
|
|
244
|
+
`(addons=${target.addonIds.join(',') || '-'} runnersStopped=${runners.stopped.join(',') || '-'} ` +
|
|
245
|
+
`dirs=${removedDirs.length})`, { meta: { warnings } });
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
success: true,
|
|
249
|
+
outcome: 'uninstalled',
|
|
250
|
+
runnersStopped: runners.stopped,
|
|
251
|
+
runnersRespawned,
|
|
252
|
+
removedDirs,
|
|
253
|
+
warnings,
|
|
254
|
+
};
|
|
255
|
+
}
|
package/dist/agent/main.js
CHANGED
|
@@ -470,6 +470,19 @@ async function startAgent(configPath) {
|
|
|
470
470
|
// (manifest strip + runtime deps + native deps + atomic swap) instead of
|
|
471
471
|
// the bare tar swap that shipped addons without their node_modules.
|
|
472
472
|
installBundleTgz: (tgzPath) => installer.installFromTgz(tgzPath),
|
|
473
|
+
// `$agent.undeploy` refuses what this node bootstraps itself, and drops the
|
|
474
|
+
// group-runner cap proxies it removes (registered by
|
|
475
|
+
// `registerGroupRunnerProviders`, ownerless — so a plain unregister owns them).
|
|
476
|
+
bootstrapPackages: new Set([
|
|
477
|
+
'@camstack/system',
|
|
478
|
+
'@camstack/addon-agent-ui',
|
|
479
|
+
...system_1.AddonInstaller.AGENT_PACKAGES,
|
|
480
|
+
]),
|
|
481
|
+
unregisterProvider: (capName, addonId) => {
|
|
482
|
+
if (capabilityRegistry.hasProvider(capName, addonId)) {
|
|
483
|
+
capabilityRegistry.unregisterProvider(capName, addonId);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
473
486
|
});
|
|
474
487
|
broker.createService(agentServiceSchema);
|
|
475
488
|
// $process service — manages forked child processes (same as hub).
|
|
@@ -77,6 +77,7 @@ const types_1 = require("@camstack/types");
|
|
|
77
77
|
const server_1 = require("@trpc/server");
|
|
78
78
|
const integration_id_backfill_1 = require("../../boot/integration-id-backfill");
|
|
79
79
|
const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
|
|
80
|
+
const addon_uninstall_js_1 = require("../../core/addon/addon-uninstall.js");
|
|
80
81
|
const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
|
|
81
82
|
const agent_installed_packages_js_1 = require("../../core/updates/agent-installed-packages.js");
|
|
82
83
|
const http_request_census_singleton_js_1 = require("../trpc/http-request-census.singleton.js");
|
|
@@ -1318,8 +1319,82 @@ function isHubNode(nodeId) {
|
|
|
1318
1319
|
* filter it out to make the list look clean — the number reaching zero is the
|
|
1319
1320
|
* point of the number.
|
|
1320
1321
|
*/
|
|
1321
|
-
|
|
1322
|
+
/**
|
|
1323
|
+
* How long the hub waits for an agent's `$agent.undeploy`. It stops runners
|
|
1324
|
+
* (each up to a 5 s SIGTERM grace plus the group reap) and may respawn a shared
|
|
1325
|
+
* runner's survivors, so it is far longer than a status read.
|
|
1326
|
+
*/
|
|
1327
|
+
const AGENT_UNDEPLOY_TIMEOUT_MS = 120_000;
|
|
1328
|
+
function parseAgentStatusAddons(reply) {
|
|
1329
|
+
if (typeof reply !== 'object' || reply === null || !('addons' in reply))
|
|
1330
|
+
return [];
|
|
1331
|
+
const addons = reply.addons;
|
|
1332
|
+
if (!Array.isArray(addons))
|
|
1333
|
+
return [];
|
|
1334
|
+
const rows = [];
|
|
1335
|
+
for (const raw of addons) {
|
|
1336
|
+
if (typeof raw !== 'object' || raw === null || !('id' in raw) || typeof raw.id !== 'string') {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const packageName = 'packageName' in raw && typeof raw.packageName === 'string' ? raw.packageName : undefined;
|
|
1340
|
+
const version = 'version' in raw && typeof raw.version === 'string' ? raw.version : undefined;
|
|
1341
|
+
rows.push({ id: raw.id, packageName, version });
|
|
1342
|
+
}
|
|
1343
|
+
return rows;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* The real seam behind `addons.uninstallPackage` / `listInstallations`. The
|
|
1347
|
+
* agent leg is the same hub-side `broker.call({ nodeID })` to the agent's
|
|
1348
|
+
* `$agent` service that `updatePackage({ nodeId })` uses.
|
|
1349
|
+
*/
|
|
1350
|
+
function buildUninstallSeam(ar, ps, broker, agentRegistry) {
|
|
1351
|
+
return {
|
|
1352
|
+
isProtected: (name) => ps.isProtected(name),
|
|
1353
|
+
listHubPackages: () => {
|
|
1354
|
+
let installed;
|
|
1355
|
+
try {
|
|
1356
|
+
installed = ps.listInstalled();
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
return [];
|
|
1360
|
+
}
|
|
1361
|
+
const idsByPackage = new Map();
|
|
1362
|
+
for (const row of ar.listAddons()) {
|
|
1363
|
+
const ids = idsByPackage.get(row.manifest.packageName) ?? [];
|
|
1364
|
+
ids.push(row.manifest.id);
|
|
1365
|
+
idsByPackage.set(row.manifest.packageName, ids);
|
|
1366
|
+
}
|
|
1367
|
+
return installed.map((p) => ({
|
|
1368
|
+
name: p.name,
|
|
1369
|
+
version: p.version,
|
|
1370
|
+
addonIds: idsByPackage.get(p.name) ?? [],
|
|
1371
|
+
}));
|
|
1372
|
+
},
|
|
1373
|
+
uninstallOnHub: async (name) => {
|
|
1374
|
+
await ps.uninstallAndReload(name);
|
|
1375
|
+
},
|
|
1376
|
+
listAgents: async () => {
|
|
1377
|
+
if (!agentRegistry)
|
|
1378
|
+
return [];
|
|
1379
|
+
const directory = await agentRegistry.listNodeDirectory();
|
|
1380
|
+
return directory.nodes
|
|
1381
|
+
.filter((n) => !n.isHub && !n.id.includes('/'))
|
|
1382
|
+
.map((n) => ({ id: n.id, name: n.name, isOnline: n.isOnline }));
|
|
1383
|
+
},
|
|
1384
|
+
agentAddons: async (nodeId) => parseAgentStatusAddons(await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: agent_installed_packages_js_1.AGENT_STATUS_TIMEOUT_MS })),
|
|
1385
|
+
agentUndeploy: (nodeId, request) => broker.call('$agent.undeploy', request, {
|
|
1386
|
+
nodeID: nodeId,
|
|
1387
|
+
timeout: AGENT_UNDEPLOY_TIMEOUT_MS,
|
|
1388
|
+
}),
|
|
1389
|
+
recordedPackages: async (nodeId) => agentRegistry ? agentRegistry.getRecordedPackages(nodeId) : [],
|
|
1390
|
+
notePackageUndeployed: async (nodeId, packageName, addonIds) => {
|
|
1391
|
+
await agentRegistry?.notePackageUndeployed(nodeId, packageName, addonIds);
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentRegistry) {
|
|
1322
1396
|
const broker = moleculer.broker;
|
|
1397
|
+
const uninstallSeam = buildUninstallSeam(ar, ps, broker, agentRegistry);
|
|
1323
1398
|
const frameworkAllowSet = new Set([addon_package_service_js_1.SYSTEM_PACKAGE]);
|
|
1324
1399
|
// Process-wide runner built once at boot (manual-boot wires its deps from the
|
|
1325
1400
|
// addon-package service + event bus). `createdBy` travels per `startJob` call.
|
|
@@ -1349,7 +1424,18 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1349
1424
|
installFromWorkspace: async (input) => ps.installFromWorkspaceAndLoad(input.packageName),
|
|
1350
1425
|
isWorkspaceAvailable: async () => ps.isWorkspaceAvailable(),
|
|
1351
1426
|
listWorkspacePackages: async () => ps.listWorkspacePackages(),
|
|
1352
|
-
uninstallPackage: async (input) =>
|
|
1427
|
+
uninstallPackage: async (input) => {
|
|
1428
|
+
try {
|
|
1429
|
+
return await (0, addon_uninstall_js_1.uninstallPackage)(uninstallSeam, input);
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
if (err instanceof addon_uninstall_js_1.PackageProtectedError) {
|
|
1433
|
+
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: err.message });
|
|
1434
|
+
}
|
|
1435
|
+
throw err;
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
listInstallations: async (input) => (0, addon_uninstall_js_1.listInstallations)(uninstallSeam, input.packageName),
|
|
1353
1439
|
reloadPackages: async () => ps.reloadPackages(),
|
|
1354
1440
|
searchAvailable: async (input) => {
|
|
1355
1441
|
const results = await ps.searchNpm(input?.query);
|
|
@@ -1486,7 +1572,9 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1486
1572
|
if (offNode.length > 0) {
|
|
1487
1573
|
throw new server_1.TRPCError({
|
|
1488
1574
|
code: 'BAD_REQUEST',
|
|
1489
|
-
message:
|
|
1575
|
+
message: input.kind === 'uninstall'
|
|
1576
|
+
? `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Uninstall from an agent via uninstallPackage({ packageName, nodeId }), or from every node via uninstallPackage({ packageName, everywhere: true }).`
|
|
1577
|
+
: `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Update an agent's packages via updatePackage({ nodeId }).`,
|
|
1490
1578
|
});
|
|
1491
1579
|
}
|
|
1492
1580
|
return lifecycleRunner.startJob({
|