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,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon registry — tracks background simulation processes.
|
|
3
|
-
*
|
|
4
|
-
* Each background simulation gets a JSON entry at `~/.miravo/daemon/<name>.json`.
|
|
5
|
-
* Management commands (ps, stop, logs, rm) use this registry to discover and
|
|
6
|
-
* interact with running simulations via their admin HTTP server.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mkdir, open, readdir, rename, unlink } from "node:fs/promises";
|
|
10
|
-
import { resolveMiravoHome } from "@miravo/core";
|
|
11
|
-
import { MiravoError, SimulationNameSchema } from "@miravo/shared";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Schema (CLI-only — not in packages/shared/)
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
const DaemonAdapterEntrySchema = z.object({
|
|
19
|
-
port: z.number().int().positive(),
|
|
20
|
-
host: z.string().min(1),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const DaemonRegistryEntrySchema = z.object({
|
|
24
|
-
name: z.string().min(1),
|
|
25
|
-
pid: z.number().int().positive(),
|
|
26
|
-
adminPort: z.number().int().positive(),
|
|
27
|
-
adminHost: z.string().min(1),
|
|
28
|
-
adapters: z.record(z.string(), DaemonAdapterEntrySchema),
|
|
29
|
-
templatePath: z.string().min(1),
|
|
30
|
-
startedAt: z.string().min(1),
|
|
31
|
-
seed: z.number().finite(),
|
|
32
|
-
runId: z.string().min(1),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
type DaemonAdapterEntry = z.infer<typeof DaemonAdapterEntrySchema>;
|
|
36
|
-
export type DaemonRegistryEntry = z.infer<typeof DaemonRegistryEntrySchema>;
|
|
37
|
-
|
|
38
|
-
interface EntryIdentityResult {
|
|
39
|
-
readonly status: "dead" | "verified" | "mismatch" | "unreachable";
|
|
40
|
-
readonly ready: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Directory resolution
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
/** Returns the path to the daemon registry directory (`~/.miravo/daemon/`). */
|
|
48
|
-
function daemonDir(): string {
|
|
49
|
-
return `${resolveMiravoHome()}/daemon`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Validate a simulation name to prevent path traversal.
|
|
54
|
-
* Reuses the shared SimulationNameSchema (lowercase alphanumeric + hyphens).
|
|
55
|
-
*/
|
|
56
|
-
function validateName(name: string): void {
|
|
57
|
-
const result = SimulationNameSchema.safeParse(name);
|
|
58
|
-
if (!result.success) {
|
|
59
|
-
throw new MiravoError(
|
|
60
|
-
`Invalid simulation name: "${name}".`,
|
|
61
|
-
"Use lowercase letters, numbers, and hyphens only (e.g., my-sim-1).",
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function entryPath(name: string): string {
|
|
67
|
-
validateName(name);
|
|
68
|
-
return `${daemonDir()}/${name}.json`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// CRUD operations
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
/** Write a registry entry atomically (temp file + rename). */
|
|
76
|
-
export async function writeEntry(entry: DaemonRegistryEntry): Promise<void> {
|
|
77
|
-
const dir = daemonDir();
|
|
78
|
-
await mkdir(dir, { recursive: true });
|
|
79
|
-
const filePath = entryPath(entry.name);
|
|
80
|
-
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
81
|
-
await Bun.write(tmpPath, JSON.stringify(entry, null, 2));
|
|
82
|
-
await rename(tmpPath, filePath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Acquire an exclusive lock for a simulation name.
|
|
87
|
-
* Prevents concurrent `start -d` from spawning duplicate daemons.
|
|
88
|
-
* Returns a release function; call it when done (even on error).
|
|
89
|
-
*
|
|
90
|
-
* Stale locks from crashed processes are detected via PID liveness and
|
|
91
|
-
* cleaned up automatically before retrying.
|
|
92
|
-
*
|
|
93
|
-
* @throws MiravoError if the name is already locked by a live process.
|
|
94
|
-
*/
|
|
95
|
-
export async function acquireNameLock(name: string): Promise<() => Promise<void>> {
|
|
96
|
-
validateName(name);
|
|
97
|
-
const dir = daemonDir();
|
|
98
|
-
await mkdir(dir, { recursive: true });
|
|
99
|
-
const lockPath = `${dir}/${name}.lock`;
|
|
100
|
-
|
|
101
|
-
const tryAcquire = async (): Promise<void> => {
|
|
102
|
-
try {
|
|
103
|
-
// O_CREAT | O_EXCL — fails if file already exists
|
|
104
|
-
const handle = await open(lockPath, "wx");
|
|
105
|
-
await handle.writeFile(String(process.pid));
|
|
106
|
-
await handle.close();
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
109
|
-
// Check if the lock belongs to a live process or is stale (crashed).
|
|
110
|
-
try {
|
|
111
|
-
const content = await Bun.file(lockPath).text();
|
|
112
|
-
const pid = parseInt(content.trim(), 10);
|
|
113
|
-
if (!Number.isNaN(pid) && !isPidAlive(pid)) {
|
|
114
|
-
// Stale lock from a crashed process — remove and retry.
|
|
115
|
-
await unlink(lockPath);
|
|
116
|
-
return tryAcquire();
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
// Lock file disappeared between EEXIST and reading — retry.
|
|
120
|
-
return tryAcquire();
|
|
121
|
-
}
|
|
122
|
-
throw new MiravoError(
|
|
123
|
-
`Another 'miravo start -d' is already starting simulation "${name}".`,
|
|
124
|
-
"Wait for the other start to complete, or remove the lock with 'miravo rm'.",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
await tryAcquire();
|
|
132
|
-
|
|
133
|
-
return async () => {
|
|
134
|
-
try {
|
|
135
|
-
await unlink(lockPath);
|
|
136
|
-
} catch {
|
|
137
|
-
// Ignore — lock may have been cleaned up already
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Read and validate a registry entry. Returns null if missing or invalid. */
|
|
143
|
-
export async function readEntry(name: string): Promise<DaemonRegistryEntry | null> {
|
|
144
|
-
const filePath = entryPath(name);
|
|
145
|
-
const file = Bun.file(filePath);
|
|
146
|
-
if (!(await file.exists())) return null;
|
|
147
|
-
try {
|
|
148
|
-
const content = await file.text();
|
|
149
|
-
const parsed: unknown = JSON.parse(content);
|
|
150
|
-
const result = DaemonRegistryEntrySchema.safeParse(parsed);
|
|
151
|
-
return result.success ? result.data : null;
|
|
152
|
-
} catch {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Remove a registry entry. Safe if file does not exist. */
|
|
158
|
-
export async function removeEntry(name: string): Promise<void> {
|
|
159
|
-
try {
|
|
160
|
-
await unlink(entryPath(name));
|
|
161
|
-
} catch (error) {
|
|
162
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** List all valid registry entries. Invalid files are silently skipped. */
|
|
167
|
-
export async function listEntries(): Promise<readonly DaemonRegistryEntry[]> {
|
|
168
|
-
const dir = daemonDir();
|
|
169
|
-
let files: string[];
|
|
170
|
-
try {
|
|
171
|
-
files = await readdir(dir);
|
|
172
|
-
} catch (error) {
|
|
173
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const entries: DaemonRegistryEntry[] = [];
|
|
178
|
-
for (const file of files) {
|
|
179
|
-
if (!file.endsWith(".json") || file.endsWith(".tmp")) continue;
|
|
180
|
-
const name = file.slice(0, -5); // strip .json
|
|
181
|
-
const entry = await readEntry(name);
|
|
182
|
-
if (entry) entries.push(entry);
|
|
183
|
-
}
|
|
184
|
-
return entries;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Liveness checks
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
/** Check if a PID is alive using signal 0. */
|
|
192
|
-
export function isPidAlive(pid: number): boolean {
|
|
193
|
-
try {
|
|
194
|
-
process.kill(pid, 0);
|
|
195
|
-
return true;
|
|
196
|
-
} catch (err) {
|
|
197
|
-
if (
|
|
198
|
-
err &&
|
|
199
|
-
typeof err === "object" &&
|
|
200
|
-
"code" in err &&
|
|
201
|
-
(err.code === "EPERM" || err.code === "EACCES")
|
|
202
|
-
) {
|
|
203
|
-
return true; // process exists but we can't signal it
|
|
204
|
-
}
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Verify that a registry entry corresponds to a live Miravo simulation.
|
|
211
|
-
*
|
|
212
|
-
* Two-step check:
|
|
213
|
-
* 1. PID alive (fast, avoids network I/O for dead processes)
|
|
214
|
-
* 2. RunId matches via GET /readyz (guards against PID reuse after reboot)
|
|
215
|
-
*/
|
|
216
|
-
export async function probeEntryIdentity(entry: DaemonRegistryEntry): Promise<EntryIdentityResult> {
|
|
217
|
-
if (!isPidAlive(entry.pid)) {
|
|
218
|
-
return { status: "dead", ready: false };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const url = `http://${entry.adminHost}:${entry.adminPort}/readyz`;
|
|
223
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
|
|
224
|
-
const bodyText = await response.text();
|
|
225
|
-
let body: unknown;
|
|
226
|
-
try {
|
|
227
|
-
body = JSON.parse(bodyText) as unknown;
|
|
228
|
-
} catch {
|
|
229
|
-
return { status: "mismatch", ready: false };
|
|
230
|
-
}
|
|
231
|
-
if (
|
|
232
|
-
typeof body !== "object" ||
|
|
233
|
-
body === null ||
|
|
234
|
-
!("runId" in body) ||
|
|
235
|
-
typeof body["runId"] !== "string"
|
|
236
|
-
) {
|
|
237
|
-
return { status: "mismatch", ready: false };
|
|
238
|
-
}
|
|
239
|
-
if (body["runId"] !== entry.runId) {
|
|
240
|
-
return { status: "mismatch", ready: false };
|
|
241
|
-
}
|
|
242
|
-
return { status: "verified", ready: response.ok };
|
|
243
|
-
} catch {
|
|
244
|
-
// Keep the registry entry when the admin server is temporarily unreachable,
|
|
245
|
-
// but do not treat the identity as safe for raw signal delivery.
|
|
246
|
-
return { status: "unreachable", ready: false };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Remove all registry entries whose processes are no longer alive.
|
|
252
|
-
* Uses full identity verification (PID + runId) to handle PID reuse.
|
|
253
|
-
*/
|
|
254
|
-
export async function sweepStaleEntries(): Promise<void> {
|
|
255
|
-
const entries = await listEntries();
|
|
256
|
-
for (const entry of entries) {
|
|
257
|
-
const identity = await probeEntryIdentity(entry);
|
|
258
|
-
if (identity.status === "dead" || identity.status === "mismatch") {
|
|
259
|
-
await removeEntry(entry.name);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
// Conflict detection
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Check for port conflicts against live registry entries.
|
|
270
|
-
*
|
|
271
|
-
* @param ports - Map of label → port to check (e.g., "admin" → 8080, "mqtt" → 1883)
|
|
272
|
-
* @param excludeName - Optional entry name to exclude (for re-checking own entry)
|
|
273
|
-
* @returns Array of human-readable conflict messages (empty = no conflicts)
|
|
274
|
-
*/
|
|
275
|
-
export async function checkPortConflicts(
|
|
276
|
-
ports: ReadonlyMap<string, number>,
|
|
277
|
-
excludeName?: string,
|
|
278
|
-
): Promise<readonly string[]> {
|
|
279
|
-
const entries = await listEntries();
|
|
280
|
-
const conflicts: string[] = [];
|
|
281
|
-
|
|
282
|
-
for (const entry of entries) {
|
|
283
|
-
if (entry.name === excludeName) continue;
|
|
284
|
-
// Use PID check only (not full identity verification) for performance —
|
|
285
|
-
// port conflicts are about actual port usage, not Miravo identity.
|
|
286
|
-
if (!isPidAlive(entry.pid)) continue;
|
|
287
|
-
|
|
288
|
-
// Check admin port
|
|
289
|
-
for (const [label, port] of ports) {
|
|
290
|
-
if (port === entry.adminPort) {
|
|
291
|
-
conflicts.push(
|
|
292
|
-
`Port ${port} (${label}) is already in use by simulation "${entry.name}". Stop it first: miravo stop ${entry.name}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
// Check adapter ports
|
|
296
|
-
for (const [adapterName, adapter] of Object.entries(entry.adapters) as [
|
|
297
|
-
string,
|
|
298
|
-
DaemonAdapterEntry,
|
|
299
|
-
][]) {
|
|
300
|
-
if (port === adapter.port) {
|
|
301
|
-
conflicts.push(
|
|
302
|
-
`Port ${port} (${label}) conflicts with ${adapterName} port of simulation "${entry.name}". Stop it first: miravo stop ${entry.name}`,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return conflicts;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Check if a simulation name is already in use by a live entry.
|
|
314
|
-
* Removes stale entries as a side effect.
|
|
315
|
-
*
|
|
316
|
-
* @returns The alive entry if the name is taken, null otherwise.
|
|
317
|
-
*/
|
|
318
|
-
export async function checkNameConflict(name: string): Promise<DaemonRegistryEntry | null> {
|
|
319
|
-
const entry = await readEntry(name);
|
|
320
|
-
if (!entry) return null;
|
|
321
|
-
|
|
322
|
-
const identity = await probeEntryIdentity(entry);
|
|
323
|
-
if (identity.status === "verified" || identity.status === "unreachable") return entry;
|
|
324
|
-
|
|
325
|
-
if (identity.status === "dead" || identity.status === "mismatch") {
|
|
326
|
-
// Stale entry — clean it up
|
|
327
|
-
await removeEntry(name);
|
|
328
|
-
}
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
// Display helpers
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Map bind addresses to connectable addresses for display.
|
|
338
|
-
* `0.0.0.0` and `::` are replaced with `127.0.0.1`.
|
|
339
|
-
*/
|
|
340
|
-
export function displayHost(host: string): string {
|
|
341
|
-
if (host === "0.0.0.0" || host === "::") return "127.0.0.1";
|
|
342
|
-
return host;
|
|
343
|
-
}
|