miravo 0.6.0 → 0.6.2
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/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2E test: concurrent OPC UA client connections, reads, clean + unclean disconnect,
|
|
3
|
-
* and reconnect. Validates the session-pool hardening changes (MAX_SESSIONS, HELLO_TIMEOUT).
|
|
4
|
-
*/
|
|
5
|
-
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import type { EventEmitter } from "node:events";
|
|
7
|
-
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
8
|
-
import { createServer } from "node:net";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
11
|
-
import type { ClientSession } from "node-opcua-client";
|
|
12
|
-
import { AttributeIds, MessageSecurityMode, OPCUAClient, SecurityPolicy } from "node-opcua-client";
|
|
13
|
-
import { stringify as stringifyYAML } from "yaml";
|
|
14
|
-
|
|
15
|
-
async function canBindLocalTcp(): Promise<boolean> {
|
|
16
|
-
const probe = createServer();
|
|
17
|
-
return new Promise((res) => {
|
|
18
|
-
const cleanup = () => {
|
|
19
|
-
try {
|
|
20
|
-
probe.close();
|
|
21
|
-
} catch {
|
|
22
|
-
/* ignore */
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
(probe as unknown as EventEmitter).once("error", () => {
|
|
26
|
-
cleanup();
|
|
27
|
-
res(false);
|
|
28
|
-
});
|
|
29
|
-
(probe as unknown as EventEmitter).once("listening", () => {
|
|
30
|
-
cleanup();
|
|
31
|
-
res(true);
|
|
32
|
-
});
|
|
33
|
-
probe.listen(0, "127.0.0.1");
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const TCP_AVAILABLE = await canBindLocalTcp();
|
|
38
|
-
const describeIfTcp = TCP_AVAILABLE ? describe : describe.skip;
|
|
39
|
-
|
|
40
|
-
interface RuntimeHandle {
|
|
41
|
-
readonly process: Bun.Subprocess<"ignore", "pipe", "pipe">;
|
|
42
|
-
readonly tmpDir: string;
|
|
43
|
-
readonly opcuaPort: number;
|
|
44
|
-
readonly instancePath: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function startRuntime(): Promise<RuntimeHandle> {
|
|
48
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "miravo-opcua-e2e-"));
|
|
49
|
-
const modelsDir = join(tmpDir, "models");
|
|
50
|
-
const templatePath = join(tmpDir, "e2e.miravo.yaml");
|
|
51
|
-
const modelPath = join(modelsDir, "e2e-pump.twin.yaml");
|
|
52
|
-
const readyFile = join(tmpDir, "ready.json");
|
|
53
|
-
|
|
54
|
-
await mkdir(modelsDir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
await Bun.write(
|
|
57
|
-
modelPath,
|
|
58
|
-
stringifyYAML({
|
|
59
|
-
schema_version: "2",
|
|
60
|
-
model: {
|
|
61
|
-
name: "e2e-pump",
|
|
62
|
-
namespace: "com.miravo.testing.e2e-pump",
|
|
63
|
-
description: "Minimal pump for OPC UA concurrent-client e2e test.",
|
|
64
|
-
},
|
|
65
|
-
parameters: {},
|
|
66
|
-
methods: {},
|
|
67
|
-
members: {
|
|
68
|
-
speed: {
|
|
69
|
-
kind: "variable",
|
|
70
|
-
data_type: "Double",
|
|
71
|
-
access: "read",
|
|
72
|
-
unit: "rpm",
|
|
73
|
-
generator: { type: "sine", center: 500, amplitude: 100, period: 10 },
|
|
74
|
-
},
|
|
75
|
-
status: {
|
|
76
|
-
kind: "property",
|
|
77
|
-
data_type: "String",
|
|
78
|
-
access: "read",
|
|
79
|
-
default: "running",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
faults: [],
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
await Bun.write(
|
|
87
|
-
templatePath,
|
|
88
|
-
stringifyYAML({
|
|
89
|
-
kind: "MiravoTemplate",
|
|
90
|
-
version: "1",
|
|
91
|
-
name: "E2E Template",
|
|
92
|
-
namespace: { prefix: "e2e" },
|
|
93
|
-
areas: [
|
|
94
|
-
{
|
|
95
|
-
name: "plant",
|
|
96
|
-
lines: [{ name: "line1", twins: [{ model: "e2e-pump", count: 1, variation: 0 }] }],
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
settings: { seed: 1, tick_interval_ms: 100, time_multiplier: 1 },
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const proc = Bun.spawn({
|
|
104
|
-
cmd: [
|
|
105
|
-
"bun",
|
|
106
|
-
"run",
|
|
107
|
-
resolve(import.meta.dir, "executable-methods-runner.ts"),
|
|
108
|
-
"--template",
|
|
109
|
-
templatePath,
|
|
110
|
-
"--models-dir",
|
|
111
|
-
modelsDir,
|
|
112
|
-
"--ready-file",
|
|
113
|
-
readyFile,
|
|
114
|
-
],
|
|
115
|
-
cwd: resolve("."),
|
|
116
|
-
stdin: "ignore",
|
|
117
|
-
stdout: "pipe",
|
|
118
|
-
stderr: "pipe",
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const ready = await Promise.race([
|
|
123
|
-
(async () => {
|
|
124
|
-
const deadline = Date.now() + 20_000;
|
|
125
|
-
while (Date.now() < deadline) {
|
|
126
|
-
if (!(await Bun.file(readyFile).exists())) {
|
|
127
|
-
await Bun.sleep(50);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
return (await Bun.file(readyFile).json()) as {
|
|
131
|
-
readonly opcuaEndpointUrl: string;
|
|
132
|
-
readonly instancePath: string;
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
throw new Error("Runtime did not write ready file within 20 s");
|
|
136
|
-
})(),
|
|
137
|
-
proc.exited.then((code) => {
|
|
138
|
-
throw new Error(`Runtime exited early with code ${code}`);
|
|
139
|
-
}),
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
const match = ready.opcuaEndpointUrl.match(/:(\d+)\//);
|
|
143
|
-
const opcuaPort = match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
144
|
-
return { process: proc, tmpDir, opcuaPort, instancePath: ready.instancePath };
|
|
145
|
-
} catch (error) {
|
|
146
|
-
proc.kill("SIGTERM");
|
|
147
|
-
await proc.exited;
|
|
148
|
-
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
149
|
-
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
150
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
151
|
-
throw new Error(
|
|
152
|
-
[
|
|
153
|
-
`Failed to start runtime: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
-
stdout.trim() ? `stdout:\n${stdout.trim()}` : "",
|
|
155
|
-
stderr.trim() ? `stderr:\n${stderr.trim()}` : "",
|
|
156
|
-
]
|
|
157
|
-
.filter(Boolean)
|
|
158
|
-
.join("\n\n"),
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function stopRuntime(handle: RuntimeHandle): Promise<void> {
|
|
164
|
-
handle.process.kill("SIGTERM");
|
|
165
|
-
await Promise.race([
|
|
166
|
-
handle.process.exited,
|
|
167
|
-
Bun.sleep(8_000).then(() => handle.process.kill("SIGKILL")),
|
|
168
|
-
]);
|
|
169
|
-
await rm(handle.tmpDir, { recursive: true, force: true });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function makeClient(): OPCUAClient {
|
|
173
|
-
return OPCUAClient.create({
|
|
174
|
-
securityMode: MessageSecurityMode.None,
|
|
175
|
-
securityPolicy: SecurityPolicy.None,
|
|
176
|
-
endpointMustExist: false,
|
|
177
|
-
connectionStrategy: { maxRetry: 0, initialDelay: 0 },
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Creates a client that will NOT send CloseSession on disconnect.
|
|
183
|
-
* `keepPendingSessionsOnDisconnect: true` skips `_close_pending_sessions()`,
|
|
184
|
-
* so the server retains the session slot until its 30 s watchdog fires.
|
|
185
|
-
* Use this to simulate unclean client crashes.
|
|
186
|
-
*/
|
|
187
|
-
function makeZombieClient(): OPCUAClient {
|
|
188
|
-
return OPCUAClient.create({
|
|
189
|
-
securityMode: MessageSecurityMode.None,
|
|
190
|
-
securityPolicy: SecurityPolicy.None,
|
|
191
|
-
endpointMustExist: false,
|
|
192
|
-
connectionStrategy: { maxRetry: 0, initialDelay: 0 },
|
|
193
|
-
keepPendingSessionsOnDisconnect: true,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function connectClient(
|
|
198
|
-
port: number,
|
|
199
|
-
): Promise<{ client: OPCUAClient; session: ClientSession }> {
|
|
200
|
-
const client = makeClient();
|
|
201
|
-
await client.connect(`opc.tcp://127.0.0.1:${port}`);
|
|
202
|
-
const session = await client.createSession();
|
|
203
|
-
return { client, session };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function closeClient(client: OPCUAClient, session: ClientSession): Promise<void> {
|
|
207
|
-
await session.close().catch(() => {});
|
|
208
|
-
await client.disconnect().catch(() => {});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function readSpeed(session: ClientSession, instancePath: string): Promise<number> {
|
|
212
|
-
const dv = await session.read({
|
|
213
|
-
nodeId: `ns=1;s=${instancePath}/speed`,
|
|
214
|
-
attributeId: AttributeIds.Value,
|
|
215
|
-
});
|
|
216
|
-
const v = dv.value.value;
|
|
217
|
-
if (typeof v !== "number") throw new Error(`Expected number, got ${typeof v}`);
|
|
218
|
-
return v;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
describeIfTcp("OPC UA concurrent-client e2e", () => {
|
|
222
|
-
let runtime: RuntimeHandle | undefined;
|
|
223
|
-
|
|
224
|
-
test("setup: start miravo runtime with OPC UA", async () => {
|
|
225
|
-
runtime = await startRuntime();
|
|
226
|
-
expect(runtime.opcuaPort).toBeGreaterThan(0);
|
|
227
|
-
}, 30_000);
|
|
228
|
-
|
|
229
|
-
test("20 clients connect concurrently and all read a value", async () => {
|
|
230
|
-
if (!runtime) return;
|
|
231
|
-
const rt = runtime;
|
|
232
|
-
const N = 20;
|
|
233
|
-
const handles = await Promise.all(Array.from({ length: N }, () => connectClient(rt.opcuaPort)));
|
|
234
|
-
try {
|
|
235
|
-
const values = await Promise.all(
|
|
236
|
-
handles.map(({ session }) => readSpeed(session, rt.instancePath)),
|
|
237
|
-
);
|
|
238
|
-
expect(values).toHaveLength(N);
|
|
239
|
-
for (const v of values) {
|
|
240
|
-
expect(typeof v).toBe("number");
|
|
241
|
-
expect(Number.isFinite(v)).toBe(true);
|
|
242
|
-
}
|
|
243
|
-
console.log(` ✓ ${N} concurrent clients read speed; sample: ${values[0]?.toFixed(2)}`);
|
|
244
|
-
} finally {
|
|
245
|
-
await Promise.all(handles.map(({ client, session }) => closeClient(client, session)));
|
|
246
|
-
}
|
|
247
|
-
}, 30_000);
|
|
248
|
-
|
|
249
|
-
test("client disconnects cleanly and reconnects — reads succeed after reconnect", async () => {
|
|
250
|
-
if (!runtime) return;
|
|
251
|
-
const { client: c1, session: s1 } = await connectClient(runtime.opcuaPort);
|
|
252
|
-
const before = await readSpeed(s1, runtime.instancePath);
|
|
253
|
-
await closeClient(c1, s1);
|
|
254
|
-
console.log(` ✓ Client read ${before.toFixed(2)} before clean disconnect`);
|
|
255
|
-
|
|
256
|
-
const { client: c2, session: s2 } = await connectClient(runtime.opcuaPort);
|
|
257
|
-
try {
|
|
258
|
-
const after = await readSpeed(s2, runtime.instancePath);
|
|
259
|
-
expect(typeof after).toBe("number");
|
|
260
|
-
console.log(` ✓ Reconnected client read ${after.toFixed(2)}`);
|
|
261
|
-
} finally {
|
|
262
|
-
await closeClient(c2, s2);
|
|
263
|
-
}
|
|
264
|
-
}, 20_000);
|
|
265
|
-
|
|
266
|
-
test("unclean disconnect stress: 15 zombie sessions exhaust old default (10) — new client still connects", async () => {
|
|
267
|
-
if (!runtime) return;
|
|
268
|
-
const rt = runtime;
|
|
269
|
-
// node-opcua default maxSessions is 10; our fix sets it to 500.
|
|
270
|
-
// makeZombieClient() uses keepPendingSessionsOnDisconnect:true so client.disconnect()
|
|
271
|
-
// skips CloseSession — the server retains each session slot for ~30 s (session watchdog).
|
|
272
|
-
// With the old default, the 11th slot is unavailable → connectClient() throws
|
|
273
|
-
// BadTooManySessions → test fails.
|
|
274
|
-
const ZOMBIE_COUNT = 15; // intentionally > old default of 10
|
|
275
|
-
|
|
276
|
-
const zombies = await Promise.all(
|
|
277
|
-
Array.from({ length: ZOMBIE_COUNT }, async () => {
|
|
278
|
-
const client = makeZombieClient();
|
|
279
|
-
await client.connect(`opc.tcp://127.0.0.1:${rt.opcuaPort}`);
|
|
280
|
-
const session = await client.createSession();
|
|
281
|
-
return { client, session };
|
|
282
|
-
}),
|
|
283
|
-
);
|
|
284
|
-
// Disconnect without CloseSession — server retains sessions, consuming pool slots.
|
|
285
|
-
await Promise.all(zombies.map(({ client }) => client.disconnect().catch(() => {})));
|
|
286
|
-
console.log(` ✓ ${ZOMBIE_COUNT} zombie sessions dropped (linger on server for ~30 s)`);
|
|
287
|
-
|
|
288
|
-
const { client: fresh, session: freshSession } = await connectClient(runtime.opcuaPort);
|
|
289
|
-
try {
|
|
290
|
-
const v = await readSpeed(freshSession, runtime.instancePath);
|
|
291
|
-
expect(typeof v).toBe("number");
|
|
292
|
-
console.log(
|
|
293
|
-
` ✓ Client #${ZOMBIE_COUNT + 1} connected and read ${v.toFixed(2)} while ${ZOMBIE_COUNT} zombie sessions hold server slots`,
|
|
294
|
-
);
|
|
295
|
-
} finally {
|
|
296
|
-
await closeClient(fresh, freshSession);
|
|
297
|
-
}
|
|
298
|
-
}, 30_000);
|
|
299
|
-
|
|
300
|
-
test("10 sequential connect/read/disconnect cycles complete without error", async () => {
|
|
301
|
-
if (!runtime) return;
|
|
302
|
-
for (let i = 0; i < 10; i++) {
|
|
303
|
-
const { client, session } = await connectClient(runtime.opcuaPort);
|
|
304
|
-
const v = await readSpeed(session, runtime.instancePath);
|
|
305
|
-
expect(typeof v).toBe("number");
|
|
306
|
-
await closeClient(client, session);
|
|
307
|
-
}
|
|
308
|
-
console.log(" ✓ 10 sequential connect-read-disconnect cycles completed");
|
|
309
|
-
}, 30_000);
|
|
310
|
-
|
|
311
|
-
test("teardown: stop miravo runtime", async () => {
|
|
312
|
-
if (runtime) await stopRuntime(runtime);
|
|
313
|
-
}, 15_000);
|
|
314
|
-
});
|
package/src/commands/adapter.ts
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
-
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
-
import type { Command } from "commander";
|
|
4
|
-
import { colors } from "consola/utils";
|
|
5
|
-
import { executeAdminCommand, getManifest, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
6
|
-
import { parsePositiveInt, parseTimeoutSeconds } from "../lib/parse.ts";
|
|
7
|
-
import {
|
|
8
|
-
engineLog,
|
|
9
|
-
handleCommandError,
|
|
10
|
-
handleCommandResult,
|
|
11
|
-
modbusLog,
|
|
12
|
-
writeOutput,
|
|
13
|
-
} from "../logger.ts";
|
|
14
|
-
import { formatTable } from "../output.ts";
|
|
15
|
-
|
|
16
|
-
interface AdapterOptions {
|
|
17
|
-
readonly port?: number;
|
|
18
|
-
readonly host?: string;
|
|
19
|
-
readonly json?: boolean;
|
|
20
|
-
readonly adminUrl?: string;
|
|
21
|
-
readonly timeout?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function registerAdapterCommand(program: Command): void {
|
|
25
|
-
const adapter = program
|
|
26
|
-
.command("adapter")
|
|
27
|
-
.description("Manage protocol adapters (MQTT, OPC UA, Modbus)");
|
|
28
|
-
|
|
29
|
-
adapter
|
|
30
|
-
.command("enable <name>")
|
|
31
|
-
.description("Enable a protocol adapter (mqtt, opcua, modbus)")
|
|
32
|
-
.option("-p, --port <number>", "Override adapter port", parsePositiveInt)
|
|
33
|
-
.option("--host <host>", "Override adapter bind host")
|
|
34
|
-
.option("--json", "Machine-readable JSON output")
|
|
35
|
-
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
36
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
37
|
-
.addHelpText(
|
|
38
|
-
"after",
|
|
39
|
-
`
|
|
40
|
-
Examples:
|
|
41
|
-
$ miravo adapter enable mqtt Enable MQTT adapter
|
|
42
|
-
$ miravo adapter enable opcua -p 4841 Enable OPC UA on custom port`,
|
|
43
|
-
)
|
|
44
|
-
.action(async (name: string, options: AdapterOptions) => {
|
|
45
|
-
try {
|
|
46
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
47
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
48
|
-
const config: Record<string, unknown> = {};
|
|
49
|
-
if (options.port !== undefined) config["port"] = options.port;
|
|
50
|
-
if (options.host !== undefined) config["host"] = options.host;
|
|
51
|
-
const command: EngineCommand = {
|
|
52
|
-
type: "enableAdapter",
|
|
53
|
-
adapter: name,
|
|
54
|
-
...(Object.keys(config).length > 0 ? { config } : {}),
|
|
55
|
-
};
|
|
56
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
57
|
-
handleCommandResult(result, options.json);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
handleCommandError(error);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
adapter
|
|
64
|
-
.command("disable <name>")
|
|
65
|
-
.description("Disable a running protocol adapter (mqtt, opcua, modbus)")
|
|
66
|
-
.option("--json", "Machine-readable JSON output")
|
|
67
|
-
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
68
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
69
|
-
.addHelpText(
|
|
70
|
-
"after",
|
|
71
|
-
`
|
|
72
|
-
Examples:
|
|
73
|
-
$ miravo adapter disable mqtt Disable MQTT adapter`,
|
|
74
|
-
)
|
|
75
|
-
.action(async (name: string, options: AdapterOptions) => {
|
|
76
|
-
try {
|
|
77
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
78
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
79
|
-
const command: EngineCommand = { type: "disableAdapter", adapter: name };
|
|
80
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
81
|
-
handleCommandResult(result, options.json);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
handleCommandError(error);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Generic adapter introspection via unified manifest
|
|
88
|
-
const inspectAction = async (name: string | undefined, options: AdapterOptions) => {
|
|
89
|
-
try {
|
|
90
|
-
const adapterName = name ?? "modbus";
|
|
91
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
92
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
93
|
-
const manifest = await getManifest(adminUrl, timeoutMs);
|
|
94
|
-
|
|
95
|
-
const allTags = manifest["tags"] as Array<Record<string, unknown>> | undefined;
|
|
96
|
-
if (!allTags || allTags.length === 0) {
|
|
97
|
-
engineLog.warn("No tags found.");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Filter to tags that have data for the requested protocol
|
|
102
|
-
const tags = allTags.filter((s) => {
|
|
103
|
-
const protocols = s["protocols"] as Record<string, unknown> | undefined;
|
|
104
|
-
return protocols?.[adapterName] !== undefined;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (tags.length === 0) {
|
|
108
|
-
engineLog.warn(`No tags found for ${adapterName} adapter.`);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (options.json) {
|
|
113
|
-
writeOutput(JSON.stringify(tags, null, 2));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Protocol-specific table rendering
|
|
118
|
-
if (adapterName === "modbus") {
|
|
119
|
-
renderModbusTable(tags);
|
|
120
|
-
} else if (adapterName === "opcua") {
|
|
121
|
-
renderOpcuaTable(tags);
|
|
122
|
-
} else {
|
|
123
|
-
renderMqttTable(tags);
|
|
124
|
-
}
|
|
125
|
-
} catch (error) {
|
|
126
|
-
handleCommandError(error);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
adapter
|
|
131
|
-
.command("inspect [name]")
|
|
132
|
-
.description("Inspect a running adapter's tag map (default: modbus)")
|
|
133
|
-
.option("--json", "Machine-readable JSON output")
|
|
134
|
-
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
135
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
136
|
-
.addHelpText(
|
|
137
|
-
"after",
|
|
138
|
-
`
|
|
139
|
-
Examples:
|
|
140
|
-
$ miravo adapter inspect modbus Show Modbus register map
|
|
141
|
-
$ miravo adapter inspect opcua Show OPC UA node map
|
|
142
|
-
$ miravo adapter inspect mqtt Show MQTT topic map
|
|
143
|
-
$ miravo adapter inspect --json JSON output for scripting`,
|
|
144
|
-
)
|
|
145
|
-
.action(inspectAction);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function renderModbusTable(tags: Array<Record<string, unknown>>): void {
|
|
149
|
-
const instanceCount = new Set(tags.map((s) => s["instance"])).size;
|
|
150
|
-
modbusLog.info(
|
|
151
|
-
`Register Map — ${colors.bold(String(tags.length))} entries, ${colors.bold(String(instanceCount))} instances`,
|
|
152
|
-
);
|
|
153
|
-
writeOutput();
|
|
154
|
-
|
|
155
|
-
const rows = tags.map((s) => {
|
|
156
|
-
const modbus = (s["protocols"] as Record<string, Record<string, unknown>>)?.["modbus"] ?? {};
|
|
157
|
-
return {
|
|
158
|
-
address: String(modbus["address"] ?? ""),
|
|
159
|
-
count: String(modbus["count"] ?? ""),
|
|
160
|
-
instance: String(s["instance"] ?? ""),
|
|
161
|
-
member: String(s["member"] ?? ""),
|
|
162
|
-
type: String(modbus["registerType"] ?? ""),
|
|
163
|
-
access: String(s["access"] ?? ""),
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
writeOutput(
|
|
168
|
-
formatTable(
|
|
169
|
-
{
|
|
170
|
-
address: colors.dim("Address"),
|
|
171
|
-
count: colors.dim("Regs"),
|
|
172
|
-
instance: colors.dim("Instance"),
|
|
173
|
-
member: colors.dim("Member"),
|
|
174
|
-
type: colors.dim("Type"),
|
|
175
|
-
access: colors.dim("Access"),
|
|
176
|
-
},
|
|
177
|
-
rows,
|
|
178
|
-
),
|
|
179
|
-
);
|
|
180
|
-
writeOutput();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function renderOpcuaTable(tags: Array<Record<string, unknown>>): void {
|
|
184
|
-
const instanceCount = new Set(tags.map((s) => s["instance"])).size;
|
|
185
|
-
engineLog.info(
|
|
186
|
-
`OPC UA — ${colors.bold(String(tags.length))} tags, ${colors.bold(String(instanceCount))} instances`,
|
|
187
|
-
);
|
|
188
|
-
writeOutput();
|
|
189
|
-
|
|
190
|
-
const rows = tags.map((s) => {
|
|
191
|
-
const opcua = (s["protocols"] as Record<string, Record<string, unknown>>)?.["opcua"] ?? {};
|
|
192
|
-
return {
|
|
193
|
-
nodeId: String(opcua["nodeId"] ?? ""),
|
|
194
|
-
instance: String(s["instance"] ?? ""),
|
|
195
|
-
member: String(s["member"] ?? ""),
|
|
196
|
-
dataType: String(s["dataType"] ?? ""),
|
|
197
|
-
access: String(s["access"] ?? ""),
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
writeOutput(
|
|
202
|
-
formatTable(
|
|
203
|
-
{
|
|
204
|
-
nodeId: colors.dim("NodeId"),
|
|
205
|
-
instance: colors.dim("Instance"),
|
|
206
|
-
member: colors.dim("Member"),
|
|
207
|
-
dataType: colors.dim("Type"),
|
|
208
|
-
access: colors.dim("Access"),
|
|
209
|
-
},
|
|
210
|
-
rows,
|
|
211
|
-
),
|
|
212
|
-
);
|
|
213
|
-
writeOutput();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function renderMqttTable(tags: Array<Record<string, unknown>>): void {
|
|
217
|
-
const instanceCount = new Set(tags.map((s) => s["instance"])).size;
|
|
218
|
-
engineLog.info(
|
|
219
|
-
`MQTT — ${colors.bold(String(tags.length))} tags, ${colors.bold(String(instanceCount))} instances`,
|
|
220
|
-
);
|
|
221
|
-
writeOutput();
|
|
222
|
-
|
|
223
|
-
const rows = tags.map((s) => {
|
|
224
|
-
const mqtt = (s["protocols"] as Record<string, Record<string, unknown>>)?.["mqtt"] ?? {};
|
|
225
|
-
return {
|
|
226
|
-
topic: String(mqtt["topic"] ?? ""),
|
|
227
|
-
instance: String(s["instance"] ?? ""),
|
|
228
|
-
member: String(s["member"] ?? ""),
|
|
229
|
-
dataType: String(s["dataType"] ?? ""),
|
|
230
|
-
access: String(s["access"] ?? ""),
|
|
231
|
-
unit: String(s["unit"] ?? ""),
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
writeOutput(
|
|
236
|
-
formatTable(
|
|
237
|
-
{
|
|
238
|
-
topic: colors.dim("Topic"),
|
|
239
|
-
instance: colors.dim("Instance"),
|
|
240
|
-
member: colors.dim("Member"),
|
|
241
|
-
dataType: colors.dim("Type"),
|
|
242
|
-
access: colors.dim("Access"),
|
|
243
|
-
unit: colors.dim("Unit"),
|
|
244
|
-
},
|
|
245
|
-
rows,
|
|
246
|
-
),
|
|
247
|
-
);
|
|
248
|
-
writeOutput();
|
|
249
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
-
import { resetExitCode, runCliCommand } from "./cli-test-setup.ts";
|
|
3
|
-
|
|
4
|
-
beforeEach(resetExitCode);
|
|
5
|
-
afterEach(resetExitCode);
|
|
6
|
-
|
|
7
|
-
test("apply le rejects invalid --scope values", async () => {
|
|
8
|
-
await runCliCommand([
|
|
9
|
-
"apply",
|
|
10
|
-
"le",
|
|
11
|
-
"--template",
|
|
12
|
-
"smart-factory",
|
|
13
|
-
"--source-hostname",
|
|
14
|
-
"10.0.0.1",
|
|
15
|
-
"--le-hostname",
|
|
16
|
-
"le.example.local",
|
|
17
|
-
"--le-api-token",
|
|
18
|
-
"token",
|
|
19
|
-
"--scope",
|
|
20
|
-
"foo,bar",
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
expect(process.exitCode).toBe(1);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("apply le rejects invalid --if-unmanaged values", async () => {
|
|
27
|
-
await runCliCommand([
|
|
28
|
-
"apply",
|
|
29
|
-
"le",
|
|
30
|
-
"--template",
|
|
31
|
-
"smart-factory",
|
|
32
|
-
"--source-hostname",
|
|
33
|
-
"10.0.0.1",
|
|
34
|
-
"--le-hostname",
|
|
35
|
-
"le.example.local",
|
|
36
|
-
"--le-api-token",
|
|
37
|
-
"token",
|
|
38
|
-
"--if-unmanaged",
|
|
39
|
-
"ignore",
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
expect(process.exitCode).toBe(1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("apply le rejects invalid --protocol values", async () => {
|
|
46
|
-
await runCliCommand([
|
|
47
|
-
"apply",
|
|
48
|
-
"le",
|
|
49
|
-
"--template",
|
|
50
|
-
"smart-factory",
|
|
51
|
-
"--source-hostname",
|
|
52
|
-
"10.0.0.1",
|
|
53
|
-
"--le-hostname",
|
|
54
|
-
"le.example.local",
|
|
55
|
-
"--le-api-token",
|
|
56
|
-
"token",
|
|
57
|
-
"--protocol",
|
|
58
|
-
"http",
|
|
59
|
-
]);
|
|
60
|
-
|
|
61
|
-
expect(process.exitCode).toBe(1);
|
|
62
|
-
});
|