@phystack/device-simulator 6.3.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/CHANGELOG.md +14 -0
- package/build-binary.sh +37 -0
- package/dist/index.js +37 -0
- package/package.json +38 -0
- package/src/__tests__/e2e/binary.e2e.test.ts +394 -0
- package/src/__tests__/preload.ts +44 -0
- package/src/command.ts +66 -0
- package/src/commands/__tests__/run-helpers.test.ts +181 -0
- package/src/commands/list.ts +25 -0
- package/src/commands/remove.ts +16 -0
- package/src/commands/run.ts +518 -0
- package/src/commands/start.ts +309 -0
- package/src/index.ts +45 -0
- package/src/services/__tests__/dev-token.test.ts +156 -0
- package/src/services/dev-token.ts +52 -0
- package/src/services/env.ts +10 -0
- package/src/simulator/__tests__/message-router.test.ts +782 -0
- package/src/simulator/__tests__/twin-cache.test.ts +129 -0
- package/src/simulator/index.ts +200 -0
- package/src/simulator/local-server.ts +184 -0
- package/src/simulator/logger.ts +44 -0
- package/src/simulator/message-router.ts +525 -0
- package/src/simulator/twin-cache.ts +61 -0
- package/src/simulator/types.ts +53 -0
- package/src/utils/__tests__/simulator-config.test.ts +230 -0
- package/src/utils/config-paths.ts +38 -0
- package/src/utils/index.ts +41 -0
- package/src/utils/simulator-config.ts +185 -0
- package/src/utils/tenant-storage.ts +106 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildDockerRunArgs,
|
|
4
|
+
resolveAppType,
|
|
5
|
+
resolveStartCommand,
|
|
6
|
+
} from "../run";
|
|
7
|
+
import { AppTypeEnum } from "../../simulator/types";
|
|
8
|
+
|
|
9
|
+
describe("resolveAppType", () => {
|
|
10
|
+
test("option type takes precedence over package type", () => {
|
|
11
|
+
expect(resolveAppType("screen", "edge")).toBe(AppTypeEnum.Screen);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("falls back to the package application-type when no option", () => {
|
|
15
|
+
expect(resolveAppType(undefined, "edge")).toBe(AppTypeEnum.Edge);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("lowercases the raw value before matching", () => {
|
|
19
|
+
expect(resolveAppType("SCREEN")).toBe(AppTypeEnum.Screen);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("throws when neither option nor package type is provided", () => {
|
|
23
|
+
expect(() => resolveAppType()).toThrow(
|
|
24
|
+
'Cannot detect app type. Set "application-type" in package.json or pass --type.',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("throws on an unsupported type", () => {
|
|
29
|
+
expect(() => resolveAppType("cloud")).toThrow(
|
|
30
|
+
/Unsupported app type "cloud"/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("resolveStartCommand", () => {
|
|
36
|
+
test("explicit --dev-command wins", () => {
|
|
37
|
+
expect(
|
|
38
|
+
resolveStartCommand(
|
|
39
|
+
{ devCommand: "bun run dev" },
|
|
40
|
+
{},
|
|
41
|
+
AppTypeEnum.Screen,
|
|
42
|
+
),
|
|
43
|
+
).toBe("bun run dev");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("uses `npm start` when package.json has a start script", () => {
|
|
47
|
+
expect(
|
|
48
|
+
resolveStartCommand(
|
|
49
|
+
{},
|
|
50
|
+
{ scripts: { start: "node index.js" } },
|
|
51
|
+
AppTypeEnum.Screen,
|
|
52
|
+
),
|
|
53
|
+
).toBe("npm start");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("edge app without a start script returns undefined (Docker path)", () => {
|
|
57
|
+
expect(resolveStartCommand({}, {}, AppTypeEnum.Edge)).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("screen app without a start script throws", () => {
|
|
61
|
+
expect(() => resolveStartCommand({}, {}, AppTypeEnum.Screen)).toThrow(
|
|
62
|
+
"No start script found in package.json. Pass --dev-command to specify one.",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("buildDockerRunArgs", () => {
|
|
68
|
+
const TWIN_ID = "twin-xyz";
|
|
69
|
+
const IMAGE = "my-image:1.0.0";
|
|
70
|
+
|
|
71
|
+
test("null config: base args, TWIN_ID + bridge-mode host mapping, image last", () => {
|
|
72
|
+
const args = buildDockerRunArgs(null, IMAGE, TWIN_ID);
|
|
73
|
+
|
|
74
|
+
expect(args[0]).toBe("run");
|
|
75
|
+
expect(args).toContain("--rm");
|
|
76
|
+
expect(args[args.length - 1]).toBe(IMAGE);
|
|
77
|
+
|
|
78
|
+
const twinEnvIndex = args.indexOf("TWIN_ID=twin-xyz");
|
|
79
|
+
expect(twinEnvIndex).toBeGreaterThan(-1);
|
|
80
|
+
expect(args[twinEnvIndex - 1]).toBe("-e");
|
|
81
|
+
|
|
82
|
+
// Bridge mode (no host network): host.docker.internal + phyos→host-gateway
|
|
83
|
+
expect(args).toContain("--add-host=host.docker.internal:host-gateway");
|
|
84
|
+
const phyosIndex = args.indexOf("phyos:host-gateway");
|
|
85
|
+
expect(phyosIndex).toBeGreaterThan(-1);
|
|
86
|
+
expect(args[phyosIndex - 1]).toBe("--add-host");
|
|
87
|
+
|
|
88
|
+
expect(args).toContain("-e");
|
|
89
|
+
expect(args).toContain(
|
|
90
|
+
"PHYSTACK_SIMULATOR_URL=http://host.docker.internal:55000",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("host network mode switches the host mappings", () => {
|
|
95
|
+
const args = buildDockerRunArgs(
|
|
96
|
+
{ HostConfig: { NetworkMode: "host" } },
|
|
97
|
+
IMAGE,
|
|
98
|
+
TWIN_ID,
|
|
99
|
+
);
|
|
100
|
+
expect(args).toContain("--network");
|
|
101
|
+
expect(args).toContain("host");
|
|
102
|
+
expect(args).toContain("phyos:127.0.0.1");
|
|
103
|
+
expect(args).not.toContain("--add-host=host.docker.internal:host-gateway");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("custom Env, Entrypoint, port bindings, binds and Cmd are translated", () => {
|
|
107
|
+
const config = {
|
|
108
|
+
Env: ["FOO=bar", "BAZ=qux"],
|
|
109
|
+
Entrypoint: ["/entry.sh"],
|
|
110
|
+
Cmd: ["--flag", "value"],
|
|
111
|
+
HostConfig: {
|
|
112
|
+
PortBindings: { "8080/tcp": [{ HostPort: "9090" }] },
|
|
113
|
+
Binds: ["/host/path:/container/path"],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const args = buildDockerRunArgs(config, IMAGE, TWIN_ID);
|
|
117
|
+
|
|
118
|
+
expect(args).toContain("FOO=bar");
|
|
119
|
+
expect(args).toContain("BAZ=qux");
|
|
120
|
+
|
|
121
|
+
const entrypointIndex = args.indexOf("--entrypoint");
|
|
122
|
+
expect(entrypointIndex).toBeGreaterThan(-1);
|
|
123
|
+
expect(args[entrypointIndex + 1]).toBe("/entry.sh");
|
|
124
|
+
|
|
125
|
+
const portIndex = args.indexOf("-p");
|
|
126
|
+
expect(portIndex).toBeGreaterThan(-1);
|
|
127
|
+
expect(args).toContain("9090:8080");
|
|
128
|
+
|
|
129
|
+
const volIndex = args.indexOf("-v");
|
|
130
|
+
expect(args[volIndex + 1]).toBe("/host/path:/container/path");
|
|
131
|
+
|
|
132
|
+
// Cmd is appended AFTER the image.
|
|
133
|
+
const imageIndex = args.indexOf(IMAGE);
|
|
134
|
+
expect(args.slice(imageIndex + 1)).toEqual(["--flag", "value"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("resource limits, privileged, readonly, ulimits, gpu device requests", () => {
|
|
138
|
+
const config = {
|
|
139
|
+
HostConfig: {
|
|
140
|
+
Privileged: true,
|
|
141
|
+
ReadonlyRootfs: true,
|
|
142
|
+
Memory: 536870912,
|
|
143
|
+
CpuShares: 512,
|
|
144
|
+
Ulimits: [{ Name: "nofile", Soft: 1024, Hard: 2048 }],
|
|
145
|
+
Devices: [{ PathOnHost: "/dev/ttyUSB0" }],
|
|
146
|
+
DeviceRequests: [{ Capabilities: [["gpu"]] }],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const args = buildDockerRunArgs(config, IMAGE, TWIN_ID);
|
|
150
|
+
|
|
151
|
+
expect(args).toContain("--privileged");
|
|
152
|
+
expect(args).toContain("--read-only");
|
|
153
|
+
|
|
154
|
+
const memIndex = args.indexOf("--memory");
|
|
155
|
+
expect(args[memIndex + 1]).toBe("536870912");
|
|
156
|
+
|
|
157
|
+
const cpuIndex = args.indexOf("--cpu-shares");
|
|
158
|
+
expect(args[cpuIndex + 1]).toBe("512");
|
|
159
|
+
|
|
160
|
+
const ulimitIndex = args.indexOf("--ulimit");
|
|
161
|
+
expect(args[ulimitIndex + 1]).toBe("nofile=1024:2048");
|
|
162
|
+
|
|
163
|
+
const deviceIndex = args.indexOf("--device");
|
|
164
|
+
expect(args[deviceIndex + 1]).toBe("/dev/ttyUSB0");
|
|
165
|
+
|
|
166
|
+
expect(args).toContain("--gpus");
|
|
167
|
+
expect(args).toContain("all");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("TZ env is forwarded when set in the environment", () => {
|
|
171
|
+
const originalTz = process.env.TZ;
|
|
172
|
+
process.env.TZ = "Europe/Stockholm";
|
|
173
|
+
try {
|
|
174
|
+
const args = buildDockerRunArgs(null, IMAGE, TWIN_ID);
|
|
175
|
+
expect(args).toContain("TZ=Europe/Stockholm");
|
|
176
|
+
} finally {
|
|
177
|
+
if (originalTz === undefined) delete process.env.TZ;
|
|
178
|
+
else process.env.TZ = originalTz;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { listSimulators } from "../utils/simulator-config";
|
|
2
|
+
|
|
3
|
+
export default function simulatorList(): void {
|
|
4
|
+
const devices = listSimulators();
|
|
5
|
+
|
|
6
|
+
if (devices.length === 0) {
|
|
7
|
+
console.log(
|
|
8
|
+
'No provisioned simulators. Use "phy simulator start --connect <name>" to create one.',
|
|
9
|
+
);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const header = `${"NAME".padEnd(20)} ${"ENV".padEnd(8)} ${"DEVICE ID".padEnd(38)} PROVISIONED`;
|
|
14
|
+
console.log(header);
|
|
15
|
+
console.log("-".repeat(header.length));
|
|
16
|
+
|
|
17
|
+
for (const d of devices) {
|
|
18
|
+
const envLabel =
|
|
19
|
+
d.environment === "prod" || !d.environment ? "" : d.environment;
|
|
20
|
+
const date = d.provisionedAt ? d.provisionedAt.split("T")[0] : "unknown";
|
|
21
|
+
console.log(
|
|
22
|
+
`${d.name.padEnd(20)} ${envLabel.padEnd(8)} ${d.deviceId.padEnd(38)} ${date}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { removeSimulator } from "../utils/simulator-config";
|
|
2
|
+
|
|
3
|
+
export default function simulatorRemove(name: string): void {
|
|
4
|
+
if (!name) {
|
|
5
|
+
console.error("Usage: phy simulator remove <name>");
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const removed = removeSimulator(name);
|
|
10
|
+
if (removed) {
|
|
11
|
+
console.log(`Removed simulator "${name}".`);
|
|
12
|
+
} else {
|
|
13
|
+
console.error(`Simulator "${name}" not found.`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|