@camstack/system 1.1.49 → 1.1.51
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 +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/coral-accelerators.d.ts +22 -0
- package/dist/builtins/platform-probe/index.js +67 -2
- package/dist/builtins/platform-probe/index.mjs +67 -2
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +162 -12
- package/dist/builtins/snapshot/index.mjs +162 -12
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +38 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +26 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-8phgmJ6b.mjs → dist-BWQX9yUj.mjs} +162 -8
- package/dist/{dist-BDV1WKRg.js → dist-C3uHEBtP.js} +173 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -23
- package/dist/index.mjs +37 -24
- package/dist/kernel/addon-installer.d.ts +8 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +1 -1
- package/dist/kernel/deps/npm-command.d.ts +38 -0
- package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +2 -0
- package/dist/{manifest-python-deps-B09I9ems.mjs → manifest-python-deps-BA6KA9If.mjs} +298 -127
- package/dist/{manifest-python-deps-DAA0ULwT.js → manifest-python-deps-CVcJ9hDX.js} +309 -126
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
/** How the caller must spawn npm: `execFile(command, [...argsPrefix, ...npmArgs])`. */
|
|
3
|
+
export interface NpmInvocation {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly argsPrefix: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ResolveNpmInvocationOptions {
|
|
8
|
+
/** Directory that holds (or receives) the bootstrapped npm package. */
|
|
9
|
+
readonly cacheDir: string;
|
|
10
|
+
/** npm registry base URL; defaults to the public registry. */
|
|
11
|
+
readonly registry?: string | undefined;
|
|
12
|
+
readonly logger: IScopedLogger;
|
|
13
|
+
/** Test seams — production callers omit all of these. */
|
|
14
|
+
readonly probeSystemNpm?: () => Promise<boolean>;
|
|
15
|
+
readonly fetchJson?: (url: string) => Promise<unknown>;
|
|
16
|
+
readonly downloadTarball?: (url: string, destTgz: string) => Promise<void>;
|
|
17
|
+
readonly extractTarball?: (tgzPath: string, destDir: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** Test-only: reset the process-wide memo. */
|
|
20
|
+
export declare function __clearNpmInvocationCacheForTests(): void;
|
|
21
|
+
export declare function resolveNpmInvocation(options: ResolveNpmInvocationOptions): Promise<NpmInvocation>;
|
|
22
|
+
export interface RunNpmOptions {
|
|
23
|
+
readonly cacheDir: string;
|
|
24
|
+
readonly registry?: string | undefined;
|
|
25
|
+
readonly logger: IScopedLogger;
|
|
26
|
+
readonly cwd?: string;
|
|
27
|
+
readonly timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a working npm (system → bootstrapped → bootstrap) and run it with
|
|
31
|
+
* `args`. The ONE entry point every installer code path uses instead of a raw
|
|
32
|
+
* `execFile('npm', ...)` — so "npm missing on this host" is either solved
|
|
33
|
+
* (bootstrap) or a LOUD, actionable error, never a swallowed ENOENT.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runNpm(args: readonly string[], options: RunNpmOptions): Promise<{
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr: string;
|
|
38
|
+
}>;
|
|
@@ -8,6 +8,8 @@ export interface PlatformSignals {
|
|
|
8
8
|
readonly hasRenderNode: boolean;
|
|
9
9
|
/** Intel NPU (Meteor Lake / Core Ultra "AI Boost") — exposed as /dev/accel/accel0. */
|
|
10
10
|
readonly hasIntelNpu: boolean;
|
|
11
|
+
/** Coral USB Edge TPU — a discrete USB accelerator (gates the `edgetpu` runtime). */
|
|
12
|
+
readonly hasCoral: boolean;
|
|
11
13
|
}
|
|
12
14
|
export declare function probePlatform(): PlatformSignals;
|
|
13
15
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { G as readNodePin } from "./dist-BWQX9yUj.mjs";
|
|
2
2
|
import { ensureBinary, ensureFfmpeg, ensurePython, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -6,14 +6,130 @@ import * as path$1 from "node:path";
|
|
|
6
6
|
import { isAbsolute, join } from "node:path";
|
|
7
7
|
import * as crypto$1 from "node:crypto";
|
|
8
8
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
9
|
+
import { DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, scopeKey, sleep } from "@camstack/types/addon";
|
|
9
10
|
import { execFile } from "node:child_process";
|
|
10
11
|
import { promisify } from "node:util";
|
|
11
12
|
import * as os from "node:os";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { unlink } from "node:fs/promises";
|
|
14
|
-
import { DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, scopeKey, sleep } from "@camstack/types/addon";
|
|
15
15
|
import { TRPCClientError, createTRPCClient } from "@trpc/client";
|
|
16
16
|
import { connect, createServer as createServer$1 } from "node:net";
|
|
17
|
+
//#region src/kernel/moleculer/addon-data-plane-facility.ts
|
|
18
|
+
/**
|
|
19
|
+
* Kernel side of the addon HTTP **data-plane** (see {@link AddonDataPlane}).
|
|
20
|
+
*
|
|
21
|
+
* Each addon context gets one facility. On the first `serve()`, it binds a
|
|
22
|
+
* `127.0.0.1` HTTP listener IN THE ADDON'S PROCESS and multiplexes registered
|
|
23
|
+
* prefixes → the addon's real `(req, res)` handlers (longest-prefix wins). Every
|
|
24
|
+
* request must carry the per-listener shared secret (injected by the hub's
|
|
25
|
+
* reverse-proxy) — anything else gets 403, so no other local process can reach
|
|
26
|
+
* the listener. The facility reports its live endpoints through a `sink` so the
|
|
27
|
+
* runner can hand them to the hub (which pulls them when it mounts the addon).
|
|
28
|
+
*
|
|
29
|
+
* The addon never sees the secret, the port, or any transport detail — it just
|
|
30
|
+
* streams to `res`. The hub authenticates and reverse-proxies in front.
|
|
31
|
+
*/
|
|
32
|
+
function trimSlashes(s) {
|
|
33
|
+
return s.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
function createAddonDataPlaneFacility(args) {
|
|
36
|
+
const bindHost = args.bindHost ?? "127.0.0.1";
|
|
37
|
+
const secret = randomBytes(24).toString("hex");
|
|
38
|
+
const served = /* @__PURE__ */ new Map();
|
|
39
|
+
let server = null;
|
|
40
|
+
let baseUrl = "";
|
|
41
|
+
const route = (req, res) => {
|
|
42
|
+
if (req.headers[DATAPLANE_SECRET_HEADER] !== secret) {
|
|
43
|
+
res.writeHead(403).end();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const normalized = `/${trimSlashes((req.url ?? "/").split("?")[0] ?? "/")}`;
|
|
47
|
+
let best = null;
|
|
48
|
+
for (const [prefix, entry] of served) {
|
|
49
|
+
const p = `/${prefix}`;
|
|
50
|
+
if (normalized === p || normalized.startsWith(`${p}/`)) {
|
|
51
|
+
if (best === null || p.length > `/${best.prefix}`.length) best = {
|
|
52
|
+
prefix,
|
|
53
|
+
entry,
|
|
54
|
+
rest: normalized.slice(p.length) || "/"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (best === null) {
|
|
59
|
+
res.writeHead(404).end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const original = req.url ?? "";
|
|
63
|
+
const qIdx = original.indexOf("?");
|
|
64
|
+
req.url = best.rest + (qIdx >= 0 ? original.slice(qIdx) : "");
|
|
65
|
+
Promise.resolve(best.entry.handler(req, res)).catch((err) => {
|
|
66
|
+
args.logger.warn("data-plane handler threw", { meta: {
|
|
67
|
+
prefix: best.prefix,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err)
|
|
69
|
+
} });
|
|
70
|
+
if (!res.headersSent) res.writeHead(500).end();
|
|
71
|
+
else res.end();
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const ensureServer = async () => {
|
|
75
|
+
if (server) return;
|
|
76
|
+
const s = createServer((req, res) => route(req, res));
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
s.once("error", reject);
|
|
79
|
+
s.listen(0, bindHost, () => resolve());
|
|
80
|
+
});
|
|
81
|
+
const addr = s.address();
|
|
82
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
83
|
+
server = s;
|
|
84
|
+
baseUrl = `http://${bindHost}:${port}`;
|
|
85
|
+
};
|
|
86
|
+
const publish = () => {
|
|
87
|
+
const endpoints = [...served.entries()].map(([prefix, entry]) => ({
|
|
88
|
+
prefix,
|
|
89
|
+
access: entry.access,
|
|
90
|
+
baseUrl,
|
|
91
|
+
secret
|
|
92
|
+
}));
|
|
93
|
+
args.sink?.set(args.addonId, endpoints);
|
|
94
|
+
};
|
|
95
|
+
const dataPlane = { serve: async (options) => {
|
|
96
|
+
const prefix = trimSlashes(options.prefix);
|
|
97
|
+
if (prefix.length === 0) throw new Error("dataPlane.serve: a non-empty prefix is required");
|
|
98
|
+
await ensureServer();
|
|
99
|
+
served.set(prefix, {
|
|
100
|
+
access: options.access,
|
|
101
|
+
handler: options.handler
|
|
102
|
+
});
|
|
103
|
+
publish();
|
|
104
|
+
args.logger.info("data-plane endpoint served", { meta: {
|
|
105
|
+
prefix,
|
|
106
|
+
access: options.access,
|
|
107
|
+
baseUrl
|
|
108
|
+
} });
|
|
109
|
+
return {
|
|
110
|
+
baseUrl,
|
|
111
|
+
prefix,
|
|
112
|
+
dispose: async () => {
|
|
113
|
+
served.delete(prefix);
|
|
114
|
+
publish();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
} };
|
|
118
|
+
const dispose = async () => {
|
|
119
|
+
served.clear();
|
|
120
|
+
args.sink?.set(args.addonId, []);
|
|
121
|
+
if (server) {
|
|
122
|
+
const s = server;
|
|
123
|
+
server = null;
|
|
124
|
+
await new Promise((resolve) => s.close(() => resolve()));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
dataPlane,
|
|
129
|
+
dispose
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
17
133
|
//#region src/kernel/addon-class-resolver.ts
|
|
18
134
|
function isRecord(value) {
|
|
19
135
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -54,9 +170,147 @@ function resolveAddonClass(mod) {
|
|
|
54
170
|
if (namedAddon) return namedAddon;
|
|
55
171
|
}
|
|
56
172
|
//#endregion
|
|
173
|
+
//#region src/kernel/deps/npm-command.ts
|
|
174
|
+
/**
|
|
175
|
+
* npm invocation resolver — makes "is npm available?" a solved problem for
|
|
176
|
+
* every installer code path that shells out to npm.
|
|
177
|
+
*
|
|
178
|
+
* Why this exists (Mac mini incident, silent for days until 2026-07-16): the
|
|
179
|
+
* packaged Electron agent ships a BARE node binary — `Resources/node/bin/node`
|
|
180
|
+
* with no npm — so every `execFileAsync('npm', ...)` died with ENOENT inside a
|
|
181
|
+
* "non-fatal" catch. Addons with runtime deps installed WITHOUT their
|
|
182
|
+
* node_modules, reported "running", and failed at first import
|
|
183
|
+
* (`Cannot find module 'sharp'`): the detection group ran 1/3 members and the
|
|
184
|
+
* node silently fell out of the balancer pool.
|
|
185
|
+
*
|
|
186
|
+
* Resolution order:
|
|
187
|
+
* 1. system `npm` on PATH (probed once per process) — hub image, dev
|
|
188
|
+
* machines, Linux agent containers;
|
|
189
|
+
* 2. a previously-bootstrapped copy under `cacheDir` — run its `npm-cli.js`
|
|
190
|
+
* with the CURRENT node binary (`process.execPath`), which always exists;
|
|
191
|
+
* 3. BOOTSTRAP: fetch `<registry>/npm/latest` (the npm package is fully
|
|
192
|
+
* self-contained — its tarball bundles every dependency), download +
|
|
193
|
+
* extract into `cacheDir`, then use it as in (2).
|
|
194
|
+
*
|
|
195
|
+
* Failures THROW with an actionable message — a caller must never proceed as
|
|
196
|
+
* if dependencies were installed when they were not.
|
|
197
|
+
*/
|
|
198
|
+
var execFileAsync$1 = promisify(execFile);
|
|
199
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
200
|
+
/** Where the bootstrap lands inside `cacheDir` (npm tgz carries a `package/` prefix). */
|
|
201
|
+
function bootstrappedCliPath(cacheDir) {
|
|
202
|
+
return path$1.join(cacheDir, "package", "bin", "npm-cli.js");
|
|
203
|
+
}
|
|
204
|
+
async function defaultProbeSystemNpm() {
|
|
205
|
+
try {
|
|
206
|
+
await execFileAsync$1("npm", ["--version"], { timeout: 15e3 });
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function defaultFetchJson(url) {
|
|
213
|
+
const res = await fetch(url);
|
|
214
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
215
|
+
return res.json();
|
|
216
|
+
}
|
|
217
|
+
async function defaultDownloadTarball(url, destTgz) {
|
|
218
|
+
const res = await fetch(url);
|
|
219
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status} downloading ${url}`);
|
|
220
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
221
|
+
await fs.promises.writeFile(destTgz, bytes);
|
|
222
|
+
}
|
|
223
|
+
async function defaultExtractTarball(tgzPath, destDir) {
|
|
224
|
+
await execFileAsync$1("tar", [
|
|
225
|
+
"-xzf",
|
|
226
|
+
tgzPath,
|
|
227
|
+
"-C",
|
|
228
|
+
destDir
|
|
229
|
+
], { timeout: 12e4 });
|
|
230
|
+
}
|
|
231
|
+
/** The `dist.tarball` URL out of a registry version-metadata document. */
|
|
232
|
+
function tarballUrlOf(metadata) {
|
|
233
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
234
|
+
const dist = metadata["dist"];
|
|
235
|
+
if (typeof dist !== "object" || dist === null) return null;
|
|
236
|
+
const tarball = dist["tarball"];
|
|
237
|
+
return typeof tarball === "string" && tarball.length > 0 ? tarball : null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Process-wide memo: resolution is stable for the process lifetime (npm does
|
|
241
|
+
* not appear/disappear mid-run, and the bootstrap is idempotent on disk).
|
|
242
|
+
*/
|
|
243
|
+
var cachedInvocation = null;
|
|
244
|
+
async function resolveNpmInvocation(options) {
|
|
245
|
+
cachedInvocation ??= resolveUncached(options).catch((err) => {
|
|
246
|
+
cachedInvocation = null;
|
|
247
|
+
throw err;
|
|
248
|
+
});
|
|
249
|
+
return cachedInvocation;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Resolve a working npm (system → bootstrapped → bootstrap) and run it with
|
|
253
|
+
* `args`. The ONE entry point every installer code path uses instead of a raw
|
|
254
|
+
* `execFile('npm', ...)` — so "npm missing on this host" is either solved
|
|
255
|
+
* (bootstrap) or a LOUD, actionable error, never a swallowed ENOENT.
|
|
256
|
+
*/
|
|
257
|
+
async function runNpm(args, options) {
|
|
258
|
+
const invocation = await resolveNpmInvocation({
|
|
259
|
+
cacheDir: options.cacheDir,
|
|
260
|
+
registry: options.registry,
|
|
261
|
+
logger: options.logger
|
|
262
|
+
});
|
|
263
|
+
return execFileAsync$1(invocation.command, [...invocation.argsPrefix, ...args], {
|
|
264
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
265
|
+
timeout: options.timeout ?? 3e5
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function resolveUncached(options) {
|
|
269
|
+
if (await (options.probeSystemNpm ?? defaultProbeSystemNpm)()) return {
|
|
270
|
+
command: "npm",
|
|
271
|
+
argsPrefix: []
|
|
272
|
+
};
|
|
273
|
+
const cliPath = bootstrappedCliPath(options.cacheDir);
|
|
274
|
+
if (fs.existsSync(cliPath)) {
|
|
275
|
+
options.logger.info("system npm unavailable — using bootstrapped npm", { meta: { cliPath } });
|
|
276
|
+
return {
|
|
277
|
+
command: process.execPath,
|
|
278
|
+
argsPrefix: [cliPath]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
options.logger.warn("system npm unavailable — bootstrapping npm from the registry", { meta: { cacheDir: options.cacheDir } });
|
|
282
|
+
try {
|
|
283
|
+
const registry = (options.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
284
|
+
const tarballUrl = tarballUrlOf(await (options.fetchJson ?? defaultFetchJson)(`${registry}/npm/latest`));
|
|
285
|
+
if (!tarballUrl) throw new Error(`registry metadata for 'npm' carries no dist.tarball (${registry})`);
|
|
286
|
+
await fs.promises.mkdir(options.cacheDir, { recursive: true });
|
|
287
|
+
const tgzPath = path$1.join(options.cacheDir, "npm-bootstrap.tgz");
|
|
288
|
+
await (options.downloadTarball ?? defaultDownloadTarball)(tarballUrl, tgzPath);
|
|
289
|
+
await (options.extractTarball ?? defaultExtractTarball)(tgzPath, options.cacheDir);
|
|
290
|
+
await fs.promises.rm(tgzPath, { force: true });
|
|
291
|
+
if (!fs.existsSync(cliPath)) throw new Error(`extracted npm tarball is missing ${cliPath}`);
|
|
292
|
+
options.logger.info("npm bootstrapped", { meta: { cliPath } });
|
|
293
|
+
return {
|
|
294
|
+
command: process.execPath,
|
|
295
|
+
argsPrefix: [cliPath]
|
|
296
|
+
};
|
|
297
|
+
} catch (err) {
|
|
298
|
+
throw new Error(`no system npm on PATH and the npm bootstrap failed — cannot install addon dependencies on this node: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
57
302
|
//#region src/kernel/deps/manifest-native-deps.ts
|
|
58
303
|
var execFileAsync = promisify(execFile);
|
|
59
304
|
/**
|
|
305
|
+
* Fallback bootstrap-cache location when the caller doesn't thread one
|
|
306
|
+
* through (the forked addon-runner's spawn-time install). tmpdir survives the
|
|
307
|
+
* process; a reboot just re-bootstraps. Callers with a persistent root (the
|
|
308
|
+
* installer) pass their own.
|
|
309
|
+
*/
|
|
310
|
+
function defaultNpmCacheDir() {
|
|
311
|
+
return path$1.join(os.tmpdir(), "camstack-npm-bootstrap");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
60
314
|
* Native node modules an addon needs at runtime but cannot be bundled
|
|
61
315
|
* (`.node` binary files require ABI-matched compilation). Mirror of the
|
|
62
316
|
* Python `requirements.txt` pattern in `manifest-python-deps.ts` —
|
|
@@ -79,7 +333,7 @@ var execFileAsync = promisify(execFile);
|
|
|
79
333
|
* ABI; surface a clear error at first `import` of the native module
|
|
80
334
|
* otherwise).
|
|
81
335
|
*/
|
|
82
|
-
async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry) {
|
|
336
|
+
async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry, npmCacheDir) {
|
|
83
337
|
const native = readNativeDeps(pkgRaw);
|
|
84
338
|
if (native == null || Object.keys(native).length === 0) return;
|
|
85
339
|
const markerFile = path$1.join(addonDir, ".camstack-native-deps-installed");
|
|
@@ -124,14 +378,17 @@ async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry) {
|
|
|
124
378
|
...specs
|
|
125
379
|
];
|
|
126
380
|
try {
|
|
127
|
-
await
|
|
381
|
+
await runNpm(args, {
|
|
382
|
+
cacheDir: npmCacheDir ?? defaultNpmCacheDir(),
|
|
383
|
+
registry,
|
|
384
|
+
logger,
|
|
128
385
|
cwd: addonDir,
|
|
129
386
|
timeout: 3e5
|
|
130
387
|
});
|
|
131
388
|
} catch (err) {
|
|
132
389
|
throw new Error(`npm install of native deps failed for ${addonDir}: ${errMsg(err)}`, { cause: err });
|
|
133
390
|
}
|
|
134
|
-
await rebuildNativeDeps(addonDir, pending.map(([name]) => name), logger);
|
|
391
|
+
await rebuildNativeDeps(addonDir, pending.map(([name]) => name), logger, npmCacheDir ?? defaultNpmCacheDir(), registry);
|
|
135
392
|
try {
|
|
136
393
|
fs.writeFileSync(markerFile, markerHash);
|
|
137
394
|
} catch (err) {
|
|
@@ -247,7 +504,7 @@ function markerMatches(markerFile, expected) {
|
|
|
247
504
|
* The first `import` of an actually-mismatched binary throws a clear
|
|
248
505
|
* error that points the operator at this rebuild step.
|
|
249
506
|
*/
|
|
250
|
-
async function rebuildNativeDeps(addonDir, packageNames, logger) {
|
|
507
|
+
async function rebuildNativeDeps(addonDir, packageNames, logger, npmCacheDir, registry) {
|
|
251
508
|
if (typeof process.versions.electron === "string") {
|
|
252
509
|
const electronVersion = process.versions.electron;
|
|
253
510
|
logger.info("Rebuilding native deps for Electron", { meta: {
|
|
@@ -291,7 +548,10 @@ async function rebuildNativeDeps(addonDir, packageNames, logger) {
|
|
|
291
548
|
packages: packageNames
|
|
292
549
|
} });
|
|
293
550
|
try {
|
|
294
|
-
await
|
|
551
|
+
await runNpm(["rebuild", ...packageNames], {
|
|
552
|
+
cacheDir: npmCacheDir,
|
|
553
|
+
registry,
|
|
554
|
+
logger,
|
|
295
555
|
cwd: addonDir,
|
|
296
556
|
timeout: 6e5
|
|
297
557
|
});
|
|
@@ -5583,6 +5843,30 @@ function safeExistsSync(path) {
|
|
|
5583
5843
|
return false;
|
|
5584
5844
|
}
|
|
5585
5845
|
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Detect a Coral USB Edge TPU: a matching USB device (`1a6e:089a` pre-init or
|
|
5848
|
+
* `18d1:9302` post-init) OR the installed libedgetpu delegate (the hub
|
|
5849
|
+
* provisions it out-of-band; its presence is a reliable "Coral is wired"
|
|
5850
|
+
* signal even where the container's `/sys` USB view is limited).
|
|
5851
|
+
*/
|
|
5852
|
+
function detectCoralUsb() {
|
|
5853
|
+
const CORAL_IDS = [{
|
|
5854
|
+
vendor: "1a6e",
|
|
5855
|
+
product: "089a"
|
|
5856
|
+
}, {
|
|
5857
|
+
vendor: "18d1",
|
|
5858
|
+
product: "9302"
|
|
5859
|
+
}];
|
|
5860
|
+
try {
|
|
5861
|
+
for (const device of fs.readdirSync("/sys/bus/usb/devices")) try {
|
|
5862
|
+
const base = `/sys/bus/usb/devices/${device}`;
|
|
5863
|
+
const vendor = fs.readFileSync(`${base}/idVendor`, "utf8").trim().toLowerCase();
|
|
5864
|
+
const product = fs.readFileSync(`${base}/idProduct`, "utf8").trim().toLowerCase();
|
|
5865
|
+
if (CORAL_IDS.some((id) => id.vendor === vendor && id.product === product)) return true;
|
|
5866
|
+
} catch {}
|
|
5867
|
+
} catch {}
|
|
5868
|
+
return safeExistsSync(process.env["CAMSTACK_EDGETPU_LIB"] ?? "/data/deps/edgetpu/lib/libedgetpu.so.1");
|
|
5869
|
+
}
|
|
5586
5870
|
/** Probe Linux GPU vendors by parsing /proc/bus/pci/devices or /sys/class/drm. */
|
|
5587
5871
|
function probeLinuxGpus() {
|
|
5588
5872
|
const signals = {
|
|
@@ -5590,7 +5874,8 @@ function probeLinuxGpus() {
|
|
|
5590
5874
|
hasAmdGpu: false,
|
|
5591
5875
|
hasIntelGpu: false,
|
|
5592
5876
|
hasRenderNode: false,
|
|
5593
|
-
hasIntelNpu: safeExistsSync("/dev/accel/accel0")
|
|
5877
|
+
hasIntelNpu: safeExistsSync("/dev/accel/accel0"),
|
|
5878
|
+
hasCoral: detectCoralUsb()
|
|
5594
5879
|
};
|
|
5595
5880
|
signals.hasRenderNode = safeExistsSync("/dev/dri/renderD128");
|
|
5596
5881
|
if (safeExistsSync("/dev/nvidia0") || safeExistsSync("/proc/driver/nvidia/version")) signals.hasNvidiaGpu = true;
|
|
@@ -5623,7 +5908,8 @@ function probePlatform() {
|
|
|
5623
5908
|
hasAmdGpu: false,
|
|
5624
5909
|
hasIntelGpu: false,
|
|
5625
5910
|
hasRenderNode: false,
|
|
5626
|
-
hasIntelNpu: false
|
|
5911
|
+
hasIntelNpu: false,
|
|
5912
|
+
hasCoral: safeExistsSync(process.env["CAMSTACK_EDGETPU_LIB"] ?? "/data/deps/edgetpu/lib/libedgetpu.so.1")
|
|
5627
5913
|
};
|
|
5628
5914
|
}
|
|
5629
5915
|
function pickBackendsForPlatform(signals) {
|
|
@@ -5708,7 +5994,8 @@ function createKernelHwAccel() {
|
|
|
5708
5994
|
function mapSignalsToInferenceHardware(signals) {
|
|
5709
5995
|
return {
|
|
5710
5996
|
gpu: signals.hasNvidiaGpu ? { type: "nvidia" } : signals.hasIntelGpu || signals.hasRenderNode ? { type: "intel" } : null,
|
|
5711
|
-
npu: signals.platform === "darwin" && signals.arch === "arm64" ? { type: "apple-ane" } : signals.hasIntelNpu ? { type: "intel-npu" } : null
|
|
5997
|
+
npu: signals.platform === "darwin" && signals.arch === "arm64" ? { type: "apple-ane" } : signals.hasIntelNpu ? { type: "intel-npu" } : null,
|
|
5998
|
+
coral: signals.hasCoral ? { type: "coral-edgetpu" } : null
|
|
5712
5999
|
};
|
|
5713
6000
|
}
|
|
5714
6001
|
/**
|
|
@@ -5720,122 +6007,6 @@ function createKernelInferenceEngine() {
|
|
|
5720
6007
|
return { resolveHardware: async () => mapSignalsToInferenceHardware(probePlatform()) };
|
|
5721
6008
|
}
|
|
5722
6009
|
//#endregion
|
|
5723
|
-
//#region src/kernel/moleculer/addon-data-plane-facility.ts
|
|
5724
|
-
/**
|
|
5725
|
-
* Kernel side of the addon HTTP **data-plane** (see {@link AddonDataPlane}).
|
|
5726
|
-
*
|
|
5727
|
-
* Each addon context gets one facility. On the first `serve()`, it binds a
|
|
5728
|
-
* `127.0.0.1` HTTP listener IN THE ADDON'S PROCESS and multiplexes registered
|
|
5729
|
-
* prefixes → the addon's real `(req, res)` handlers (longest-prefix wins). Every
|
|
5730
|
-
* request must carry the per-listener shared secret (injected by the hub's
|
|
5731
|
-
* reverse-proxy) — anything else gets 403, so no other local process can reach
|
|
5732
|
-
* the listener. The facility reports its live endpoints through a `sink` so the
|
|
5733
|
-
* runner can hand them to the hub (which pulls them when it mounts the addon).
|
|
5734
|
-
*
|
|
5735
|
-
* The addon never sees the secret, the port, or any transport detail — it just
|
|
5736
|
-
* streams to `res`. The hub authenticates and reverse-proxies in front.
|
|
5737
|
-
*/
|
|
5738
|
-
function trimSlashes(s) {
|
|
5739
|
-
return s.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
5740
|
-
}
|
|
5741
|
-
function createAddonDataPlaneFacility(args) {
|
|
5742
|
-
const bindHost = args.bindHost ?? "127.0.0.1";
|
|
5743
|
-
const secret = randomBytes(24).toString("hex");
|
|
5744
|
-
const served = /* @__PURE__ */ new Map();
|
|
5745
|
-
let server = null;
|
|
5746
|
-
let baseUrl = "";
|
|
5747
|
-
const route = (req, res) => {
|
|
5748
|
-
if (req.headers[DATAPLANE_SECRET_HEADER] !== secret) {
|
|
5749
|
-
res.writeHead(403).end();
|
|
5750
|
-
return;
|
|
5751
|
-
}
|
|
5752
|
-
const normalized = `/${trimSlashes((req.url ?? "/").split("?")[0] ?? "/")}`;
|
|
5753
|
-
let best = null;
|
|
5754
|
-
for (const [prefix, entry] of served) {
|
|
5755
|
-
const p = `/${prefix}`;
|
|
5756
|
-
if (normalized === p || normalized.startsWith(`${p}/`)) {
|
|
5757
|
-
if (best === null || p.length > `/${best.prefix}`.length) best = {
|
|
5758
|
-
prefix,
|
|
5759
|
-
entry,
|
|
5760
|
-
rest: normalized.slice(p.length) || "/"
|
|
5761
|
-
};
|
|
5762
|
-
}
|
|
5763
|
-
}
|
|
5764
|
-
if (best === null) {
|
|
5765
|
-
res.writeHead(404).end();
|
|
5766
|
-
return;
|
|
5767
|
-
}
|
|
5768
|
-
const original = req.url ?? "";
|
|
5769
|
-
const qIdx = original.indexOf("?");
|
|
5770
|
-
req.url = best.rest + (qIdx >= 0 ? original.slice(qIdx) : "");
|
|
5771
|
-
Promise.resolve(best.entry.handler(req, res)).catch((err) => {
|
|
5772
|
-
args.logger.warn("data-plane handler threw", { meta: {
|
|
5773
|
-
prefix: best.prefix,
|
|
5774
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5775
|
-
} });
|
|
5776
|
-
if (!res.headersSent) res.writeHead(500).end();
|
|
5777
|
-
else res.end();
|
|
5778
|
-
});
|
|
5779
|
-
};
|
|
5780
|
-
const ensureServer = async () => {
|
|
5781
|
-
if (server) return;
|
|
5782
|
-
const s = createServer((req, res) => route(req, res));
|
|
5783
|
-
await new Promise((resolve, reject) => {
|
|
5784
|
-
s.once("error", reject);
|
|
5785
|
-
s.listen(0, bindHost, () => resolve());
|
|
5786
|
-
});
|
|
5787
|
-
const addr = s.address();
|
|
5788
|
-
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
5789
|
-
server = s;
|
|
5790
|
-
baseUrl = `http://${bindHost}:${port}`;
|
|
5791
|
-
};
|
|
5792
|
-
const publish = () => {
|
|
5793
|
-
const endpoints = [...served.entries()].map(([prefix, entry]) => ({
|
|
5794
|
-
prefix,
|
|
5795
|
-
access: entry.access,
|
|
5796
|
-
baseUrl,
|
|
5797
|
-
secret
|
|
5798
|
-
}));
|
|
5799
|
-
args.sink?.set(args.addonId, endpoints);
|
|
5800
|
-
};
|
|
5801
|
-
const dataPlane = { serve: async (options) => {
|
|
5802
|
-
const prefix = trimSlashes(options.prefix);
|
|
5803
|
-
if (prefix.length === 0) throw new Error("dataPlane.serve: a non-empty prefix is required");
|
|
5804
|
-
await ensureServer();
|
|
5805
|
-
served.set(prefix, {
|
|
5806
|
-
access: options.access,
|
|
5807
|
-
handler: options.handler
|
|
5808
|
-
});
|
|
5809
|
-
publish();
|
|
5810
|
-
args.logger.info("data-plane endpoint served", { meta: {
|
|
5811
|
-
prefix,
|
|
5812
|
-
access: options.access,
|
|
5813
|
-
baseUrl
|
|
5814
|
-
} });
|
|
5815
|
-
return {
|
|
5816
|
-
baseUrl,
|
|
5817
|
-
prefix,
|
|
5818
|
-
dispose: async () => {
|
|
5819
|
-
served.delete(prefix);
|
|
5820
|
-
publish();
|
|
5821
|
-
}
|
|
5822
|
-
};
|
|
5823
|
-
} };
|
|
5824
|
-
const dispose = async () => {
|
|
5825
|
-
served.clear();
|
|
5826
|
-
args.sink?.set(args.addonId, []);
|
|
5827
|
-
if (server) {
|
|
5828
|
-
const s = server;
|
|
5829
|
-
server = null;
|
|
5830
|
-
await new Promise((resolve) => s.close(() => resolve()));
|
|
5831
|
-
}
|
|
5832
|
-
};
|
|
5833
|
-
return {
|
|
5834
|
-
dataPlane,
|
|
5835
|
-
dispose
|
|
5836
|
-
};
|
|
5837
|
-
}
|
|
5838
|
-
//#endregion
|
|
5839
6010
|
//#region src/kernel/deps/addon-deps-manager.ts
|
|
5840
6011
|
/**
|
|
5841
6012
|
* Canonical implementation of `IAddonDepsManager`, injected into every
|
|
@@ -7123,4 +7294,4 @@ async function installManifestPythonDeps(declaration, addonDir, deps, logger) {
|
|
|
7123
7294
|
await deps.installPythonRequirements(reqAbs);
|
|
7124
7295
|
}
|
|
7125
7296
|
//#endregion
|
|
7126
|
-
export { buildUdsNativeCapProxy as $, createParentUnownedCallHandler as A, AGENT_CAP_FWD_SERVICE as B, buildLinkChain as C, HUB_CAP_FWD_ACTION as D, localProviderLink as E, createUdsLoggerWithControl as F, createLocalTransport as G, CapRouteError as H, LocalChildClient as I, SocketChannel as J, UdsLocalTransportClient as K, LocalChildRegistry as L, createUdsEventBus as M, udsChildLogToWorkerEntry as N, HUB_CAP_FWD_SERVICE as O, createUdsLogger as P, buildNativeCapProxy as Q, UDS_NO_ROUTE_PREFIX as R, brokerTransportLink as S, ipcParentLink as T, classifyCapRoute as U, CapRouteResolver as V, callWithServiceDiscovery as W, FrameDecoder as X, localEndpointPath as Y, encodeFrame as Z, resolveHwAccel as _, CapabilityUnavailableError as _t, getWorkerDeviceRegistry as a, createAddonService as at, getCapUsageRegistry as b, setHubConnected as c, capActionName as ct, getMoleculerEventStats as d, capServiceName as dt, createBrokerDeviceManagerApi as et, registerEventBusService as f, parseCapAction as ft, createKernelHwAccel as g, CapabilityHandle as gt, AddonDepsManager as h, DeviceRegistry as ht, createUdsAddonContext as i, setWorkerNativeCapsChangeListener as it, createUdsEventBridge as j, createHubCapForwardService as k, EVENT_TOPIC_PREFIX as l, capActionSuffix as lt, subscribePassthrough as m, serializeTypedArrays as mt, adaptBrokerToCluster as n, getWorkerNativeCapSnapshot as nt, getOrInitReadinessRegistry as o, validateProviderRegistrations as ot, setNodeEventInterest as p, deserializeTypedArrays as pt, UdsLocalTransportServer as q, createAddonContext as r, mountNativeCapService as rt, getOrInitReadinessRegistryForClient as s, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestPythonDeps as t, getWorkerNativeCapProvider as tt, getBrokerEventBus as u, capBareAction as ut, CapUsageRegistry as v, installManifestNativeDeps as vt, ipcChildLink as w, brokerCallForCap as x, __resetCapUsageRegistryForTests as y,
|
|
7297
|
+
export { buildUdsNativeCapProxy as $, createParentUnownedCallHandler as A, AGENT_CAP_FWD_SERVICE as B, buildLinkChain as C, HUB_CAP_FWD_ACTION as D, localProviderLink as E, createUdsLoggerWithControl as F, createLocalTransport as G, CapRouteError as H, LocalChildClient as I, SocketChannel as J, UdsLocalTransportClient as K, LocalChildRegistry as L, createUdsEventBus as M, udsChildLogToWorkerEntry as N, HUB_CAP_FWD_SERVICE as O, createUdsLogger as P, buildNativeCapProxy as Q, UDS_NO_ROUTE_PREFIX as R, brokerTransportLink as S, ipcParentLink as T, classifyCapRoute as U, CapRouteResolver as V, callWithServiceDiscovery as W, FrameDecoder as X, localEndpointPath as Y, encodeFrame as Z, resolveHwAccel as _, CapabilityUnavailableError as _t, getWorkerDeviceRegistry as a, createAddonService as at, getCapUsageRegistry as b, resolveAddonClass as bt, setHubConnected as c, capActionName as ct, getMoleculerEventStats as d, capServiceName as dt, createBrokerDeviceManagerApi as et, registerEventBusService as f, parseCapAction as ft, createKernelHwAccel as g, CapabilityHandle as gt, AddonDepsManager as h, DeviceRegistry as ht, createUdsAddonContext as i, setWorkerNativeCapsChangeListener as it, createUdsEventBridge as j, createHubCapForwardService as k, EVENT_TOPIC_PREFIX as l, capActionSuffix as lt, subscribePassthrough as m, serializeTypedArrays as mt, adaptBrokerToCluster as n, getWorkerNativeCapSnapshot as nt, getOrInitReadinessRegistry as o, validateProviderRegistrations as ot, setNodeEventInterest as p, deserializeTypedArrays as pt, UdsLocalTransportServer as q, createAddonContext as r, mountNativeCapService as rt, getOrInitReadinessRegistryForClient as s, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestPythonDeps as t, getWorkerNativeCapProvider as tt, getBrokerEventBus as u, capBareAction as ut, CapUsageRegistry as v, installManifestNativeDeps as vt, ipcChildLink as w, brokerCallForCap as x, createAddonDataPlaneFacility as xt, __resetCapUsageRegistryForTests as y, runNpm as yt, AGENT_CAP_FWD_ACTION as z };
|