@camstack/system 1.2.124 → 1.2.126
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/addon-runner.js +224 -224
- package/dist/addon-runner.mjs +224 -224
- package/dist/index.js +43 -3
- package/dist/index.mjs +43 -3
- package/dist/kernel/heap-watch.d.ts +11 -8
- package/dist/kernel/moleculer/process-service.d.ts +12 -1
- package/dist/kernel/moleculer/runner-native-allocator.d.ts +95 -0
- package/dist/{manifest-python-deps-DB_4H3tR.js → manifest-python-deps-BTkFwAk_.js} +12 -9
- package/dist/{manifest-python-deps-D1Ydw7J0.mjs → manifest-python-deps-Dmcnb287.mjs} +12 -9
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-BTkFwAk_.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -10,229 +10,6 @@ let node_v8 = require("node:v8");
|
|
|
10
10
|
node_v8 = require_chunk.__toESM(node_v8);
|
|
11
11
|
let _camstack_types_addon = require("@camstack/types/addon");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
|
-
//#region src/kernel/moleculer/worker-device-restore.ts
|
|
14
|
-
/**
|
|
15
|
-
* Worker-side device restore helper.
|
|
16
|
-
*
|
|
17
|
-
* Shared by `process-runner` (single-addon subprocess) and
|
|
18
|
-
* `group-runner` (group subprocess). Calls the worker addon's
|
|
19
|
-
* `restoreDevices(SavedDevice[])` with devices read from the hub's
|
|
20
|
-
* `device-manager` capability. Blocks on the `system.ready-state`
|
|
21
|
-
* readiness of the hub's `device-manager` via
|
|
22
|
-
* `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
|
|
23
|
-
*
|
|
24
|
-
* Lives in its own module so it can be imported without triggering
|
|
25
|
-
* the runner entry-point side effects (each runner is also an
|
|
26
|
-
* executable that calls `main()` at import time).
|
|
27
|
-
*/
|
|
28
|
-
async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
|
|
29
|
-
const log = context.logger;
|
|
30
|
-
log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
|
|
31
|
-
const restoreFn = addon.restoreDevices;
|
|
32
|
-
if (typeof restoreFn !== "function") {
|
|
33
|
-
log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const api = context.api;
|
|
37
|
-
if (!api) {
|
|
38
|
-
log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const bus = context.eventBus;
|
|
42
|
-
if (!bus) {
|
|
43
|
-
log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const shared = context.kernel?.readinessRegistry ?? null;
|
|
47
|
-
const registry = shared ?? new _camstack_types_addon.ReadinessRegistry({
|
|
48
|
-
eventBus: bus,
|
|
49
|
-
sourceNodeId,
|
|
50
|
-
logger: context.logger
|
|
51
|
-
});
|
|
52
|
-
log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
|
|
53
|
-
try {
|
|
54
|
-
await registry.awaitReady("device-manager", {
|
|
55
|
-
type: "node",
|
|
56
|
-
nodeId: "hub"
|
|
57
|
-
}, { timeoutMs: 6e4 });
|
|
58
|
-
log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
if (err instanceof _camstack_types_addon.ReadinessTimeoutError) {
|
|
61
|
-
context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
throw err;
|
|
65
|
-
} finally {
|
|
66
|
-
if (!shared && registry instanceof _camstack_types_addon.ReadinessRegistry) registry.close();
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const deviceManager = Reflect.get(api, "deviceManager");
|
|
70
|
-
log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
|
|
71
|
-
const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
|
|
72
|
-
log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
|
|
73
|
-
const rows = Array.isArray(rowsResult) ? rowsResult : [];
|
|
74
|
-
if (rows.length === 0) {
|
|
75
|
-
context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const savedDevices = await Promise.all(rows.map(async (row) => {
|
|
79
|
-
const config = await deviceManager.loadConfig.query({ deviceId: row.id });
|
|
80
|
-
return {
|
|
81
|
-
id: row.id,
|
|
82
|
-
stableId: row.stableId,
|
|
83
|
-
type: row.type,
|
|
84
|
-
name: row.name,
|
|
85
|
-
parentDeviceId: row.parentDeviceId,
|
|
86
|
-
config: config ?? {}
|
|
87
|
-
};
|
|
88
|
-
}));
|
|
89
|
-
await restoreFn.call(addon, savedDevices);
|
|
90
|
-
log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
|
|
91
|
-
} catch (err) {
|
|
92
|
-
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${(0, _camstack_types_addon.errMsg)(err)}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/kernel/moleculer/register-framework-resolver.ts
|
|
97
|
-
/**
|
|
98
|
-
* Register the ESM resolver hook so this runner's addon imports of the
|
|
99
|
-
* host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
|
|
100
|
-
* `frameworkDir/node_modules` instead of failing to walk up from the addon's
|
|
101
|
-
* isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
|
|
102
|
-
* where workspace symlinks already resolve the framework.
|
|
103
|
-
*/
|
|
104
|
-
function registerFrameworkResolver(frameworkDir) {
|
|
105
|
-
if (!frameworkDir) return;
|
|
106
|
-
(0, node_module.register)((0, node_url.pathToFileURL)(node_path.join(__dirname, "framework-resolver-hook.mjs")), {
|
|
107
|
-
parentURL: (0, node_url.pathToFileURL)(`${__dirname}/`).href,
|
|
108
|
-
data: { frameworkDir }
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Start warning if `initialize()` has not returned within `firstMs`.
|
|
113
|
-
*
|
|
114
|
-
* The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
|
|
115
|
-
* runner turns into `process.exit(1)`) does not keep the timer alive.
|
|
116
|
-
*/
|
|
117
|
-
function startInitWatchdog(options) {
|
|
118
|
-
const now = options.now ?? Date.now;
|
|
119
|
-
const firstMs = options.firstMs ?? 6e4;
|
|
120
|
-
const repeatMs = options.repeatMs ?? 3e5;
|
|
121
|
-
const startedAt = now();
|
|
122
|
-
let timer;
|
|
123
|
-
let cleared = false;
|
|
124
|
-
const schedule = (ms) => {
|
|
125
|
-
timer = setTimeout(fire, ms);
|
|
126
|
-
timer.unref?.();
|
|
127
|
-
};
|
|
128
|
-
function fire() {
|
|
129
|
-
if (cleared) return;
|
|
130
|
-
const elapsedMs = now() - startedAt;
|
|
131
|
-
options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
|
|
132
|
-
addonId: options.addonId,
|
|
133
|
-
elapsedMs,
|
|
134
|
-
unpublishedCapabilities: [...options.declaredCapabilities]
|
|
135
|
-
});
|
|
136
|
-
schedule(repeatMs);
|
|
137
|
-
}
|
|
138
|
-
schedule(firstMs);
|
|
139
|
-
return { clear() {
|
|
140
|
-
cleared = true;
|
|
141
|
-
if (timer !== void 0) {
|
|
142
|
-
clearTimeout(timer);
|
|
143
|
-
timer = void 0;
|
|
144
|
-
}
|
|
145
|
-
} };
|
|
146
|
-
}
|
|
147
|
-
//#endregion
|
|
148
|
-
//#region src/kernel/moleculer/child-cap-dispatch.ts
|
|
149
|
-
/** Type guard narrowing a dynamically-read provider member to a callable method. */
|
|
150
|
-
function isCapMethod(value) {
|
|
151
|
-
return typeof value === "function";
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Extract a string `addonId` out of arbitrary method args — the fallback
|
|
155
|
-
* provider-selection hint for calls that arrive WITHOUT the out-of-band
|
|
156
|
-
* `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
|
|
157
|
-
* forwarded by the parent, where the raw input still carries `addonId`).
|
|
158
|
-
*/
|
|
159
|
-
function extractArgsAddonId(args) {
|
|
160
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
161
|
-
const raw = Reflect.get(args, "addonId");
|
|
162
|
-
return typeof raw === "string" ? raw : void 0;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Creates a unified dispatcher that routes a {@link CapCallInput} to either a
|
|
166
|
-
* device-scoped or singleton provider, then invokes the requested method.
|
|
167
|
-
*
|
|
168
|
-
* Pure module — no Moleculer, no broker, no addon-runner dependency.
|
|
169
|
-
*/
|
|
170
|
-
function createChildCapDispatch(deps) {
|
|
171
|
-
return async (call) => {
|
|
172
|
-
const { capName, method, args, deviceId } = call;
|
|
173
|
-
const addonId = call.addonId ?? extractArgsAddonId(args);
|
|
174
|
-
let provider;
|
|
175
|
-
if (deviceId !== void 0) {
|
|
176
|
-
provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
|
|
177
|
-
if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
|
|
178
|
-
} else {
|
|
179
|
-
provider = deps.getSingletonProvider(capName, addonId);
|
|
180
|
-
if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
|
|
181
|
-
}
|
|
182
|
-
const fn = Reflect.get(provider, method);
|
|
183
|
-
if (!isCapMethod(fn)) {
|
|
184
|
-
if (deps.isCollectionListMethod(capName, method)) return [];
|
|
185
|
-
throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
|
|
186
|
-
}
|
|
187
|
-
return await fn.call(provider, args);
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/kernel/moleculer/child-cap-descriptors.ts
|
|
192
|
-
/**
|
|
193
|
-
* Builds the {@link ChildCapDescriptor} array that the UDS child transport
|
|
194
|
-
* sends to the parent on registration.
|
|
195
|
-
*
|
|
196
|
-
* - Singleton caps come from the runner UNION manifest (all caps registered by
|
|
197
|
-
* every addon in this runner). One descriptor per unique (addonId, capName)
|
|
198
|
-
* pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
|
|
199
|
-
* parent's manifest builder register each hosted addon's caps under the
|
|
200
|
-
* REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
|
|
201
|
-
* and deduplicating by capName alone would collapse two co-located providers
|
|
202
|
-
* of the same collection cap (e.g. `network-access` from cloudflare-tunnel
|
|
203
|
-
* AND tailscale-ingress) into one descriptor registered under a synthetic
|
|
204
|
-
* `addonId = childId` (the group name) — the "@camstack/addon-net-access"
|
|
205
|
-
* admin-UI bug.
|
|
206
|
-
* - Device-scoped caps come from the native-cap snapshot — one descriptor per
|
|
207
|
-
* (capName, deviceId) pair, mode `'collection'`, stamped with the owning
|
|
208
|
-
* addon's id.
|
|
209
|
-
*
|
|
210
|
-
* Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
|
|
211
|
-
* import time) so it is unit-testable.
|
|
212
|
-
*/
|
|
213
|
-
function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
|
|
214
|
-
const descriptors = [];
|
|
215
|
-
const seen = /* @__PURE__ */ new Set();
|
|
216
|
-
for (const entry of manifest) for (const capName of entry.capabilities) {
|
|
217
|
-
const key = `${entry.addonId}::${capName}`;
|
|
218
|
-
if (!seen.has(key)) {
|
|
219
|
-
seen.add(key);
|
|
220
|
-
descriptors.push({
|
|
221
|
-
capName,
|
|
222
|
-
mode: "singleton",
|
|
223
|
-
addonId: entry.addonId
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
|
|
228
|
-
capName,
|
|
229
|
-
mode: "collection",
|
|
230
|
-
deviceId,
|
|
231
|
-
addonId
|
|
232
|
-
});
|
|
233
|
-
return descriptors;
|
|
234
|
-
}
|
|
235
|
-
//#endregion
|
|
236
13
|
//#region src/kernel/moleculer/child-addon-call-dispatch.ts
|
|
237
14
|
function isRecord$1(value) {
|
|
238
15
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -324,6 +101,229 @@ function createChildAddonCallDispatch(deps) {
|
|
|
324
101
|
};
|
|
325
102
|
}
|
|
326
103
|
//#endregion
|
|
104
|
+
//#region src/kernel/moleculer/child-cap-descriptors.ts
|
|
105
|
+
/**
|
|
106
|
+
* Builds the {@link ChildCapDescriptor} array that the UDS child transport
|
|
107
|
+
* sends to the parent on registration.
|
|
108
|
+
*
|
|
109
|
+
* - Singleton caps come from the runner UNION manifest (all caps registered by
|
|
110
|
+
* every addon in this runner). One descriptor per unique (addonId, capName)
|
|
111
|
+
* pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
|
|
112
|
+
* parent's manifest builder register each hosted addon's caps under the
|
|
113
|
+
* REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
|
|
114
|
+
* and deduplicating by capName alone would collapse two co-located providers
|
|
115
|
+
* of the same collection cap (e.g. `network-access` from cloudflare-tunnel
|
|
116
|
+
* AND tailscale-ingress) into one descriptor registered under a synthetic
|
|
117
|
+
* `addonId = childId` (the group name) — the "@camstack/addon-net-access"
|
|
118
|
+
* admin-UI bug.
|
|
119
|
+
* - Device-scoped caps come from the native-cap snapshot — one descriptor per
|
|
120
|
+
* (capName, deviceId) pair, mode `'collection'`, stamped with the owning
|
|
121
|
+
* addon's id.
|
|
122
|
+
*
|
|
123
|
+
* Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
|
|
124
|
+
* import time) so it is unit-testable.
|
|
125
|
+
*/
|
|
126
|
+
function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
|
|
127
|
+
const descriptors = [];
|
|
128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
129
|
+
for (const entry of manifest) for (const capName of entry.capabilities) {
|
|
130
|
+
const key = `${entry.addonId}::${capName}`;
|
|
131
|
+
if (!seen.has(key)) {
|
|
132
|
+
seen.add(key);
|
|
133
|
+
descriptors.push({
|
|
134
|
+
capName,
|
|
135
|
+
mode: "singleton",
|
|
136
|
+
addonId: entry.addonId
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
|
|
141
|
+
capName,
|
|
142
|
+
mode: "collection",
|
|
143
|
+
deviceId,
|
|
144
|
+
addonId
|
|
145
|
+
});
|
|
146
|
+
return descriptors;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/kernel/moleculer/child-cap-dispatch.ts
|
|
150
|
+
/** Type guard narrowing a dynamically-read provider member to a callable method. */
|
|
151
|
+
function isCapMethod(value) {
|
|
152
|
+
return typeof value === "function";
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract a string `addonId` out of arbitrary method args — the fallback
|
|
156
|
+
* provider-selection hint for calls that arrive WITHOUT the out-of-band
|
|
157
|
+
* `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
|
|
158
|
+
* forwarded by the parent, where the raw input still carries `addonId`).
|
|
159
|
+
*/
|
|
160
|
+
function extractArgsAddonId(args) {
|
|
161
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
162
|
+
const raw = Reflect.get(args, "addonId");
|
|
163
|
+
return typeof raw === "string" ? raw : void 0;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Creates a unified dispatcher that routes a {@link CapCallInput} to either a
|
|
167
|
+
* device-scoped or singleton provider, then invokes the requested method.
|
|
168
|
+
*
|
|
169
|
+
* Pure module — no Moleculer, no broker, no addon-runner dependency.
|
|
170
|
+
*/
|
|
171
|
+
function createChildCapDispatch(deps) {
|
|
172
|
+
return async (call) => {
|
|
173
|
+
const { capName, method, args, deviceId } = call;
|
|
174
|
+
const addonId = call.addonId ?? extractArgsAddonId(args);
|
|
175
|
+
let provider;
|
|
176
|
+
if (deviceId !== void 0) {
|
|
177
|
+
provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
|
|
178
|
+
if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
|
|
179
|
+
} else {
|
|
180
|
+
provider = deps.getSingletonProvider(capName, addonId);
|
|
181
|
+
if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
|
|
182
|
+
}
|
|
183
|
+
const fn = Reflect.get(provider, method);
|
|
184
|
+
if (!isCapMethod(fn)) {
|
|
185
|
+
if (deps.isCollectionListMethod(capName, method)) return [];
|
|
186
|
+
throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
|
|
187
|
+
}
|
|
188
|
+
return await fn.call(provider, args);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Start warning if `initialize()` has not returned within `firstMs`.
|
|
193
|
+
*
|
|
194
|
+
* The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
|
|
195
|
+
* runner turns into `process.exit(1)`) does not keep the timer alive.
|
|
196
|
+
*/
|
|
197
|
+
function startInitWatchdog(options) {
|
|
198
|
+
const now = options.now ?? Date.now;
|
|
199
|
+
const firstMs = options.firstMs ?? 6e4;
|
|
200
|
+
const repeatMs = options.repeatMs ?? 3e5;
|
|
201
|
+
const startedAt = now();
|
|
202
|
+
let timer;
|
|
203
|
+
let cleared = false;
|
|
204
|
+
const schedule = (ms) => {
|
|
205
|
+
timer = setTimeout(fire, ms);
|
|
206
|
+
timer.unref?.();
|
|
207
|
+
};
|
|
208
|
+
function fire() {
|
|
209
|
+
if (cleared) return;
|
|
210
|
+
const elapsedMs = now() - startedAt;
|
|
211
|
+
options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
|
|
212
|
+
addonId: options.addonId,
|
|
213
|
+
elapsedMs,
|
|
214
|
+
unpublishedCapabilities: [...options.declaredCapabilities]
|
|
215
|
+
});
|
|
216
|
+
schedule(repeatMs);
|
|
217
|
+
}
|
|
218
|
+
schedule(firstMs);
|
|
219
|
+
return { clear() {
|
|
220
|
+
cleared = true;
|
|
221
|
+
if (timer !== void 0) {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
timer = void 0;
|
|
224
|
+
}
|
|
225
|
+
} };
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/kernel/moleculer/register-framework-resolver.ts
|
|
229
|
+
/**
|
|
230
|
+
* Register the ESM resolver hook so this runner's addon imports of the
|
|
231
|
+
* host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
|
|
232
|
+
* `frameworkDir/node_modules` instead of failing to walk up from the addon's
|
|
233
|
+
* isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
|
|
234
|
+
* where workspace symlinks already resolve the framework.
|
|
235
|
+
*/
|
|
236
|
+
function registerFrameworkResolver(frameworkDir) {
|
|
237
|
+
if (!frameworkDir) return;
|
|
238
|
+
(0, node_module.register)((0, node_url.pathToFileURL)(node_path.join(__dirname, "framework-resolver-hook.mjs")), {
|
|
239
|
+
parentURL: (0, node_url.pathToFileURL)(`${__dirname}/`).href,
|
|
240
|
+
data: { frameworkDir }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/kernel/moleculer/worker-device-restore.ts
|
|
245
|
+
/**
|
|
246
|
+
* Worker-side device restore helper.
|
|
247
|
+
*
|
|
248
|
+
* Shared by `process-runner` (single-addon subprocess) and
|
|
249
|
+
* `group-runner` (group subprocess). Calls the worker addon's
|
|
250
|
+
* `restoreDevices(SavedDevice[])` with devices read from the hub's
|
|
251
|
+
* `device-manager` capability. Blocks on the `system.ready-state`
|
|
252
|
+
* readiness of the hub's `device-manager` via
|
|
253
|
+
* `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
|
|
254
|
+
*
|
|
255
|
+
* Lives in its own module so it can be imported without triggering
|
|
256
|
+
* the runner entry-point side effects (each runner is also an
|
|
257
|
+
* executable that calls `main()` at import time).
|
|
258
|
+
*/
|
|
259
|
+
async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
|
|
260
|
+
const log = context.logger;
|
|
261
|
+
log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
|
|
262
|
+
const restoreFn = addon.restoreDevices;
|
|
263
|
+
if (typeof restoreFn !== "function") {
|
|
264
|
+
log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const api = context.api;
|
|
268
|
+
if (!api) {
|
|
269
|
+
log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const bus = context.eventBus;
|
|
273
|
+
if (!bus) {
|
|
274
|
+
log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const shared = context.kernel?.readinessRegistry ?? null;
|
|
278
|
+
const registry = shared ?? new _camstack_types_addon.ReadinessRegistry({
|
|
279
|
+
eventBus: bus,
|
|
280
|
+
sourceNodeId,
|
|
281
|
+
logger: context.logger
|
|
282
|
+
});
|
|
283
|
+
log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
|
|
284
|
+
try {
|
|
285
|
+
await registry.awaitReady("device-manager", {
|
|
286
|
+
type: "node",
|
|
287
|
+
nodeId: "hub"
|
|
288
|
+
}, { timeoutMs: 6e4 });
|
|
289
|
+
log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (err instanceof _camstack_types_addon.ReadinessTimeoutError) {
|
|
292
|
+
context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw err;
|
|
296
|
+
} finally {
|
|
297
|
+
if (!shared && registry instanceof _camstack_types_addon.ReadinessRegistry) registry.close();
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const deviceManager = Reflect.get(api, "deviceManager");
|
|
301
|
+
log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
|
|
302
|
+
const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
|
|
303
|
+
log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
|
|
304
|
+
const rows = Array.isArray(rowsResult) ? rowsResult : [];
|
|
305
|
+
if (rows.length === 0) {
|
|
306
|
+
context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const savedDevices = await Promise.all(rows.map(async (row) => {
|
|
310
|
+
const config = await deviceManager.loadConfig.query({ deviceId: row.id });
|
|
311
|
+
return {
|
|
312
|
+
id: row.id,
|
|
313
|
+
stableId: row.stableId,
|
|
314
|
+
type: row.type,
|
|
315
|
+
name: row.name,
|
|
316
|
+
parentDeviceId: row.parentDeviceId,
|
|
317
|
+
config: config ?? {}
|
|
318
|
+
};
|
|
319
|
+
}));
|
|
320
|
+
await restoreFn.call(addon, savedDevices);
|
|
321
|
+
log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${(0, _camstack_types_addon.errMsg)(err)}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
327
|
//#region src/kernel/moleculer/addon-runner.ts
|
|
328
328
|
/**
|
|
329
329
|
* Addon Runner — THE unified entry point for spawned subprocesses.
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-
|
|
1
|
+
import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-Dmcnb287.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -6,229 +6,6 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as v8 from "node:v8";
|
|
7
7
|
import { ReadinessRegistry, ReadinessTimeoutError, errMsg, isCollectionArrayMethodName, normalizeAddonInitResult } from "@camstack/types/addon";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
-
//#region src/kernel/moleculer/worker-device-restore.ts
|
|
10
|
-
/**
|
|
11
|
-
* Worker-side device restore helper.
|
|
12
|
-
*
|
|
13
|
-
* Shared by `process-runner` (single-addon subprocess) and
|
|
14
|
-
* `group-runner` (group subprocess). Calls the worker addon's
|
|
15
|
-
* `restoreDevices(SavedDevice[])` with devices read from the hub's
|
|
16
|
-
* `device-manager` capability. Blocks on the `system.ready-state`
|
|
17
|
-
* readiness of the hub's `device-manager` via
|
|
18
|
-
* `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
|
|
19
|
-
*
|
|
20
|
-
* Lives in its own module so it can be imported without triggering
|
|
21
|
-
* the runner entry-point side effects (each runner is also an
|
|
22
|
-
* executable that calls `main()` at import time).
|
|
23
|
-
*/
|
|
24
|
-
async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
|
|
25
|
-
const log = context.logger;
|
|
26
|
-
log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
|
|
27
|
-
const restoreFn = addon.restoreDevices;
|
|
28
|
-
if (typeof restoreFn !== "function") {
|
|
29
|
-
log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const api = context.api;
|
|
33
|
-
if (!api) {
|
|
34
|
-
log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const bus = context.eventBus;
|
|
38
|
-
if (!bus) {
|
|
39
|
-
log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const shared = context.kernel?.readinessRegistry ?? null;
|
|
43
|
-
const registry = shared ?? new ReadinessRegistry({
|
|
44
|
-
eventBus: bus,
|
|
45
|
-
sourceNodeId,
|
|
46
|
-
logger: context.logger
|
|
47
|
-
});
|
|
48
|
-
log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
|
|
49
|
-
try {
|
|
50
|
-
await registry.awaitReady("device-manager", {
|
|
51
|
-
type: "node",
|
|
52
|
-
nodeId: "hub"
|
|
53
|
-
}, { timeoutMs: 6e4 });
|
|
54
|
-
log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
if (err instanceof ReadinessTimeoutError) {
|
|
57
|
-
context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
throw err;
|
|
61
|
-
} finally {
|
|
62
|
-
if (!shared && registry instanceof ReadinessRegistry) registry.close();
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const deviceManager = Reflect.get(api, "deviceManager");
|
|
66
|
-
log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
|
|
67
|
-
const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
|
|
68
|
-
log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
|
|
69
|
-
const rows = Array.isArray(rowsResult) ? rowsResult : [];
|
|
70
|
-
if (rows.length === 0) {
|
|
71
|
-
context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const savedDevices = await Promise.all(rows.map(async (row) => {
|
|
75
|
-
const config = await deviceManager.loadConfig.query({ deviceId: row.id });
|
|
76
|
-
return {
|
|
77
|
-
id: row.id,
|
|
78
|
-
stableId: row.stableId,
|
|
79
|
-
type: row.type,
|
|
80
|
-
name: row.name,
|
|
81
|
-
parentDeviceId: row.parentDeviceId,
|
|
82
|
-
config: config ?? {}
|
|
83
|
-
};
|
|
84
|
-
}));
|
|
85
|
-
await restoreFn.call(addon, savedDevices);
|
|
86
|
-
log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${errMsg(err)}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
92
|
-
//#region src/kernel/moleculer/register-framework-resolver.ts
|
|
93
|
-
/**
|
|
94
|
-
* Register the ESM resolver hook so this runner's addon imports of the
|
|
95
|
-
* host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
|
|
96
|
-
* `frameworkDir/node_modules` instead of failing to walk up from the addon's
|
|
97
|
-
* isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
|
|
98
|
-
* where workspace symlinks already resolve the framework.
|
|
99
|
-
*/
|
|
100
|
-
function registerFrameworkResolver(frameworkDir) {
|
|
101
|
-
if (!frameworkDir) return;
|
|
102
|
-
register(pathToFileURL(path$1.join(__dirname, "framework-resolver-hook.mjs")), {
|
|
103
|
-
parentURL: pathToFileURL(`${__dirname}/`).href,
|
|
104
|
-
data: { frameworkDir }
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Start warning if `initialize()` has not returned within `firstMs`.
|
|
109
|
-
*
|
|
110
|
-
* The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
|
|
111
|
-
* runner turns into `process.exit(1)`) does not keep the timer alive.
|
|
112
|
-
*/
|
|
113
|
-
function startInitWatchdog(options) {
|
|
114
|
-
const now = options.now ?? Date.now;
|
|
115
|
-
const firstMs = options.firstMs ?? 6e4;
|
|
116
|
-
const repeatMs = options.repeatMs ?? 3e5;
|
|
117
|
-
const startedAt = now();
|
|
118
|
-
let timer;
|
|
119
|
-
let cleared = false;
|
|
120
|
-
const schedule = (ms) => {
|
|
121
|
-
timer = setTimeout(fire, ms);
|
|
122
|
-
timer.unref?.();
|
|
123
|
-
};
|
|
124
|
-
function fire() {
|
|
125
|
-
if (cleared) return;
|
|
126
|
-
const elapsedMs = now() - startedAt;
|
|
127
|
-
options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
|
|
128
|
-
addonId: options.addonId,
|
|
129
|
-
elapsedMs,
|
|
130
|
-
unpublishedCapabilities: [...options.declaredCapabilities]
|
|
131
|
-
});
|
|
132
|
-
schedule(repeatMs);
|
|
133
|
-
}
|
|
134
|
-
schedule(firstMs);
|
|
135
|
-
return { clear() {
|
|
136
|
-
cleared = true;
|
|
137
|
-
if (timer !== void 0) {
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
timer = void 0;
|
|
140
|
-
}
|
|
141
|
-
} };
|
|
142
|
-
}
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/kernel/moleculer/child-cap-dispatch.ts
|
|
145
|
-
/** Type guard narrowing a dynamically-read provider member to a callable method. */
|
|
146
|
-
function isCapMethod(value) {
|
|
147
|
-
return typeof value === "function";
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Extract a string `addonId` out of arbitrary method args — the fallback
|
|
151
|
-
* provider-selection hint for calls that arrive WITHOUT the out-of-band
|
|
152
|
-
* `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
|
|
153
|
-
* forwarded by the parent, where the raw input still carries `addonId`).
|
|
154
|
-
*/
|
|
155
|
-
function extractArgsAddonId(args) {
|
|
156
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
157
|
-
const raw = Reflect.get(args, "addonId");
|
|
158
|
-
return typeof raw === "string" ? raw : void 0;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Creates a unified dispatcher that routes a {@link CapCallInput} to either a
|
|
162
|
-
* device-scoped or singleton provider, then invokes the requested method.
|
|
163
|
-
*
|
|
164
|
-
* Pure module — no Moleculer, no broker, no addon-runner dependency.
|
|
165
|
-
*/
|
|
166
|
-
function createChildCapDispatch(deps) {
|
|
167
|
-
return async (call) => {
|
|
168
|
-
const { capName, method, args, deviceId } = call;
|
|
169
|
-
const addonId = call.addonId ?? extractArgsAddonId(args);
|
|
170
|
-
let provider;
|
|
171
|
-
if (deviceId !== void 0) {
|
|
172
|
-
provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
|
|
173
|
-
if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
|
|
174
|
-
} else {
|
|
175
|
-
provider = deps.getSingletonProvider(capName, addonId);
|
|
176
|
-
if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
|
|
177
|
-
}
|
|
178
|
-
const fn = Reflect.get(provider, method);
|
|
179
|
-
if (!isCapMethod(fn)) {
|
|
180
|
-
if (deps.isCollectionListMethod(capName, method)) return [];
|
|
181
|
-
throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
|
|
182
|
-
}
|
|
183
|
-
return await fn.call(provider, args);
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
//#endregion
|
|
187
|
-
//#region src/kernel/moleculer/child-cap-descriptors.ts
|
|
188
|
-
/**
|
|
189
|
-
* Builds the {@link ChildCapDescriptor} array that the UDS child transport
|
|
190
|
-
* sends to the parent on registration.
|
|
191
|
-
*
|
|
192
|
-
* - Singleton caps come from the runner UNION manifest (all caps registered by
|
|
193
|
-
* every addon in this runner). One descriptor per unique (addonId, capName)
|
|
194
|
-
* pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
|
|
195
|
-
* parent's manifest builder register each hosted addon's caps under the
|
|
196
|
-
* REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
|
|
197
|
-
* and deduplicating by capName alone would collapse two co-located providers
|
|
198
|
-
* of the same collection cap (e.g. `network-access` from cloudflare-tunnel
|
|
199
|
-
* AND tailscale-ingress) into one descriptor registered under a synthetic
|
|
200
|
-
* `addonId = childId` (the group name) — the "@camstack/addon-net-access"
|
|
201
|
-
* admin-UI bug.
|
|
202
|
-
* - Device-scoped caps come from the native-cap snapshot — one descriptor per
|
|
203
|
-
* (capName, deviceId) pair, mode `'collection'`, stamped with the owning
|
|
204
|
-
* addon's id.
|
|
205
|
-
*
|
|
206
|
-
* Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
|
|
207
|
-
* import time) so it is unit-testable.
|
|
208
|
-
*/
|
|
209
|
-
function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
|
|
210
|
-
const descriptors = [];
|
|
211
|
-
const seen = /* @__PURE__ */ new Set();
|
|
212
|
-
for (const entry of manifest) for (const capName of entry.capabilities) {
|
|
213
|
-
const key = `${entry.addonId}::${capName}`;
|
|
214
|
-
if (!seen.has(key)) {
|
|
215
|
-
seen.add(key);
|
|
216
|
-
descriptors.push({
|
|
217
|
-
capName,
|
|
218
|
-
mode: "singleton",
|
|
219
|
-
addonId: entry.addonId
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
|
|
224
|
-
capName,
|
|
225
|
-
mode: "collection",
|
|
226
|
-
deviceId,
|
|
227
|
-
addonId
|
|
228
|
-
});
|
|
229
|
-
return descriptors;
|
|
230
|
-
}
|
|
231
|
-
//#endregion
|
|
232
9
|
//#region src/kernel/moleculer/child-addon-call-dispatch.ts
|
|
233
10
|
function isRecord$1(value) {
|
|
234
11
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -320,6 +97,229 @@ function createChildAddonCallDispatch(deps) {
|
|
|
320
97
|
};
|
|
321
98
|
}
|
|
322
99
|
//#endregion
|
|
100
|
+
//#region src/kernel/moleculer/child-cap-descriptors.ts
|
|
101
|
+
/**
|
|
102
|
+
* Builds the {@link ChildCapDescriptor} array that the UDS child transport
|
|
103
|
+
* sends to the parent on registration.
|
|
104
|
+
*
|
|
105
|
+
* - Singleton caps come from the runner UNION manifest (all caps registered by
|
|
106
|
+
* every addon in this runner). One descriptor per unique (addonId, capName)
|
|
107
|
+
* pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
|
|
108
|
+
* parent's manifest builder register each hosted addon's caps under the
|
|
109
|
+
* REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
|
|
110
|
+
* and deduplicating by capName alone would collapse two co-located providers
|
|
111
|
+
* of the same collection cap (e.g. `network-access` from cloudflare-tunnel
|
|
112
|
+
* AND tailscale-ingress) into one descriptor registered under a synthetic
|
|
113
|
+
* `addonId = childId` (the group name) — the "@camstack/addon-net-access"
|
|
114
|
+
* admin-UI bug.
|
|
115
|
+
* - Device-scoped caps come from the native-cap snapshot — one descriptor per
|
|
116
|
+
* (capName, deviceId) pair, mode `'collection'`, stamped with the owning
|
|
117
|
+
* addon's id.
|
|
118
|
+
*
|
|
119
|
+
* Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
|
|
120
|
+
* import time) so it is unit-testable.
|
|
121
|
+
*/
|
|
122
|
+
function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
|
|
123
|
+
const descriptors = [];
|
|
124
|
+
const seen = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const entry of manifest) for (const capName of entry.capabilities) {
|
|
126
|
+
const key = `${entry.addonId}::${capName}`;
|
|
127
|
+
if (!seen.has(key)) {
|
|
128
|
+
seen.add(key);
|
|
129
|
+
descriptors.push({
|
|
130
|
+
capName,
|
|
131
|
+
mode: "singleton",
|
|
132
|
+
addonId: entry.addonId
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
|
|
137
|
+
capName,
|
|
138
|
+
mode: "collection",
|
|
139
|
+
deviceId,
|
|
140
|
+
addonId
|
|
141
|
+
});
|
|
142
|
+
return descriptors;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/kernel/moleculer/child-cap-dispatch.ts
|
|
146
|
+
/** Type guard narrowing a dynamically-read provider member to a callable method. */
|
|
147
|
+
function isCapMethod(value) {
|
|
148
|
+
return typeof value === "function";
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Extract a string `addonId` out of arbitrary method args — the fallback
|
|
152
|
+
* provider-selection hint for calls that arrive WITHOUT the out-of-band
|
|
153
|
+
* `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
|
|
154
|
+
* forwarded by the parent, where the raw input still carries `addonId`).
|
|
155
|
+
*/
|
|
156
|
+
function extractArgsAddonId(args) {
|
|
157
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
158
|
+
const raw = Reflect.get(args, "addonId");
|
|
159
|
+
return typeof raw === "string" ? raw : void 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Creates a unified dispatcher that routes a {@link CapCallInput} to either a
|
|
163
|
+
* device-scoped or singleton provider, then invokes the requested method.
|
|
164
|
+
*
|
|
165
|
+
* Pure module — no Moleculer, no broker, no addon-runner dependency.
|
|
166
|
+
*/
|
|
167
|
+
function createChildCapDispatch(deps) {
|
|
168
|
+
return async (call) => {
|
|
169
|
+
const { capName, method, args, deviceId } = call;
|
|
170
|
+
const addonId = call.addonId ?? extractArgsAddonId(args);
|
|
171
|
+
let provider;
|
|
172
|
+
if (deviceId !== void 0) {
|
|
173
|
+
provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
|
|
174
|
+
if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
|
|
175
|
+
} else {
|
|
176
|
+
provider = deps.getSingletonProvider(capName, addonId);
|
|
177
|
+
if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
|
|
178
|
+
}
|
|
179
|
+
const fn = Reflect.get(provider, method);
|
|
180
|
+
if (!isCapMethod(fn)) {
|
|
181
|
+
if (deps.isCollectionListMethod(capName, method)) return [];
|
|
182
|
+
throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
|
|
183
|
+
}
|
|
184
|
+
return await fn.call(provider, args);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Start warning if `initialize()` has not returned within `firstMs`.
|
|
189
|
+
*
|
|
190
|
+
* The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
|
|
191
|
+
* runner turns into `process.exit(1)`) does not keep the timer alive.
|
|
192
|
+
*/
|
|
193
|
+
function startInitWatchdog(options) {
|
|
194
|
+
const now = options.now ?? Date.now;
|
|
195
|
+
const firstMs = options.firstMs ?? 6e4;
|
|
196
|
+
const repeatMs = options.repeatMs ?? 3e5;
|
|
197
|
+
const startedAt = now();
|
|
198
|
+
let timer;
|
|
199
|
+
let cleared = false;
|
|
200
|
+
const schedule = (ms) => {
|
|
201
|
+
timer = setTimeout(fire, ms);
|
|
202
|
+
timer.unref?.();
|
|
203
|
+
};
|
|
204
|
+
function fire() {
|
|
205
|
+
if (cleared) return;
|
|
206
|
+
const elapsedMs = now() - startedAt;
|
|
207
|
+
options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
|
|
208
|
+
addonId: options.addonId,
|
|
209
|
+
elapsedMs,
|
|
210
|
+
unpublishedCapabilities: [...options.declaredCapabilities]
|
|
211
|
+
});
|
|
212
|
+
schedule(repeatMs);
|
|
213
|
+
}
|
|
214
|
+
schedule(firstMs);
|
|
215
|
+
return { clear() {
|
|
216
|
+
cleared = true;
|
|
217
|
+
if (timer !== void 0) {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
timer = void 0;
|
|
220
|
+
}
|
|
221
|
+
} };
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/kernel/moleculer/register-framework-resolver.ts
|
|
225
|
+
/**
|
|
226
|
+
* Register the ESM resolver hook so this runner's addon imports of the
|
|
227
|
+
* host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
|
|
228
|
+
* `frameworkDir/node_modules` instead of failing to walk up from the addon's
|
|
229
|
+
* isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
|
|
230
|
+
* where workspace symlinks already resolve the framework.
|
|
231
|
+
*/
|
|
232
|
+
function registerFrameworkResolver(frameworkDir) {
|
|
233
|
+
if (!frameworkDir) return;
|
|
234
|
+
register(pathToFileURL(path$1.join(__dirname, "framework-resolver-hook.mjs")), {
|
|
235
|
+
parentURL: pathToFileURL(`${__dirname}/`).href,
|
|
236
|
+
data: { frameworkDir }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/kernel/moleculer/worker-device-restore.ts
|
|
241
|
+
/**
|
|
242
|
+
* Worker-side device restore helper.
|
|
243
|
+
*
|
|
244
|
+
* Shared by `process-runner` (single-addon subprocess) and
|
|
245
|
+
* `group-runner` (group subprocess). Calls the worker addon's
|
|
246
|
+
* `restoreDevices(SavedDevice[])` with devices read from the hub's
|
|
247
|
+
* `device-manager` capability. Blocks on the `system.ready-state`
|
|
248
|
+
* readiness of the hub's `device-manager` via
|
|
249
|
+
* `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
|
|
250
|
+
*
|
|
251
|
+
* Lives in its own module so it can be imported without triggering
|
|
252
|
+
* the runner entry-point side effects (each runner is also an
|
|
253
|
+
* executable that calls `main()` at import time).
|
|
254
|
+
*/
|
|
255
|
+
async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
|
|
256
|
+
const log = context.logger;
|
|
257
|
+
log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
|
|
258
|
+
const restoreFn = addon.restoreDevices;
|
|
259
|
+
if (typeof restoreFn !== "function") {
|
|
260
|
+
log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const api = context.api;
|
|
264
|
+
if (!api) {
|
|
265
|
+
log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const bus = context.eventBus;
|
|
269
|
+
if (!bus) {
|
|
270
|
+
log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const shared = context.kernel?.readinessRegistry ?? null;
|
|
274
|
+
const registry = shared ?? new ReadinessRegistry({
|
|
275
|
+
eventBus: bus,
|
|
276
|
+
sourceNodeId,
|
|
277
|
+
logger: context.logger
|
|
278
|
+
});
|
|
279
|
+
log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
|
|
280
|
+
try {
|
|
281
|
+
await registry.awaitReady("device-manager", {
|
|
282
|
+
type: "node",
|
|
283
|
+
nodeId: "hub"
|
|
284
|
+
}, { timeoutMs: 6e4 });
|
|
285
|
+
log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err instanceof ReadinessTimeoutError) {
|
|
288
|
+
context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
throw err;
|
|
292
|
+
} finally {
|
|
293
|
+
if (!shared && registry instanceof ReadinessRegistry) registry.close();
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const deviceManager = Reflect.get(api, "deviceManager");
|
|
297
|
+
log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
|
|
298
|
+
const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
|
|
299
|
+
log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
|
|
300
|
+
const rows = Array.isArray(rowsResult) ? rowsResult : [];
|
|
301
|
+
if (rows.length === 0) {
|
|
302
|
+
context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const savedDevices = await Promise.all(rows.map(async (row) => {
|
|
306
|
+
const config = await deviceManager.loadConfig.query({ deviceId: row.id });
|
|
307
|
+
return {
|
|
308
|
+
id: row.id,
|
|
309
|
+
stableId: row.stableId,
|
|
310
|
+
type: row.type,
|
|
311
|
+
name: row.name,
|
|
312
|
+
parentDeviceId: row.parentDeviceId,
|
|
313
|
+
config: config ?? {}
|
|
314
|
+
};
|
|
315
|
+
}));
|
|
316
|
+
await restoreFn.call(addon, savedDevices);
|
|
317
|
+
log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${errMsg(err)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
323
|
//#region src/kernel/moleculer/addon-runner.ts
|
|
324
324
|
/**
|
|
325
325
|
* Addon Runner — THE unified entry point for spawned subprocesses.
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const require_builtins_system_config_system_config_addon = require("./builtins/s
|
|
|
25
25
|
require("./builtins/system-config/index.js");
|
|
26
26
|
const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
|
|
27
27
|
const require_file_data_plane = require("./file-data-plane-DO8KbxCe.js");
|
|
28
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
28
|
+
const require_manifest_python_deps = require("./manifest-python-deps-BTkFwAk_.js");
|
|
29
29
|
const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
|
|
30
30
|
const require_lan_http_bind = require("./lan-http-bind-DmgpFP6_.js");
|
|
31
31
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
|
@@ -93976,6 +93976,34 @@ var CrashSupervisor = class {
|
|
|
93976
93976
|
this.crashes.delete(runnerId);
|
|
93977
93977
|
}
|
|
93978
93978
|
};
|
|
93979
|
+
/**
|
|
93980
|
+
* Resolve the native-allocator env for a child runner from the parent's env.
|
|
93981
|
+
*
|
|
93982
|
+
* Precedence, highest first:
|
|
93983
|
+
* 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
|
|
93984
|
+
* restores the glibc/sharp defaults. Recoverable without a rebuild.
|
|
93985
|
+
* 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
|
|
93986
|
+
* `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
|
|
93987
|
+
* 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
|
|
93988
|
+
* 4. The defaults above.
|
|
93989
|
+
*
|
|
93990
|
+
* A nonsense override falls back to the default rather than being passed through:
|
|
93991
|
+
* glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
|
|
93992
|
+
* exists to leave.
|
|
93993
|
+
*/
|
|
93994
|
+
function runnerNativeAllocatorEnv(parentEnv, platform = process.platform) {
|
|
93995
|
+
if (platform !== "linux") return {};
|
|
93996
|
+
if (parentEnv["CAMSTACK_RUNNER_NATIVE_ALLOCATOR"] === "off") return {};
|
|
93997
|
+
return {
|
|
93998
|
+
MALLOC_ARENA_MAX: parentEnv["MALLOC_ARENA_MAX"] ?? String(positiveInt(parentEnv["CAMSTACK_RUNNER_MALLOC_ARENA_MAX"], 2)),
|
|
93999
|
+
VIPS_CONCURRENCY: parentEnv["VIPS_CONCURRENCY"] ?? String(1)
|
|
94000
|
+
};
|
|
94001
|
+
}
|
|
94002
|
+
function positiveInt(raw, fallback) {
|
|
94003
|
+
if (raw === void 0) return fallback;
|
|
94004
|
+
const n = Number.parseInt(raw, 10);
|
|
94005
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
94006
|
+
}
|
|
93979
94007
|
//#endregion
|
|
93980
94008
|
//#region src/kernel/moleculer/process-service.ts
|
|
93981
94009
|
/**
|
|
@@ -94136,7 +94164,18 @@ function isHeavyRunner(addons) {
|
|
|
94136
94164
|
*
|
|
94137
94165
|
* A runner whose working set genuinely does not fit declares its own number in its manifest
|
|
94138
94166
|
* (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
|
|
94139
|
-
* `
|
|
94167
|
+
* `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
|
|
94168
|
+
* shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
|
|
94169
|
+
* `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
|
|
94170
|
+
* space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
|
|
94171
|
+
* from RSS.
|
|
94172
|
+
*
|
|
94173
|
+
* ## What this flag CANNOT do
|
|
94174
|
+
*
|
|
94175
|
+
* It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
|
|
94176
|
+
* addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
|
|
94177
|
+
* whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
|
|
94178
|
+
* Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
|
|
94140
94179
|
*/
|
|
94141
94180
|
var HEAVY_MAX_OLD_MB_DEFAULT = 1024;
|
|
94142
94181
|
/**
|
|
@@ -94262,10 +94301,11 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
94262
94301
|
CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
|
|
94263
94302
|
...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
|
|
94264
94303
|
...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
|
|
94304
|
+
...runnerNativeAllocatorEnv(process.env),
|
|
94265
94305
|
...env
|
|
94266
94306
|
};
|
|
94267
94307
|
const heapFlags = runnerHeapFlags(addons);
|
|
94268
|
-
capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
|
|
94308
|
+
capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
|
|
94269
94309
|
const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
|
|
94270
94310
|
env: childEnv,
|
|
94271
94311
|
stdio: [
|
package/dist/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.
|
|
|
24
24
|
import "./builtins/system-config/index.mjs";
|
|
25
25
|
import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
|
|
26
26
|
import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-BhKdxJgf.mjs";
|
|
27
|
-
import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-
|
|
27
|
+
import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-Dmcnb287.mjs";
|
|
28
28
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
|
|
29
29
|
import { C as evaluateExistingCert, S as SERVER_AUTH_OID, _ as CA_COMMON_NAME, a as closeLanHttp, b as LEAF_RENEWAL_WINDOW_DAYS, c as readExtraSans, d as writeExtraSans, f as writeTlsMode, g as reissueTlsLeaf, h as loadTlsCert, i as bindPendingLanHttp, l as readTlsAccessStatus, m as ensureTlsCert, n as allFamiliesListenHost, o as readLanHttpState, p as validateUploadedTls, r as applyLanHttp, s as registerLanHttpHandler, t as DEFAULT_LAN_HTTP_PORT, u as readTlsMode, v as collectCertIdentity, x as MAX_LEAF_VALIDITY_DAYS, y as CA_VALIDITY_DAYS } from "./lan-http-bind-jKrj6OjQ.mjs";
|
|
30
30
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
@@ -93969,6 +93969,34 @@ var CrashSupervisor = class {
|
|
|
93969
93969
|
this.crashes.delete(runnerId);
|
|
93970
93970
|
}
|
|
93971
93971
|
};
|
|
93972
|
+
/**
|
|
93973
|
+
* Resolve the native-allocator env for a child runner from the parent's env.
|
|
93974
|
+
*
|
|
93975
|
+
* Precedence, highest first:
|
|
93976
|
+
* 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
|
|
93977
|
+
* restores the glibc/sharp defaults. Recoverable without a rebuild.
|
|
93978
|
+
* 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
|
|
93979
|
+
* `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
|
|
93980
|
+
* 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
|
|
93981
|
+
* 4. The defaults above.
|
|
93982
|
+
*
|
|
93983
|
+
* A nonsense override falls back to the default rather than being passed through:
|
|
93984
|
+
* glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
|
|
93985
|
+
* exists to leave.
|
|
93986
|
+
*/
|
|
93987
|
+
function runnerNativeAllocatorEnv(parentEnv, platform = process.platform) {
|
|
93988
|
+
if (platform !== "linux") return {};
|
|
93989
|
+
if (parentEnv["CAMSTACK_RUNNER_NATIVE_ALLOCATOR"] === "off") return {};
|
|
93990
|
+
return {
|
|
93991
|
+
MALLOC_ARENA_MAX: parentEnv["MALLOC_ARENA_MAX"] ?? String(positiveInt(parentEnv["CAMSTACK_RUNNER_MALLOC_ARENA_MAX"], 2)),
|
|
93992
|
+
VIPS_CONCURRENCY: parentEnv["VIPS_CONCURRENCY"] ?? String(1)
|
|
93993
|
+
};
|
|
93994
|
+
}
|
|
93995
|
+
function positiveInt(raw, fallback) {
|
|
93996
|
+
if (raw === void 0) return fallback;
|
|
93997
|
+
const n = Number.parseInt(raw, 10);
|
|
93998
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
93999
|
+
}
|
|
93972
94000
|
//#endregion
|
|
93973
94001
|
//#region src/kernel/moleculer/process-service.ts
|
|
93974
94002
|
/**
|
|
@@ -94129,7 +94157,18 @@ function isHeavyRunner(addons) {
|
|
|
94129
94157
|
*
|
|
94130
94158
|
* A runner whose working set genuinely does not fit declares its own number in its manifest
|
|
94131
94159
|
* (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
|
|
94132
|
-
* `
|
|
94160
|
+
* `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
|
|
94161
|
+
* shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
|
|
94162
|
+
* `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
|
|
94163
|
+
* space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
|
|
94164
|
+
* from RSS.
|
|
94165
|
+
*
|
|
94166
|
+
* ## What this flag CANNOT do
|
|
94167
|
+
*
|
|
94168
|
+
* It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
|
|
94169
|
+
* addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
|
|
94170
|
+
* whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
|
|
94171
|
+
* Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
|
|
94133
94172
|
*/
|
|
94134
94173
|
var HEAVY_MAX_OLD_MB_DEFAULT = 1024;
|
|
94135
94174
|
/**
|
|
@@ -94255,10 +94294,11 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
94255
94294
|
CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
|
|
94256
94295
|
...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
|
|
94257
94296
|
...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
|
|
94297
|
+
...runnerNativeAllocatorEnv(process.env),
|
|
94258
94298
|
...env
|
|
94259
94299
|
};
|
|
94260
94300
|
const heapFlags = runnerHeapFlags(addons);
|
|
94261
|
-
capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
|
|
94301
|
+
capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
|
|
94262
94302
|
const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
|
|
94263
94303
|
env: childEnv,
|
|
94264
94304
|
stdio: [
|
|
@@ -291,14 +291,17 @@ export interface RunnerHeapWatchOptions {
|
|
|
291
291
|
*
|
|
292
292
|
* ## Why heavy runners get hub-main's mechanism
|
|
293
293
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* `
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
294
|
+
* Because a heap ceiling cannot reach what actually strands in them. Every
|
|
295
|
+
* media-path runner now carries an old-space ceiling sized from its own
|
|
296
|
+
* measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
|
|
297
|
+
* and that changes nothing here: what a heavy runner strands is NATIVE —
|
|
298
|
+
* `external`, `arrayBuffers`, glibc arena free lists — none of which
|
|
299
|
+
* `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
|
|
300
|
+
* reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
|
|
301
|
+
* ceiling that was in force the whole time. hub-main had exactly this shape and
|
|
302
|
+
* its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
|
|
303
|
+
* at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
|
|
304
|
+
* gate; never a second implementation.
|
|
302
305
|
*
|
|
303
306
|
* ## The stall, priced for the runners that get it
|
|
304
307
|
*
|
|
@@ -36,7 +36,18 @@ interface RunnerAddonSpec {
|
|
|
36
36
|
*
|
|
37
37
|
* A runner whose working set genuinely does not fit declares its own number in its manifest
|
|
38
38
|
* (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
|
|
39
|
-
* `
|
|
39
|
+
* `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
|
|
40
|
+
* shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
|
|
41
|
+
* `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
|
|
42
|
+
* space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
|
|
43
|
+
* from RSS.
|
|
44
|
+
*
|
|
45
|
+
* ## What this flag CANNOT do
|
|
46
|
+
*
|
|
47
|
+
* It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
|
|
48
|
+
* addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
|
|
49
|
+
* whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
|
|
50
|
+
* Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
|
|
40
51
|
*/
|
|
41
52
|
export declare const HEAVY_MAX_OLD_MB_DEFAULT = 1024;
|
|
42
53
|
interface SpawnedProcess {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native-allocator environment for a forked addon runner.
|
|
3
|
+
*
|
|
4
|
+
* ## The half of a runner's memory that no V8 flag reaches
|
|
5
|
+
*
|
|
6
|
+
* `--max-old-space-size` bounds V8's old space and nothing else. It does not
|
|
7
|
+
* bound `external`, it does not bound `arrayBuffers`, and it does not bound what
|
|
8
|
+
* a native addon (`sharp`/`libvips`, decoders, codecs) allocates through `malloc`.
|
|
9
|
+
*
|
|
10
|
+
* Measured on the live hub, 2026-08-25, over a ~6h window of `[mem]` heartbeats:
|
|
11
|
+
*
|
|
12
|
+
* hub/pipeline-analytics rss 309-7334MB heapTotal max 343MB external max 5479MB
|
|
13
|
+
*
|
|
14
|
+
* It already carried `--max-old-space-size=1024`, so the flag was in force the
|
|
15
|
+
* whole time and the process still reached 7.3GB — because 96% of it was never
|
|
16
|
+
* V8's to bound. Its reclaim line names the shape exactly:
|
|
17
|
+
*
|
|
18
|
+
* reclaim hub/pipeline-analytics stranded=2552MB rss=2765MB→2647MB
|
|
19
|
+
* freed=118MB arrayBuffers=81MB→29MB took=58ms
|
|
20
|
+
*
|
|
21
|
+
* The `arrayBuffers` FLOOR returns to ~1MB every cycle, so this is not a leak.
|
|
22
|
+
* It is glibc holding freed pages: with `MALLOC_ARENA_MAX` unset, glibc opens up
|
|
23
|
+
* to `8 × ncpu` arenas — 160 on this 20-core host — and each keeps its own free
|
|
24
|
+
* list, which is returned to the OS only when the top of that arena is free.
|
|
25
|
+
*
|
|
26
|
+
* ## Why this matters more than the cgroup number suggests
|
|
27
|
+
*
|
|
28
|
+
* The container is capped at 24GiB and `memory.current` rides at 21-24GB. Near
|
|
29
|
+
* the limit the kernel performs direct reclaim IN THE CONTEXT OF THE TASK THAT
|
|
30
|
+
* ALLOCATES, so every process in the container stalls together — measured as
|
|
31
|
+
* four idle, mutually unrelated runners sharing one ~39s stall in the same
|
|
32
|
+
* sample. Returning stranded pages is therefore not a tidiness win; it is what
|
|
33
|
+
* keeps unrelated runners off the direct-reclaim path.
|
|
34
|
+
*
|
|
35
|
+
* ## Why `VIPS_CONCURRENCY` is welded to it
|
|
36
|
+
*
|
|
37
|
+
* `sharp` reads `MALLOC_ARENA_MAX` in its NATIVE binding (not in JS — grepping
|
|
38
|
+
* the bundle finds nothing). Unset on glibc, sharp pins libvips concurrency to 1
|
|
39
|
+
* as its own fragmentation defence; the moment the variable appears it restores
|
|
40
|
+
* concurrency to `availableParallelism()`. Verified in the live container:
|
|
41
|
+
*
|
|
42
|
+
* MALLOC_ARENA_MAX unset → sharp.concurrency() === 1
|
|
43
|
+
* MALLOC_ARENA_MAX=2 → sharp.concurrency() === 20
|
|
44
|
+
* MALLOC_ARENA_MAX=2 VIPS_CONCURRENCY=1 → sharp.concurrency() === 1
|
|
45
|
+
*
|
|
46
|
+
* Shipping the arena bound alone would therefore have quietly shipped a 1→20
|
|
47
|
+
* libvips threading change to the busiest addon on a host already at ~75% CPU,
|
|
48
|
+
* under the banner of a memory fix. This module ships the allocator change and
|
|
49
|
+
* ONLY the allocator change; raising libvips concurrency is a separate decision.
|
|
50
|
+
*
|
|
51
|
+
* ## What it costs, measured
|
|
52
|
+
*
|
|
53
|
+
* Same container, same 12-image corpus, same pipeline as analytics (decode →
|
|
54
|
+
* extract → resize → encode → raw), 3 interleaved pairs so host load cancels,
|
|
55
|
+
* concurrency pinned to 1 on both arms:
|
|
56
|
+
*
|
|
57
|
+
* arenas default : mean 51.9ms p50 60.0ms p90 67.3ms p99 70.7ms rss 505MB
|
|
58
|
+
* arenas=2 : mean 51.5ms p50 57.6ms p90 67.2ms p99 72.9ms rss 354MB
|
|
59
|
+
*
|
|
60
|
+
* Latency is unchanged inside noise (-0.8% mean, +3.1% p99); the same work is
|
|
61
|
+
* done in 30% less RSS.
|
|
62
|
+
*/
|
|
63
|
+
/** Bounded glibc arenas per forked runner. */
|
|
64
|
+
export declare const RUNNER_MALLOC_ARENA_MAX_DEFAULT = 2;
|
|
65
|
+
/**
|
|
66
|
+
* libvips worker threads per forked runner. `1` is not a tuning choice — it is
|
|
67
|
+
* sharp's OWN default on glibc, pinned here so that bounding the arenas does not
|
|
68
|
+
* change it as a side effect. See the module header.
|
|
69
|
+
*/
|
|
70
|
+
export declare const RUNNER_VIPS_CONCURRENCY_DEFAULT = 1;
|
|
71
|
+
/**
|
|
72
|
+
* The native-allocator variables to add to a runner's environment. Both keys are
|
|
73
|
+
* present or both are absent — see the `VIPS_CONCURRENCY` note in the module
|
|
74
|
+
* header for why one without the other is the dangerous configuration.
|
|
75
|
+
*/
|
|
76
|
+
export interface RunnerNativeAllocatorEnv {
|
|
77
|
+
readonly MALLOC_ARENA_MAX?: string;
|
|
78
|
+
readonly VIPS_CONCURRENCY?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the native-allocator env for a child runner from the parent's env.
|
|
82
|
+
*
|
|
83
|
+
* Precedence, highest first:
|
|
84
|
+
* 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
|
|
85
|
+
* restores the glibc/sharp defaults. Recoverable without a rebuild.
|
|
86
|
+
* 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
|
|
87
|
+
* `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
|
|
88
|
+
* 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
|
|
89
|
+
* 4. The defaults above.
|
|
90
|
+
*
|
|
91
|
+
* A nonsense override falls back to the default rather than being passed through:
|
|
92
|
+
* glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
|
|
93
|
+
* exists to leave.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runnerNativeAllocatorEnv(parentEnv: NodeJS.ProcessEnv, platform?: NodeJS.Platform): RunnerNativeAllocatorEnv;
|
|
@@ -422,14 +422,17 @@ var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
|
|
|
422
422
|
*
|
|
423
423
|
* ## Why heavy runners get hub-main's mechanism
|
|
424
424
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* `
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
425
|
+
* Because a heap ceiling cannot reach what actually strands in them. Every
|
|
426
|
+
* media-path runner now carries an old-space ceiling sized from its own
|
|
427
|
+
* measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
|
|
428
|
+
* and that changes nothing here: what a heavy runner strands is NATIVE —
|
|
429
|
+
* `external`, `arrayBuffers`, glibc arena free lists — none of which
|
|
430
|
+
* `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
|
|
431
|
+
* reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
|
|
432
|
+
* ceiling that was in force the whole time. hub-main had exactly this shape and
|
|
433
|
+
* its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
|
|
434
|
+
* at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
|
|
435
|
+
* gate; never a second implementation.
|
|
433
436
|
*
|
|
434
437
|
* ## The stall, priced for the runners that get it
|
|
435
438
|
*
|
|
@@ -453,7 +456,7 @@ function startRunnerHeapWatch(options) {
|
|
|
453
456
|
const reclaimer = createV8Reclaimer();
|
|
454
457
|
reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
|
|
455
458
|
}
|
|
456
|
-
return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0,
|
|
459
|
+
return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
|
|
457
460
|
}
|
|
458
461
|
//#endregion
|
|
459
462
|
//#region src/kernel/moleculer/addon-data-plane-facility.ts
|
|
@@ -418,14 +418,17 @@ var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
|
|
|
418
418
|
*
|
|
419
419
|
* ## Why heavy runners get hub-main's mechanism
|
|
420
420
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* `
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
421
|
+
* Because a heap ceiling cannot reach what actually strands in them. Every
|
|
422
|
+
* media-path runner now carries an old-space ceiling sized from its own
|
|
423
|
+
* measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
|
|
424
|
+
* and that changes nothing here: what a heavy runner strands is NATIVE —
|
|
425
|
+
* `external`, `arrayBuffers`, glibc arena free lists — none of which
|
|
426
|
+
* `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
|
|
427
|
+
* reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
|
|
428
|
+
* ceiling that was in force the whole time. hub-main had exactly this shape and
|
|
429
|
+
* its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
|
|
430
|
+
* at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
|
|
431
|
+
* gate; never a second implementation.
|
|
429
432
|
*
|
|
430
433
|
* ## The stall, priced for the runners that get it
|
|
431
434
|
*
|
|
@@ -449,7 +452,7 @@ function startRunnerHeapWatch(options) {
|
|
|
449
452
|
const reclaimer = createV8Reclaimer();
|
|
450
453
|
reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
|
|
451
454
|
}
|
|
452
|
-
return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0,
|
|
455
|
+
return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
|
|
453
456
|
}
|
|
454
457
|
//#endregion
|
|
455
458
|
//#region src/kernel/moleculer/addon-data-plane-facility.ts
|