@camstack/system 1.0.7 → 1.1.0
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 +3 -3
- package/dist/addon-runner.mjs +3 -3
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +8 -8
- package/dist/builtins/device-manager/device-manager.addon.mjs +8 -8
- package/dist/builtins/platform-probe/index.js +27 -139
- package/dist/builtins/platform-probe/index.mjs +28 -140
- package/dist/builtins/platform-probe/platform-scorer.d.ts +17 -10
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
- package/dist/index.js +153 -78
- package/dist/index.mjs +153 -79
- package/dist/kernel/addon-installer.d.ts +20 -0
- package/dist/kernel/config-manager.d.ts +4 -4
- package/dist/kernel/fs-utils.d.ts +16 -6
- package/dist/kernel/index.d.ts +2 -1
- package/dist/kernel/moleculer/addon-deploy-source.d.ts +16 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +10 -0
- package/dist/{manifest-python-deps-eBDj5HEY.js → manifest-python-deps-BWURo7dc.js} +34 -17
- package/dist/{manifest-python-deps-CoJXeb9u.mjs → manifest-python-deps-BcrTzHH_.mjs} +54 -37
- package/dist/{model-download-service-JtVQtbb6.js → model-download-service-1eEOkNeS.js} +35 -5
- package/dist/{model-download-service-C7AjBsX9.mjs → model-download-service-RxAOiYvX.mjs} +35 -5
- 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-BWURo7dc.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
node_fs = require_chunk.__toESM(node_fs);
|
|
@@ -170,10 +170,10 @@ function toSerializableRoute(route) {
|
|
|
170
170
|
*/
|
|
171
171
|
async function dispatchSettings(addon, addonId, method, args) {
|
|
172
172
|
switch (method) {
|
|
173
|
-
case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap) ?? null : null;
|
|
173
|
+
case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap, args.nodeId) ?? null : null;
|
|
174
174
|
case "updateGlobalSettings":
|
|
175
175
|
if (typeof addon.updateGlobalSettings !== "function") throw new Error(`child-addon-call: addon "${addonId}" does not implement updateGlobalSettings`);
|
|
176
|
-
await addon.updateGlobalSettings(args.patch ?? {});
|
|
176
|
+
await addon.updateGlobalSettings(args.patch ?? {}, args.nodeId);
|
|
177
177
|
return { success: true };
|
|
178
178
|
case "getDeviceSettings": return typeof addon.getDeviceSettings === "function" && typeof args.deviceId === "number" ? await addon.getDeviceSettings(args.deviceId) ?? null : null;
|
|
179
179
|
case "updateDeviceSettings":
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, L as LocalChildClient, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-
|
|
1
|
+
import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, L as LocalChildClient, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-BcrTzHH_.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -166,10 +166,10 @@ function toSerializableRoute(route) {
|
|
|
166
166
|
*/
|
|
167
167
|
async function dispatchSettings(addon, addonId, method, args) {
|
|
168
168
|
switch (method) {
|
|
169
|
-
case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap) ?? null : null;
|
|
169
|
+
case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap, args.nodeId) ?? null : null;
|
|
170
170
|
case "updateGlobalSettings":
|
|
171
171
|
if (typeof addon.updateGlobalSettings !== "function") throw new Error(`child-addon-call: addon "${addonId}" does not implement updateGlobalSettings`);
|
|
172
|
-
await addon.updateGlobalSettings(args.patch ?? {});
|
|
172
|
+
await addon.updateGlobalSettings(args.patch ?? {}, args.nodeId);
|
|
173
173
|
return { success: true };
|
|
174
174
|
case "getDeviceSettings": return typeof addon.getDeviceSettings === "function" && typeof args.deviceId === "number" ? await addon.getDeviceSettings(args.deviceId) ?? null : null;
|
|
175
175
|
case "updateDeviceSettings":
|
package/dist/addon-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("./chunk-Cek0wNdY.js");
|
|
3
|
-
const require_model_download_service = require("./model-download-service-
|
|
3
|
+
const require_model_download_service = require("./model-download-service-1eEOkNeS.js");
|
|
4
4
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
5
5
|
exports.CustomActionRegistry = require_custom_action_registry.CustomActionRegistry;
|
|
6
6
|
exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
|
package/dist/addon-utils.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-
|
|
1
|
+
import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-RxAOiYvX.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
3
3
|
export { CustomActionRegistry, ModelDownloadService, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, isModelDownloaded };
|
|
@@ -1638,8 +1638,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
1638
1638
|
if (!persisted) return null;
|
|
1639
1639
|
const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
|
|
1640
1640
|
if (links.length === 0) return null;
|
|
1641
|
-
const
|
|
1642
|
-
const schema =
|
|
1641
|
+
const capRegistry = this.capabilityRegistry;
|
|
1642
|
+
const schema = capRegistry?.getDefinition(cap)?.status?.schema;
|
|
1643
1643
|
if (!schema) return null;
|
|
1644
1644
|
const allMeta = await readMeta();
|
|
1645
1645
|
let containerStableId = persisted.stableId;
|
|
@@ -1655,7 +1655,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
1655
1655
|
const srcId = resolveSourceDeviceId(containerStableId, link.source.sourceKey, allMeta);
|
|
1656
1656
|
if (srcId === null) continue;
|
|
1657
1657
|
try {
|
|
1658
|
-
const srcProvider =
|
|
1658
|
+
const srcProvider = capRegistry?.getProviderForDevice(link.source.cap, srcId);
|
|
1659
1659
|
const raw = (0, _camstack_types.getByPath)(typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0, link.source.fieldPath);
|
|
1660
1660
|
resolved.push({
|
|
1661
1661
|
link,
|
|
@@ -1840,7 +1840,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
1840
1840
|
this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
1841
1841
|
idToAddonId.delete(deviceId);
|
|
1842
1842
|
this.devicesWithLinks.delete(deviceId);
|
|
1843
|
-
for (const
|
|
1843
|
+
for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
|
|
1844
1844
|
await rebuildLinkDependents();
|
|
1845
1845
|
this.ctx.logger.info("removed device", { tags: {
|
|
1846
1846
|
deviceId,
|
|
@@ -2386,7 +2386,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
2386
2386
|
listLocations: async () => {
|
|
2387
2387
|
const store = await settings.readAddonStore();
|
|
2388
2388
|
const meta = store.deviceMeta ?? {};
|
|
2389
|
-
const
|
|
2389
|
+
const locations = store.locations ?? [];
|
|
2390
2390
|
const seen = /* @__PURE__ */ new Map();
|
|
2391
2391
|
const consider = (raw) => {
|
|
2392
2392
|
if (typeof raw !== "string") return;
|
|
@@ -2395,7 +2395,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
2395
2395
|
const key = trimmed.toLowerCase();
|
|
2396
2396
|
if (!seen.has(key)) seen.set(key, trimmed);
|
|
2397
2397
|
};
|
|
2398
|
-
for (const label of
|
|
2398
|
+
for (const label of locations) consider(label);
|
|
2399
2399
|
for (const m of Object.values(meta)) consider(m.location);
|
|
2400
2400
|
return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
2401
2401
|
},
|
|
@@ -2960,9 +2960,9 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
|
|
|
2960
2960
|
return this.getDeviceAggregate(input.deviceId, "live");
|
|
2961
2961
|
},
|
|
2962
2962
|
getDeviceAggregate: async (input) => {
|
|
2963
|
-
const [
|
|
2963
|
+
const [settingsAggregate, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
|
|
2964
2964
|
return {
|
|
2965
|
-
settings,
|
|
2965
|
+
settings: settingsAggregate,
|
|
2966
2966
|
live
|
|
2967
2967
|
};
|
|
2968
2968
|
},
|
|
@@ -1633,8 +1633,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1633
1633
|
if (!persisted) return null;
|
|
1634
1634
|
const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
|
|
1635
1635
|
if (links.length === 0) return null;
|
|
1636
|
-
const
|
|
1637
|
-
const schema =
|
|
1636
|
+
const capRegistry = this.capabilityRegistry;
|
|
1637
|
+
const schema = capRegistry?.getDefinition(cap)?.status?.schema;
|
|
1638
1638
|
if (!schema) return null;
|
|
1639
1639
|
const allMeta = await readMeta();
|
|
1640
1640
|
let containerStableId = persisted.stableId;
|
|
@@ -1650,7 +1650,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1650
1650
|
const srcId = resolveSourceDeviceId(containerStableId, link.source.sourceKey, allMeta);
|
|
1651
1651
|
if (srcId === null) continue;
|
|
1652
1652
|
try {
|
|
1653
|
-
const srcProvider =
|
|
1653
|
+
const srcProvider = capRegistry?.getProviderForDevice(link.source.cap, srcId);
|
|
1654
1654
|
const raw = getByPath(typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0, link.source.fieldPath);
|
|
1655
1655
|
resolved.push({
|
|
1656
1656
|
link,
|
|
@@ -1835,7 +1835,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
1835
1835
|
this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
1836
1836
|
idToAddonId.delete(deviceId);
|
|
1837
1837
|
this.devicesWithLinks.delete(deviceId);
|
|
1838
|
-
for (const
|
|
1838
|
+
for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
|
|
1839
1839
|
await rebuildLinkDependents();
|
|
1840
1840
|
this.ctx.logger.info("removed device", { tags: {
|
|
1841
1841
|
deviceId,
|
|
@@ -2381,7 +2381,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
2381
2381
|
listLocations: async () => {
|
|
2382
2382
|
const store = await settings.readAddonStore();
|
|
2383
2383
|
const meta = store.deviceMeta ?? {};
|
|
2384
|
-
const
|
|
2384
|
+
const locations = store.locations ?? [];
|
|
2385
2385
|
const seen = /* @__PURE__ */ new Map();
|
|
2386
2386
|
const consider = (raw) => {
|
|
2387
2387
|
if (typeof raw !== "string") return;
|
|
@@ -2390,7 +2390,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
2390
2390
|
const key = trimmed.toLowerCase();
|
|
2391
2391
|
if (!seen.has(key)) seen.set(key, trimmed);
|
|
2392
2392
|
};
|
|
2393
|
-
for (const label of
|
|
2393
|
+
for (const label of locations) consider(label);
|
|
2394
2394
|
for (const m of Object.values(meta)) consider(m.location);
|
|
2395
2395
|
return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
2396
2396
|
},
|
|
@@ -2955,9 +2955,9 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
|
|
|
2955
2955
|
return this.getDeviceAggregate(input.deviceId, "live");
|
|
2956
2956
|
},
|
|
2957
2957
|
getDeviceAggregate: async (input) => {
|
|
2958
|
-
const [
|
|
2958
|
+
const [settingsAggregate, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
|
|
2959
2959
|
return {
|
|
2960
|
-
settings,
|
|
2960
|
+
settings: settingsAggregate,
|
|
2961
2961
|
live
|
|
2962
2962
|
};
|
|
2963
2963
|
},
|
|
@@ -60,8 +60,8 @@ async function getAvailableRAM_MB() {
|
|
|
60
60
|
const match = readFileSync("/proc/meminfo", "utf8").match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
61
61
|
if (match) return Math.round(parseInt(match[1]) / 1024);
|
|
62
62
|
}
|
|
63
|
-
} catch (
|
|
64
|
-
console.debug(`RAM probe failed, using total RAM fallback: ${(0, _camstack_types.errMsg)(
|
|
63
|
+
} catch (probeErr) {
|
|
64
|
+
console.debug(`RAM probe failed, using total RAM fallback: ${(0, _camstack_types.errMsg)(probeErr)}`);
|
|
65
65
|
}
|
|
66
66
|
return Math.round(node_os.totalmem() / 1024 / 1024);
|
|
67
67
|
}
|
|
@@ -73,10 +73,9 @@ var PlatformScorer = class {
|
|
|
73
73
|
* Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
|
|
74
74
|
* The Docker hub ships NO system `python3` — inference runs on the
|
|
75
75
|
* downloaded portable build — so probing system `python3`/`python` would
|
|
76
|
-
* wrongly report "Python not found"
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* environments where Python is on PATH).
|
|
76
|
+
* wrongly report "Python not found". When set, the interpreter lookup
|
|
77
|
+
* tries THIS binary first; `null` falls back to a system `python3`/`python`
|
|
78
|
+
* lookup (dev / agent environments where Python is on PATH).
|
|
80
79
|
*/
|
|
81
80
|
embeddedPythonPath;
|
|
82
81
|
constructor(logger = noopLogger, embeddedPythonPath = null) {
|
|
@@ -84,14 +83,16 @@ var PlatformScorer = class {
|
|
|
84
83
|
this.embeddedPythonPath = embeddedPythonPath;
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
|
-
* Probe hardware +
|
|
86
|
+
* Probe hardware + derive scores from pure rules and score all backend combos.
|
|
88
87
|
*
|
|
89
88
|
* An optional `onPhase` callback is invoked at each step so consumers
|
|
90
89
|
* (e.g. the platform-probe addon) can emit live progress events on
|
|
91
90
|
* the event bus. The callback takes a phase id + a typed payload; all
|
|
92
|
-
* phases fire in strict order: `started` → `hardware` → `
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* phases fire in strict order: `started` → `hardware` → `scored` → `done`.
|
|
92
|
+
* On failure, `error` fires once with the exception. Cached after first call.
|
|
93
|
+
*
|
|
94
|
+
* Scores are hardware-driven only (no Python module import subprocess):
|
|
95
|
+
* `scoreRuntimes` from `@camstack/types` is the single source of truth.
|
|
95
96
|
*/
|
|
96
97
|
async probe(onPhase) {
|
|
97
98
|
if (this.cached) return this.cached;
|
|
@@ -109,19 +110,10 @@ var PlatformScorer = class {
|
|
|
109
110
|
if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
|
|
110
111
|
if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
|
|
111
112
|
onPhase?.("hardware", { hardware });
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
pythonPath: pythonInfo.pythonPath,
|
|
116
|
-
backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
|
|
117
|
-
} });
|
|
113
|
+
const { scores, best: bestScore } = (0, _camstack_types.scoreRuntimes)(hardware);
|
|
114
|
+
const pythonPath = await this.resolvePythonPath();
|
|
115
|
+
if (pythonPath) this.logger.info("Python interpreter located", { meta: { pythonPath } });
|
|
118
116
|
else this.logger.info("Python: not found");
|
|
119
|
-
onPhase?.("python-backends", {
|
|
120
|
-
pythonPath: pythonInfo.pythonPath,
|
|
121
|
-
backends: pythonInfo.backends
|
|
122
|
-
});
|
|
123
|
-
const scores = this.scoreBackends(hardware, pythonInfo.backends);
|
|
124
|
-
const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
|
|
125
117
|
const elapsed = Date.now() - start;
|
|
126
118
|
this.logger.info("Scoring complete", { meta: {
|
|
127
119
|
elapsedMs: elapsed,
|
|
@@ -144,9 +136,9 @@ var PlatformScorer = class {
|
|
|
144
136
|
} });
|
|
145
137
|
this.cached = {
|
|
146
138
|
hardware,
|
|
147
|
-
scores,
|
|
139
|
+
scores: [...scores],
|
|
148
140
|
bestScore,
|
|
149
|
-
pythonPath
|
|
141
|
+
pythonPath
|
|
150
142
|
};
|
|
151
143
|
onPhase?.("scored", {
|
|
152
144
|
scores,
|
|
@@ -231,130 +223,26 @@ var PlatformScorer = class {
|
|
|
231
223
|
npu
|
|
232
224
|
};
|
|
233
225
|
}
|
|
234
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Locates a Python interpreter without importing any module.
|
|
228
|
+
*
|
|
229
|
+
* Tries: embedded path (if configured) → `python3` → `python`.
|
|
230
|
+
* Returns the first executable found, or `null` if none respond.
|
|
231
|
+
* MUST NOT run `-c "import ..."` — interpreter location only.
|
|
232
|
+
*/
|
|
233
|
+
async resolvePythonPath() {
|
|
235
234
|
const candidates = [
|
|
236
235
|
...this.embeddedPythonPath ? [this.embeddedPythonPath] : [],
|
|
237
236
|
"python3",
|
|
238
237
|
"python"
|
|
239
238
|
];
|
|
240
|
-
let pythonPath = null;
|
|
241
239
|
for (const cmd of candidates) try {
|
|
242
240
|
await execFileAsync$2(cmd, ["--version"], { timeout: 5e3 });
|
|
243
|
-
|
|
244
|
-
break;
|
|
241
|
+
return cmd;
|
|
245
242
|
} catch (err) {
|
|
246
|
-
|
|
243
|
+
this.logger.debug(`Python command "${cmd}" not found`, { meta: { error: (0, _camstack_types.errMsg)(err) } });
|
|
247
244
|
}
|
|
248
|
-
|
|
249
|
-
pythonPath: null,
|
|
250
|
-
backends: []
|
|
251
|
-
};
|
|
252
|
-
const results = await Promise.all([
|
|
253
|
-
[
|
|
254
|
-
"coremltools",
|
|
255
|
-
"coreml",
|
|
256
|
-
"coreml"
|
|
257
|
-
],
|
|
258
|
-
[
|
|
259
|
-
"openvino.runtime",
|
|
260
|
-
"openvino",
|
|
261
|
-
"openvino"
|
|
262
|
-
],
|
|
263
|
-
[
|
|
264
|
-
"torch",
|
|
265
|
-
"pytorch",
|
|
266
|
-
"onnx"
|
|
267
|
-
],
|
|
268
|
-
[
|
|
269
|
-
"onnxruntime",
|
|
270
|
-
"onnx-py",
|
|
271
|
-
"onnx"
|
|
272
|
-
]
|
|
273
|
-
].map(async ([mod, id, format]) => {
|
|
274
|
-
const probeStart = Date.now();
|
|
275
|
-
let available = false;
|
|
276
|
-
try {
|
|
277
|
-
await execFileAsync$2(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
|
|
278
|
-
available = true;
|
|
279
|
-
this.logger.info("Python backend confirmed", { meta: {
|
|
280
|
-
backend: id,
|
|
281
|
-
module: mod
|
|
282
|
-
} });
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.debug(`Python module "${mod}" not installed: ${(0, _camstack_types.errMsg)(err)}`);
|
|
285
|
-
if (id === "openvino") this.logger.info("Python backend not yet confirmed: openvino (may be pending proactive install on Intel hardware — falling back to onnx-cpu)", { meta: { module: mod } });
|
|
286
|
-
}
|
|
287
|
-
const probeMs = Date.now() - probeStart;
|
|
288
|
-
this.logger.debug("Python module probed", { meta: {
|
|
289
|
-
module: mod,
|
|
290
|
-
available,
|
|
291
|
-
probeMs
|
|
292
|
-
} });
|
|
293
|
-
return {
|
|
294
|
-
mod,
|
|
295
|
-
id,
|
|
296
|
-
format,
|
|
297
|
-
available
|
|
298
|
-
};
|
|
299
|
-
}));
|
|
300
|
-
const backends = [];
|
|
301
|
-
for (const r of results) if (r.id === "coreml" && node_os.platform() === "darwin") backends.push({
|
|
302
|
-
id: r.id,
|
|
303
|
-
format: r.format,
|
|
304
|
-
available: r.available
|
|
305
|
-
});
|
|
306
|
-
else if (r.available) backends.push({
|
|
307
|
-
id: r.id,
|
|
308
|
-
format: r.format,
|
|
309
|
-
available: r.available
|
|
310
|
-
});
|
|
311
|
-
return {
|
|
312
|
-
pythonPath,
|
|
313
|
-
backends
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
scoreBackends(hardware, pythonBackends) {
|
|
317
|
-
const scores = [];
|
|
318
|
-
if (hardware.platform === "darwin" && hardware.arch === "arm64") {
|
|
319
|
-
const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
|
|
320
|
-
if (pyCoreMl) scores.push({
|
|
321
|
-
runtime: "python",
|
|
322
|
-
backend: "coreml",
|
|
323
|
-
format: "coreml",
|
|
324
|
-
score: 95,
|
|
325
|
-
reason: "Apple Neural Engine (Python CoreML)",
|
|
326
|
-
available: pyCoreMl.available
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
if (hardware.gpu?.type === "nvidia") scores.push({
|
|
330
|
-
runtime: "python",
|
|
331
|
-
backend: "cuda",
|
|
332
|
-
format: "onnx",
|
|
333
|
-
score: 85,
|
|
334
|
-
reason: "NVIDIA CUDA (Python ONNX Runtime)",
|
|
335
|
-
available: true
|
|
336
|
-
});
|
|
337
|
-
const openvino = pythonBackends.find((b) => b.id === "openvino");
|
|
338
|
-
if (openvino) {
|
|
339
|
-
const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
|
|
340
|
-
scores.push({
|
|
341
|
-
runtime: "python",
|
|
342
|
-
backend: "openvino",
|
|
343
|
-
format: "openvino",
|
|
344
|
-
score,
|
|
345
|
-
reason: "Intel OpenVINO",
|
|
346
|
-
available: openvino.available
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
scores.push({
|
|
350
|
-
runtime: "python",
|
|
351
|
-
backend: "cpu",
|
|
352
|
-
format: "onnx",
|
|
353
|
-
score: 50,
|
|
354
|
-
reason: "CPU (Python ONNX Runtime)",
|
|
355
|
-
available: true
|
|
356
|
-
});
|
|
357
|
-
return scores.toSorted((a, b) => b.score - a.score);
|
|
245
|
+
return null;
|
|
358
246
|
}
|
|
359
247
|
};
|
|
360
248
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { BaseAddon, EventCategory, errMsg, platformProbeCapability } from "@camstack/types";
|
|
2
|
+
import { BaseAddon, EventCategory, errMsg, platformProbeCapability, scoreRuntimes } from "@camstack/types";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import * as os from "node:os";
|
|
@@ -53,8 +53,8 @@ async function getAvailableRAM_MB() {
|
|
|
53
53
|
const match = readFileSync("/proc/meminfo", "utf8").match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
54
54
|
if (match) return Math.round(parseInt(match[1]) / 1024);
|
|
55
55
|
}
|
|
56
|
-
} catch (
|
|
57
|
-
console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(
|
|
56
|
+
} catch (probeErr) {
|
|
57
|
+
console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(probeErr)}`);
|
|
58
58
|
}
|
|
59
59
|
return Math.round(os.totalmem() / 1024 / 1024);
|
|
60
60
|
}
|
|
@@ -66,10 +66,9 @@ var PlatformScorer = class {
|
|
|
66
66
|
* Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
|
|
67
67
|
* The Docker hub ships NO system `python3` — inference runs on the
|
|
68
68
|
* downloaded portable build — so probing system `python3`/`python` would
|
|
69
|
-
* wrongly report "Python not found"
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* environments where Python is on PATH).
|
|
69
|
+
* wrongly report "Python not found". When set, the interpreter lookup
|
|
70
|
+
* tries THIS binary first; `null` falls back to a system `python3`/`python`
|
|
71
|
+
* lookup (dev / agent environments where Python is on PATH).
|
|
73
72
|
*/
|
|
74
73
|
embeddedPythonPath;
|
|
75
74
|
constructor(logger = noopLogger, embeddedPythonPath = null) {
|
|
@@ -77,14 +76,16 @@ var PlatformScorer = class {
|
|
|
77
76
|
this.embeddedPythonPath = embeddedPythonPath;
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
|
-
* Probe hardware +
|
|
79
|
+
* Probe hardware + derive scores from pure rules and score all backend combos.
|
|
81
80
|
*
|
|
82
81
|
* An optional `onPhase` callback is invoked at each step so consumers
|
|
83
82
|
* (e.g. the platform-probe addon) can emit live progress events on
|
|
84
83
|
* the event bus. The callback takes a phase id + a typed payload; all
|
|
85
|
-
* phases fire in strict order: `started` → `hardware` → `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* phases fire in strict order: `started` → `hardware` → `scored` → `done`.
|
|
85
|
+
* On failure, `error` fires once with the exception. Cached after first call.
|
|
86
|
+
*
|
|
87
|
+
* Scores are hardware-driven only (no Python module import subprocess):
|
|
88
|
+
* `scoreRuntimes` from `@camstack/types` is the single source of truth.
|
|
88
89
|
*/
|
|
89
90
|
async probe(onPhase) {
|
|
90
91
|
if (this.cached) return this.cached;
|
|
@@ -102,19 +103,10 @@ var PlatformScorer = class {
|
|
|
102
103
|
if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
|
|
103
104
|
if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
|
|
104
105
|
onPhase?.("hardware", { hardware });
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
pythonPath: pythonInfo.pythonPath,
|
|
109
|
-
backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
|
|
110
|
-
} });
|
|
106
|
+
const { scores, best: bestScore } = scoreRuntimes(hardware);
|
|
107
|
+
const pythonPath = await this.resolvePythonPath();
|
|
108
|
+
if (pythonPath) this.logger.info("Python interpreter located", { meta: { pythonPath } });
|
|
111
109
|
else this.logger.info("Python: not found");
|
|
112
|
-
onPhase?.("python-backends", {
|
|
113
|
-
pythonPath: pythonInfo.pythonPath,
|
|
114
|
-
backends: pythonInfo.backends
|
|
115
|
-
});
|
|
116
|
-
const scores = this.scoreBackends(hardware, pythonInfo.backends);
|
|
117
|
-
const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
|
|
118
110
|
const elapsed = Date.now() - start;
|
|
119
111
|
this.logger.info("Scoring complete", { meta: {
|
|
120
112
|
elapsedMs: elapsed,
|
|
@@ -137,9 +129,9 @@ var PlatformScorer = class {
|
|
|
137
129
|
} });
|
|
138
130
|
this.cached = {
|
|
139
131
|
hardware,
|
|
140
|
-
scores,
|
|
132
|
+
scores: [...scores],
|
|
141
133
|
bestScore,
|
|
142
|
-
pythonPath
|
|
134
|
+
pythonPath
|
|
143
135
|
};
|
|
144
136
|
onPhase?.("scored", {
|
|
145
137
|
scores,
|
|
@@ -224,130 +216,26 @@ var PlatformScorer = class {
|
|
|
224
216
|
npu
|
|
225
217
|
};
|
|
226
218
|
}
|
|
227
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Locates a Python interpreter without importing any module.
|
|
221
|
+
*
|
|
222
|
+
* Tries: embedded path (if configured) → `python3` → `python`.
|
|
223
|
+
* Returns the first executable found, or `null` if none respond.
|
|
224
|
+
* MUST NOT run `-c "import ..."` — interpreter location only.
|
|
225
|
+
*/
|
|
226
|
+
async resolvePythonPath() {
|
|
228
227
|
const candidates = [
|
|
229
228
|
...this.embeddedPythonPath ? [this.embeddedPythonPath] : [],
|
|
230
229
|
"python3",
|
|
231
230
|
"python"
|
|
232
231
|
];
|
|
233
|
-
let pythonPath = null;
|
|
234
232
|
for (const cmd of candidates) try {
|
|
235
233
|
await execFileAsync$2(cmd, ["--version"], { timeout: 5e3 });
|
|
236
|
-
|
|
237
|
-
break;
|
|
234
|
+
return cmd;
|
|
238
235
|
} catch (err) {
|
|
239
|
-
|
|
236
|
+
this.logger.debug(`Python command "${cmd}" not found`, { meta: { error: errMsg(err) } });
|
|
240
237
|
}
|
|
241
|
-
|
|
242
|
-
pythonPath: null,
|
|
243
|
-
backends: []
|
|
244
|
-
};
|
|
245
|
-
const results = await Promise.all([
|
|
246
|
-
[
|
|
247
|
-
"coremltools",
|
|
248
|
-
"coreml",
|
|
249
|
-
"coreml"
|
|
250
|
-
],
|
|
251
|
-
[
|
|
252
|
-
"openvino.runtime",
|
|
253
|
-
"openvino",
|
|
254
|
-
"openvino"
|
|
255
|
-
],
|
|
256
|
-
[
|
|
257
|
-
"torch",
|
|
258
|
-
"pytorch",
|
|
259
|
-
"onnx"
|
|
260
|
-
],
|
|
261
|
-
[
|
|
262
|
-
"onnxruntime",
|
|
263
|
-
"onnx-py",
|
|
264
|
-
"onnx"
|
|
265
|
-
]
|
|
266
|
-
].map(async ([mod, id, format]) => {
|
|
267
|
-
const probeStart = Date.now();
|
|
268
|
-
let available = false;
|
|
269
|
-
try {
|
|
270
|
-
await execFileAsync$2(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
|
|
271
|
-
available = true;
|
|
272
|
-
this.logger.info("Python backend confirmed", { meta: {
|
|
273
|
-
backend: id,
|
|
274
|
-
module: mod
|
|
275
|
-
} });
|
|
276
|
-
} catch (err) {
|
|
277
|
-
console.debug(`Python module "${mod}" not installed: ${errMsg(err)}`);
|
|
278
|
-
if (id === "openvino") this.logger.info("Python backend not yet confirmed: openvino (may be pending proactive install on Intel hardware — falling back to onnx-cpu)", { meta: { module: mod } });
|
|
279
|
-
}
|
|
280
|
-
const probeMs = Date.now() - probeStart;
|
|
281
|
-
this.logger.debug("Python module probed", { meta: {
|
|
282
|
-
module: mod,
|
|
283
|
-
available,
|
|
284
|
-
probeMs
|
|
285
|
-
} });
|
|
286
|
-
return {
|
|
287
|
-
mod,
|
|
288
|
-
id,
|
|
289
|
-
format,
|
|
290
|
-
available
|
|
291
|
-
};
|
|
292
|
-
}));
|
|
293
|
-
const backends = [];
|
|
294
|
-
for (const r of results) if (r.id === "coreml" && os.platform() === "darwin") backends.push({
|
|
295
|
-
id: r.id,
|
|
296
|
-
format: r.format,
|
|
297
|
-
available: r.available
|
|
298
|
-
});
|
|
299
|
-
else if (r.available) backends.push({
|
|
300
|
-
id: r.id,
|
|
301
|
-
format: r.format,
|
|
302
|
-
available: r.available
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
pythonPath,
|
|
306
|
-
backends
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
scoreBackends(hardware, pythonBackends) {
|
|
310
|
-
const scores = [];
|
|
311
|
-
if (hardware.platform === "darwin" && hardware.arch === "arm64") {
|
|
312
|
-
const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
|
|
313
|
-
if (pyCoreMl) scores.push({
|
|
314
|
-
runtime: "python",
|
|
315
|
-
backend: "coreml",
|
|
316
|
-
format: "coreml",
|
|
317
|
-
score: 95,
|
|
318
|
-
reason: "Apple Neural Engine (Python CoreML)",
|
|
319
|
-
available: pyCoreMl.available
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
if (hardware.gpu?.type === "nvidia") scores.push({
|
|
323
|
-
runtime: "python",
|
|
324
|
-
backend: "cuda",
|
|
325
|
-
format: "onnx",
|
|
326
|
-
score: 85,
|
|
327
|
-
reason: "NVIDIA CUDA (Python ONNX Runtime)",
|
|
328
|
-
available: true
|
|
329
|
-
});
|
|
330
|
-
const openvino = pythonBackends.find((b) => b.id === "openvino");
|
|
331
|
-
if (openvino) {
|
|
332
|
-
const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
|
|
333
|
-
scores.push({
|
|
334
|
-
runtime: "python",
|
|
335
|
-
backend: "openvino",
|
|
336
|
-
format: "openvino",
|
|
337
|
-
score,
|
|
338
|
-
reason: "Intel OpenVINO",
|
|
339
|
-
available: openvino.available
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
scores.push({
|
|
343
|
-
runtime: "python",
|
|
344
|
-
backend: "cpu",
|
|
345
|
-
format: "onnx",
|
|
346
|
-
score: 50,
|
|
347
|
-
reason: "CPU (Python ONNX Runtime)",
|
|
348
|
-
available: true
|
|
349
|
-
});
|
|
350
|
-
return scores.toSorted((a, b) => b.score - a.score);
|
|
238
|
+
return null;
|
|
351
239
|
}
|
|
352
240
|
};
|
|
353
241
|
//#endregion
|
|
@@ -6,25 +6,32 @@ export declare class PlatformScorer {
|
|
|
6
6
|
* Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
|
|
7
7
|
* The Docker hub ships NO system `python3` — inference runs on the
|
|
8
8
|
* downloaded portable build — so probing system `python3`/`python` would
|
|
9
|
-
* wrongly report "Python not found"
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* environments where Python is on PATH).
|
|
9
|
+
* wrongly report "Python not found". When set, the interpreter lookup
|
|
10
|
+
* tries THIS binary first; `null` falls back to a system `python3`/`python`
|
|
11
|
+
* lookup (dev / agent environments where Python is on PATH).
|
|
13
12
|
*/
|
|
14
13
|
private readonly embeddedPythonPath;
|
|
15
14
|
constructor(logger?: IScopedLogger, embeddedPythonPath?: string | null);
|
|
16
15
|
/**
|
|
17
|
-
* Probe hardware +
|
|
16
|
+
* Probe hardware + derive scores from pure rules and score all backend combos.
|
|
18
17
|
*
|
|
19
18
|
* An optional `onPhase` callback is invoked at each step so consumers
|
|
20
19
|
* (e.g. the platform-probe addon) can emit live progress events on
|
|
21
20
|
* the event bus. The callback takes a phase id + a typed payload; all
|
|
22
|
-
* phases fire in strict order: `started` → `hardware` → `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* phases fire in strict order: `started` → `hardware` → `scored` → `done`.
|
|
22
|
+
* On failure, `error` fires once with the exception. Cached after first call.
|
|
23
|
+
*
|
|
24
|
+
* Scores are hardware-driven only (no Python module import subprocess):
|
|
25
|
+
* `scoreRuntimes` from `@camstack/types` is the single source of truth.
|
|
25
26
|
*/
|
|
26
27
|
probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities>;
|
|
27
28
|
probeHardware(): Promise<HardwareInfo>;
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Locates a Python interpreter without importing any module.
|
|
31
|
+
*
|
|
32
|
+
* Tries: embedded path (if configured) → `python3` → `python`.
|
|
33
|
+
* Returns the first executable found, or `null` if none respond.
|
|
34
|
+
* MUST NOT run `-c "import ..."` — interpreter location only.
|
|
35
|
+
*/
|
|
36
|
+
private resolvePythonPath;
|
|
30
37
|
}
|