@rainbow-robotics/muscat-plugin-kit 1.3.7 → 1.3.8-dev.1
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/README.md +6 -6
- package/cli/README.md +11 -5
- package/cli/bin/muscat-pk.mjs +1 -1
- package/cli/bundle-templates.mjs +0 -2
- package/cli/docker/Dockerfile.cpp +6 -22
- package/cli/docker/Dockerfile.dev +6 -22
- package/cli/docker/Dockerfile.python +0 -18
- package/cli/src/build.mjs +5 -5
- package/cli/src/completion.mjs +1 -1
- package/cli/src/cpp.mjs +11 -3
- package/cli/src/cppIde.mjs +75 -0
- package/cli/src/dev.mjs +19 -26
- package/cli/src/devBack.mjs +29 -22
- package/cli/src/devUi.mjs +10 -1
- package/cli/src/muscatLocal.mjs +198 -0
- package/cli/src/new.mjs +15 -0
- package/cli/src/ports.mjs +1 -1
- package/cli/src/ros2.mjs +11 -3
- package/cli/src/sdk.mjs +48 -2
- package/cli/src/type.mjs +2 -0
- package/cli/src/util.mjs +46 -3
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-CdTgUqrV.js → Color-YHDXOIA2-DeirXVfl.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CbRKwHpQ.js → Docs-BLxyi8cP.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CsxMprX5.js → Docs-Buyf7Djy.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-FyPFrbUS.js → Docs-BxmwdaYa.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-C8wx5zsC.js → Docs-CnfIOwf8.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cf0s04QW.js → Docs-NrBqEYvM.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-YtqU-aVu.js → Docs-wzFMK-XA.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-VSENNRoY.js → DocsRenderer-CFRXHY34-CWc43rTP.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-DfL0kqmN.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-WxSc_gsR.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-C1D3oUiY.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-C1Qa88rn.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-CmteJX3t.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-B7E-mhyI.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-De67vMdU.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-DvCV1MGk.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-DuHDZ9-v.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BT1o0ngg.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CSLBT83Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-QavkYgV0.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-C0RPGnO7.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-BUCAT0Sd.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-CnBXTQ4J.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-5FbsZcdy.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-FwTKjtCi.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-C0y5psMI.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-sJjAZBVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-COJqEkDr.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-BnXj-Woa.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-DE1yl3rz.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-CYASERLQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-BYkWjdMG.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-XLLTJZRf.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-DpVylrQl.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-DcPRAan8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-NOTo-jJG.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-CUZaQyw8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-5UC0ZA3H.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-BIRNPjwt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-CJWeIQTt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-Cu0txCUp.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-DnGhYrxa.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BlfvKcGb.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CFwmlhAE.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-wMrFLJ7u.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-Bu5wtV-T.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-Db1d940z.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-NCpjg-2W.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-CqsM8byU.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-TMQEaHk-.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-pnsBvtzU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-BMDBfq82.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-COOQ_HNF.js → RBRangeDateInput.stories-ljAsdz0Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-B5YDW687.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-BookV8Sm.js → RBSelectBox.stories-DPtyIwEr.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-DMTeF_hx.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DLsS3mzL.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-BjTYZ3ri.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-BWBvOGUY.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-tpthe98k.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-BijmaLIo.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-Dojsz28q.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-CQajMiEa.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-BslRXoCw.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-N44Q45Xp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-scpiy0Kx.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-D_suHJaA.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-B73JQcKo.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-CmujM833.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-DhCAfiKu.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-BixEjKZy.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-B5_xLK8z.js → SideNumberInput-B7fbx2jk.js} +83 -83
- package/cli/storybook-dist/assets/{entry-preview-raBQdKjS.js → entry-preview-BeR5Td7C.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BmIue__l.js → iframe-D3pKf_6w.js} +2 -2
- package/cli/storybook-dist/assets/{index-CSIe-YDr.js → index-gXlU0WyC.js} +3 -3
- package/cli/storybook-dist/assets/preview-BYhMD55w.css +1 -0
- package/cli/storybook-dist/assets/preview-D-VpQB0_.js +3 -0
- package/cli/storybook-dist/assets/{preview-DD51RTKv.js → preview-GwaHbm9r.js} +2 -2
- package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +1 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +10 -22
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +3 -2
- package/cli/templates/readme-cpp/README.md +18 -23
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +3 -2
- package/package.json +3 -4
- package/cli/docker/Dockerfile.bridge +0 -17
- package/cli/docker/bridge-package.json +0 -8
- package/cli/src/bridge.mjs +0 -152
- package/cli/src/socketBridge.mjs +0 -133
- package/cli/storybook-dist/assets/preview-CUgthyJT.js +0 -3
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels-BaelaOGy.css +0 -1
- /package/cli/storybook-dist/assets/{react-resizable-panels.browser-CYcsrUgg.js → react-resizable-panels.browser-D49JKwKo.js} +0 -0
|
@@ -8,8 +8,9 @@ endif()
|
|
|
8
8
|
find_package(ament_cmake REQUIRED)
|
|
9
9
|
find_package(rclcpp REQUIRED)
|
|
10
10
|
# Rainbow Robotics C++ SDK (apt: rainbow-rb-sdk-dev, pre-installed in the dev
|
|
11
|
-
# container).
|
|
12
|
-
|
|
11
|
+
# container). The version here is the one the dev container installs; change it
|
|
12
|
+
# with `muscat-pk sdk update <version>`.
|
|
13
|
+
find_package(rb_sdk 0.10.2 EXACT REQUIRED)
|
|
13
14
|
|
|
14
15
|
add_executable(__PLUGIN_SLUG___node src/node.cpp)
|
|
15
16
|
ament_target_dependencies(__PLUGIN_SLUG___node rclcpp)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/muscat-plugin-kit",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -53,10 +53,9 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
-
"@rainbow-robotics/plugin-ui": "1.3.
|
|
57
|
-
"@rainbow-robotics/preview-host": "1.3.
|
|
56
|
+
"@rainbow-robotics/plugin-ui": "1.3.8-dev.1",
|
|
57
|
+
"@rainbow-robotics/preview-host": "1.3.8-dev.1",
|
|
58
58
|
"postcss": "^8.5.8",
|
|
59
|
-
"socket.io": "^4.8.1",
|
|
60
59
|
"typescript": "^5.8.0",
|
|
61
60
|
"vite-plugin-css-injected-by-js": "^3.5.2",
|
|
62
61
|
"zod": "^3.25.0"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Socket.IO bridge container — the frontend (preview-host) and the backend
|
|
2
|
-
# container rendezvous here during `muscat-pk dev`.
|
|
3
|
-
# - frontend (host) → published port localhost:10000
|
|
4
|
-
# - backend container → shares this container's netns (--network container:),
|
|
5
|
-
# so it reaches the bridge at localhost:10000 too
|
|
6
|
-
# Build context is the `cli/` dir so it can copy src/socketBridge.mjs.
|
|
7
|
-
FROM node:20-slim
|
|
8
|
-
LABEL com.rainbow-robotics.muscat-pk="true"
|
|
9
|
-
|
|
10
|
-
WORKDIR /bridge
|
|
11
|
-
COPY docker/bridge-package.json package.json
|
|
12
|
-
RUN npm install --omit=dev --no-audit --no-fund
|
|
13
|
-
|
|
14
|
-
COPY src/socketBridge.mjs ./socketBridge.mjs
|
|
15
|
-
|
|
16
|
-
EXPOSE 10000
|
|
17
|
-
CMD ["node", "socketBridge.mjs"]
|
package/cli/src/bridge.mjs
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
// Socket.IO bridge container lifecycle for `muscat-pk dev`.
|
|
2
|
-
// The bridge runs in Docker and publishes port 10000 to the host:
|
|
3
|
-
// - frontend (preview-host, on the host) connects to localhost:10000
|
|
4
|
-
// - the backend container joins via `--network container:<bridge>` and reaches
|
|
5
|
-
// it at localhost:10000 too — no host.docker.internal needed (robust cross-OS).
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { createServer, connect } from "node:net";
|
|
8
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { dirname, resolve } from "node:path";
|
|
11
|
-
import { canSkipImageBuild, hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
|
|
12
|
-
import { MEDIA_PORTS, mediaPortFlags } from "./ports.mjs";
|
|
13
|
-
import { usesHostNetwork } from "./zenohDev.mjs";
|
|
14
|
-
|
|
15
|
-
export const BRIDGE_IMAGE = "muscat-pk-bridge";
|
|
16
|
-
export const BRIDGE_CONTAINER = "muscat-pk-bridge-run";
|
|
17
|
-
export const BRIDGE_PORT = 10000;
|
|
18
|
-
|
|
19
|
-
// 컴파일된 바이너리에는 cli/ 트리가 없어 임베드 스냅샷에서 build context 를 tmp 에 복원한다.
|
|
20
|
-
const BRIDGE_CONTEXT_FILES = ["docker/Dockerfile.bridge", "docker/bridge-package.json", "src/socketBridge.mjs"];
|
|
21
|
-
|
|
22
|
-
async function resolveBridgeContext() {
|
|
23
|
-
const context = resolve(kitRoot, "cli");
|
|
24
|
-
const dockerfile = resolve(context, "docker/Dockerfile.bridge");
|
|
25
|
-
if (existsSync(dockerfile)) return { dockerfile, context };
|
|
26
|
-
|
|
27
|
-
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
28
|
-
const dir = resolve(tmpdir(), "muscat-pk-bridge-context");
|
|
29
|
-
for (const key of BRIDGE_CONTEXT_FILES) {
|
|
30
|
-
const b64 = TEMPLATES[key];
|
|
31
|
-
if (!b64) throw new Error(`Embedded file missing: ${key}. Reinstall muscat-plugin-kit.`);
|
|
32
|
-
const file = resolve(dir, key);
|
|
33
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
34
|
-
writeFileSync(file, Buffer.from(b64, "base64"));
|
|
35
|
-
}
|
|
36
|
-
return { dockerfile: resolve(dir, "docker/Dockerfile.bridge"), context: dir };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function assertDockerReady() {
|
|
40
|
-
if (!hasBin("docker")) {
|
|
41
|
-
throw new Error("Docker not found. `muscat-pk dev` runs the Socket.IO bridge in a container.");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Is a host TCP port already bound? (Catches another bridge or a stray process.)
|
|
46
|
-
function isPortInUse(port) {
|
|
47
|
-
return new Promise((res) => {
|
|
48
|
-
const srv = createServer();
|
|
49
|
-
srv.once("error", (err) => res(err.code === "EADDRINUSE"));
|
|
50
|
-
srv.once("listening", () => srv.close(() => res(false)));
|
|
51
|
-
srv.listen(port, "0.0.0.0");
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Wait until the published port accepts a TCP connection (bridge is ready).
|
|
56
|
-
function waitForPort(port, timeoutMs = 15_000) {
|
|
57
|
-
const start = Date.now();
|
|
58
|
-
return new Promise((res, rej) => {
|
|
59
|
-
const tryOnce = () => {
|
|
60
|
-
const sock = connect(port, "127.0.0.1");
|
|
61
|
-
sock.once("connect", () => { sock.destroy(); res(); });
|
|
62
|
-
sock.once("error", () => {
|
|
63
|
-
sock.destroy();
|
|
64
|
-
if (Date.now() - start > timeoutMs) rej(new Error("bridge container did not become ready in time"));
|
|
65
|
-
else setTimeout(tryOnce, 300);
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
tryOnce();
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function ensureImage() {
|
|
73
|
-
if (canSkipImageBuild(BRIDGE_IMAGE)) {
|
|
74
|
-
log.step(`${BRIDGE_IMAGE} already built — skipping docker build`);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const { dockerfile, context } = await resolveBridgeContext();
|
|
78
|
-
log.step(`docker build -t ${BRIDGE_IMAGE} (first run is slow, then cached)…`);
|
|
79
|
-
const r = spawnSync("docker", ["build", "-t", BRIDGE_IMAGE, "-f", dockerfile, context], { stdio: "inherit" });
|
|
80
|
-
if (r.status !== 0) throw new Error("docker build failed (bridge image).");
|
|
81
|
-
pruneMuskatImages();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** A running Muscat container (e.g. muscat-dev) that owns host port 10000, or null. */
|
|
85
|
-
export function localMuscatContainer() {
|
|
86
|
-
const r = spawnSync("docker", ["ps", "--filter", `publish=${BRIDGE_PORT}`, "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
87
|
-
return (r.stdout || "").split("\n").map((n) => n.trim()).find((n) => n && n !== BRIDGE_CONTAINER) ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Build the image, preflight the port, and start the bridge container.
|
|
92
|
-
* Returns false if port 10000 was already in use (caller decides), else { media }:
|
|
93
|
-
* whether the MediaMTX ports are free for the backend's own MediaMTX.
|
|
94
|
-
*/
|
|
95
|
-
export async function startBridge({ zenohPort = null, pluginPorts = [] } = {}) {
|
|
96
|
-
assertDockerReady();
|
|
97
|
-
|
|
98
|
-
// remove our own stale container first, then check the host port is free
|
|
99
|
-
spawnSync("docker", ["rm", "-f", BRIDGE_CONTAINER], { stdio: "ignore" });
|
|
100
|
-
if (await isPortInUse(BRIDGE_PORT)) return false;
|
|
101
|
-
|
|
102
|
-
// Publishing a busy port makes `docker run` fail with a raw daemon error —
|
|
103
|
-
// say which port and keep the bridge up instead.
|
|
104
|
-
const busy = [];
|
|
105
|
-
for (const port of pluginPorts) {
|
|
106
|
-
if (await isPortInUse(port)) busy.push(port);
|
|
107
|
-
}
|
|
108
|
-
if (busy.length > 0) {
|
|
109
|
-
log.warn(
|
|
110
|
-
`declared plugin port(s) already in use on the host: ${busy.join(", ")} — ` +
|
|
111
|
-
`not published (the backend still listens inside the container).`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
const publishable = pluginPorts.filter((port) => !busy.includes(port));
|
|
115
|
-
|
|
116
|
-
const mediaBusy = [];
|
|
117
|
-
for (const port of MEDIA_PORTS) {
|
|
118
|
-
if (await isPortInUse(port)) mediaBusy.push(port);
|
|
119
|
-
}
|
|
120
|
-
const media = mediaBusy.length === 0;
|
|
121
|
-
if (!media) log.warn(`MediaMTX port(s) already in use: ${mediaBusy.join(", ")} — video streaming disabled in dev.`);
|
|
122
|
-
|
|
123
|
-
await ensureImage();
|
|
124
|
-
|
|
125
|
-
log.step("starting Socket.IO bridge container…");
|
|
126
|
-
// The backend joins THIS container's netns (--network container:), and a
|
|
127
|
-
// netns can only publish ports when its owner starts — so the plugin's
|
|
128
|
-
// declared ports have to be published here, not on the backend container.
|
|
129
|
-
const portFlags = [
|
|
130
|
-
"-p", `127.0.0.1:${BRIDGE_PORT}:${BRIDGE_PORT}`,
|
|
131
|
-
...(zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : []),
|
|
132
|
-
...publishable.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]),
|
|
133
|
-
...(media && !usesHostNetwork() ? mediaPortFlags() : []),
|
|
134
|
-
];
|
|
135
|
-
const up = spawnSync(
|
|
136
|
-
"docker",
|
|
137
|
-
["run", "-d", "--name", BRIDGE_CONTAINER, ...portFlags, BRIDGE_IMAGE],
|
|
138
|
-
{ stdio: "inherit" },
|
|
139
|
-
);
|
|
140
|
-
if (up.status !== 0) throw new Error("failed to start bridge container.");
|
|
141
|
-
|
|
142
|
-
await waitForPort(BRIDGE_PORT);
|
|
143
|
-
log.step(`Socket.IO bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
144
|
-
if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
|
|
145
|
-
if (publishable.length > 0)
|
|
146
|
-
log.step(`plugin backend ports published on 127.0.0.1: ${publishable.join(", ")}`);
|
|
147
|
-
return { media };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function stopBridge() {
|
|
151
|
-
spawnSync("docker", ["rm", "-f", BRIDGE_CONTAINER], { stdio: "ignore" });
|
|
152
|
-
}
|
package/cli/src/socketBridge.mjs
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { Server } from "socket.io";
|
|
5
|
-
|
|
6
|
-
// Self-contained logger — this module also runs standalone inside the bridge
|
|
7
|
-
// Docker container, where ./util.mjs is not available.
|
|
8
|
-
const log = {
|
|
9
|
-
step: (...m) => console.log("›", ...m),
|
|
10
|
-
warn: (...m) => console.warn("⚠", ...m),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const DEFAULT_ACTION_TIMEOUT_MS = 10_000;
|
|
14
|
-
|
|
15
|
-
export function startSocketBridge({ port = 10000, host = "0.0.0.0" } = {}) {
|
|
16
|
-
const httpServer = createServer();
|
|
17
|
-
const io = new Server(httpServer, { cors: { origin: "*" } });
|
|
18
|
-
|
|
19
|
-
// topic → socketId (serve_register로 등록된 queryable)
|
|
20
|
-
const queryables = new Map();
|
|
21
|
-
// call_id → { timer, resolve } (action → serve_call → serve_reply 대기)
|
|
22
|
-
const pending = new Map();
|
|
23
|
-
|
|
24
|
-
function cleanupSocket(socket) {
|
|
25
|
-
for (const [topic, sid] of queryables) {
|
|
26
|
-
if (sid === socket.id) queryables.delete(topic);
|
|
27
|
-
}
|
|
28
|
-
for (const [id, p] of pending) {
|
|
29
|
-
if (p.sid !== socket.id) continue;
|
|
30
|
-
clearTimeout(p.timer);
|
|
31
|
-
p.resolve(undefined);
|
|
32
|
-
pending.delete(id);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
io.on("connection", (socket) => {
|
|
37
|
-
socket.on("sub", async (data = {}) => {
|
|
38
|
-
if (!data.topic) return;
|
|
39
|
-
await socket.join(`t:${data.topic}`);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
socket.on("unsub", async (data = {}) => {
|
|
43
|
-
if (!data.topic) return;
|
|
44
|
-
await socket.leave(`t:${data.topic}`);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
socket.on("pub", (data = {}) => {
|
|
48
|
-
const { topic, payload } = data;
|
|
49
|
-
if (!topic) return;
|
|
50
|
-
io.to(`t:${topic}`).emit("msg", { topic, payload });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
socket.on("serve_register", (data = {}) => {
|
|
54
|
-
if (!data.topic) return;
|
|
55
|
-
queryables.set(data.topic, socket.id);
|
|
56
|
-
log.step(`serve registered: ${data.topic}`);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
socket.on("serve_unregister", (data = {}) => {
|
|
60
|
-
if (queryables.get(data.topic) === socket.id) queryables.delete(data.topic);
|
|
61
|
-
if (data.topic) log.step(`serve unregistered: ${data.topic}`);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
socket.on("serve_reply", (data = {}) => {
|
|
65
|
-
const p = pending.get(data.id);
|
|
66
|
-
if (!p) return;
|
|
67
|
-
clearTimeout(p.timer);
|
|
68
|
-
p.resolve(data.payload);
|
|
69
|
-
pending.delete(data.id);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
socket.on("action", (data = {}, ack) => {
|
|
73
|
-
const { topic, payload } = data;
|
|
74
|
-
const targetSid = queryables.get(topic);
|
|
75
|
-
const target = targetSid && io.sockets.sockets.get(targetSid);
|
|
76
|
-
if (!target) {
|
|
77
|
-
log.warn(`action has no handler: ${topic}`);
|
|
78
|
-
if (typeof ack === "function") ack({ error: `No handler registered for ${topic}` });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
log.step(`action: ${topic}`);
|
|
82
|
-
const id = randomUUID();
|
|
83
|
-
const timer = setTimeout(() => {
|
|
84
|
-
pending.delete(id);
|
|
85
|
-
if (typeof ack === "function") ack(undefined);
|
|
86
|
-
}, DEFAULT_ACTION_TIMEOUT_MS);
|
|
87
|
-
pending.set(id, {
|
|
88
|
-
sid: targetSid,
|
|
89
|
-
timer,
|
|
90
|
-
resolve: (v) => { if (typeof ack === "function") ack(v); },
|
|
91
|
-
});
|
|
92
|
-
target.emit("serve_call", { id, topic, payload });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
socket.on("disconnect", () => cleanupSocket(socket));
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return new Promise((resolve) => {
|
|
99
|
-
httpServer.once("error", (err) => {
|
|
100
|
-
if (err.code === "EADDRINUSE") {
|
|
101
|
-
log.warn(`Socket.IO bridge already running on :${port} — skipping`);
|
|
102
|
-
io.close();
|
|
103
|
-
httpServer.close();
|
|
104
|
-
resolve({ close: () => {} });
|
|
105
|
-
} else {
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
httpServer.listen(port, host, () => {
|
|
111
|
-
log.step(`Socket.IO bridge listening on ws://127.0.0.1:${port}`);
|
|
112
|
-
|
|
113
|
-
const close = () => {
|
|
114
|
-
for (const p of pending.values()) {
|
|
115
|
-
clearTimeout(p.timer);
|
|
116
|
-
p.resolve(undefined);
|
|
117
|
-
}
|
|
118
|
-
pending.clear();
|
|
119
|
-
queryables.clear();
|
|
120
|
-
io.close();
|
|
121
|
-
httpServer.close();
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
resolve({ close });
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// When executed directly (inside the bridge Docker container), start the server.
|
|
130
|
-
if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
131
|
-
const port = Number(process.env.PORT) || 10000;
|
|
132
|
-
startSocketBridge({ port });
|
|
133
|
-
}
|