@rainbow-robotics/muscat-plugin-kit 1.3.8-dev.2 → 1.10.1-dev.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/README.md +282 -282
- package/cli/README.md +274 -274
- package/cli/bin/create-muscat-plugin.mjs +8 -8
- package/cli/bin/muscat-pk.mjs +155 -153
- package/cli/bundle-templates.mjs +31 -31
- package/cli/docker/Dockerfile.cpp +28 -28
- package/cli/docker/Dockerfile.dev +30 -30
- package/cli/docker/Dockerfile.python +21 -21
- package/cli/m5-e2e.mjs +69 -69
- package/cli/prepack.mjs +10 -10
- package/cli/src/ai-rules.mjs +70 -0
- package/cli/src/build.mjs +447 -437
- package/cli/src/check-package.mjs +13 -13
- package/cli/src/completion.mjs +166 -166
- package/cli/src/cpp.mjs +78 -79
- package/cli/src/cppIde.mjs +75 -75
- package/cli/src/dev.mjs +165 -164
- package/cli/src/devBack.mjs +727 -670
- package/cli/src/devUi.mjs +243 -238
- package/cli/src/esbuild.mjs +56 -56
- package/cli/src/host_agent.py +223 -222
- package/cli/src/manifest.mjs +287 -283
- package/cli/src/mcp.mjs +1071 -1016
- package/cli/src/muscatLocal.mjs +209 -198
- package/cli/src/new.mjs +343 -338
- package/cli/src/pluginDevServer.mjs +111 -112
- package/cli/src/ports.mjs +193 -193
- package/cli/src/python.mjs +112 -113
- package/cli/src/ros2.mjs +109 -110
- package/cli/src/sdk.mjs +117 -117
- package/cli/src/storybook.mjs +168 -168
- package/cli/src/type.mjs +141 -141
- package/cli/src/uninstall.mjs +81 -81
- package/cli/src/update.mjs +295 -295
- package/cli/src/util.mjs +441 -437
- package/cli/src/zenohDev.mjs +81 -81
- package/cli/storybook-dist/_design-tokens.css +4957 -4957
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-DeirXVfl.js → Color-YHDXOIA2-D6JqZzDJ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BLxyi8cP.js → Docs-6gD2kUo1.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-NrBqEYvM.js → Docs-B9wd8Ok7.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-wzFMK-XA.js → Docs-CMjovnSY.js} +4 -4
- package/cli/storybook-dist/assets/{Docs-Buyf7Djy.js → Docs-CgFbWWXM.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-CnfIOwf8.js → Docs-DBQup8Bu.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-BxmwdaYa.js → Docs-ESb7JAHg.js} +3 -3
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-CWc43rTP.js → DocsRenderer-CFRXHY34-BOUI1AHi.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-DfL0kqmN.js → NxAvatar.stories-DsYFMUh3.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-WxSc_gsR.js → NxButton.stories-CHfNcqsc.js} +8 -8
- package/cli/storybook-dist/assets/{NxNumberInput.stories-C1D3oUiY.js → NxNumberInput.stories-BFw_4vFE.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-C1Qa88rn.js → NxPagination.stories-DVfxyw4M.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-CmteJX3t.js → NxPointerHoldButton.stories-BzyxUWoZ.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-B7E-mhyI.js → NxRangeInput.stories-DmRdzBc1.js} +6 -6
- package/cli/storybook-dist/assets/{NxSelectBox.stories-De67vMdU.js → NxSelectBox.stories-B1KkNUOf.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-DvCV1MGk.js → NxSlideRange.stories-CyOdvfQ6.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DuHDZ9-v.js → NxSwitch.stories-DrDBw0qp.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-BT1o0ngg.js → NxTextArea.stories-DTB2aqNS.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-CSLBT83Q.js → NxTextInput.stories-DU0rucmM.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-QavkYgV0.js → RBAccordion.stories-DTfOtXtp.js} +3 -3
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-C0RPGnO7.js → RBAccordionGroup.stories-9y9BjrUe.js} +10 -10
- package/cli/storybook-dist/assets/{RBAlert.stories-BUCAT0Sd.js → RBAlert.stories-DLTAv9fT.js} +3 -3
- package/cli/storybook-dist/assets/{RBBadge.stories-CnBXTQ4J.js → RBBadge.stories-BNMACewA.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-5FbsZcdy.js → RBBattery.stories-B3kXMAt5.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-FwTKjtCi.js → RBCard.stories-Y-s8R2n7.js} +39 -39
- package/cli/storybook-dist/assets/{RBCheckBox.stories-C0y5psMI.js → RBCheckBox.stories-DBuT9tL4.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-sJjAZBVh.js → RBCheckIconButton.stories-CWu1TL56.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-COJqEkDr.js → RBCheckLabelButton.stories-sCu2MGCX.js} +17 -17
- package/cli/storybook-dist/assets/{RBChipTab.stories-BnXj-Woa.js → RBChipTab.stories-BX9qLJkR.js} +5 -5
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-DE1yl3rz.js → RBChipsBoxTab.stories-35u1aJ-P.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-CYASERLQ.js → RBColorIcons.stories-DAOZ5Ies.js} +1 -1
- package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +73 -0
- package/cli/storybook-dist/assets/{RBConfirm.stories-XLLTJZRf.js → RBConfirm.stories-iMXcUh7Z.js} +3 -3
- package/cli/storybook-dist/assets/{RBDateInput.stories-DpVylrQl.js → RBDateInput.stories-hK5rXECf.js} +1 -1
- package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +38 -0
- package/cli/storybook-dist/assets/{RBDivider.stories-NOTo-jJG.js → RBDivider.stories-BAK-iZ1Z.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-CUZaQyw8.js → RBDrawer.stories-B2nqf2Nh.js} +12 -12
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +86 -0
- package/cli/storybook-dist/assets/{RBEmptyView.stories-BIRNPjwt.js → RBEmptyView.stories-nCenrgBh.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-CJWeIQTt.js → RBIconButton.stories-DRH7O3sp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-Cu0txCUp.js → RBIcons.stories-D3PyJXG8.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DnGhYrxa.js → RBInlineLabelButton.stories-modQbt10.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BlfvKcGb.js → RBJoyStick.stories-BY14qVgR.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-CFwmlhAE.js → RBLabelButton.stories-DzF7vGwm.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-wMrFLJ7u.js → RBLight.stories-lF4FRmVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-Bu5wtV-T.js → RBMenubar.stories-XhrI7bsV.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-Db1d940z.js → RBMultiRangeInput.stories-DprllQDl.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-NCpjg-2W.js → RBPagination.stories-2v2gBJfC.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-CqsM8byU.js → RBPopover.stories-C8iLIZpv.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-TMQEaHk-.js → RBPrompt.stories-Th_HrWHI.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-pnsBvtzU.js → RBRadioBox.stories-CoHuLaN3.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-BMDBfq82.js → RBRadioButton.stories-DULxE_ff.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-ljAsdz0Q.js → RBRangeDateInput.stories-D8A9KBSS.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-B5YDW687.js → RBResizable.stories-C41QfrQ3.js} +14 -14
- package/cli/storybook-dist/assets/{RBSelectBox.stories-DPtyIwEr.js → RBSelectBox.stories-C1gNmwzw.js} +1 -1
- package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +39 -0
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DLsS3mzL.js → RBSideNumberInput.stories-9LlgQ8nh.js} +3 -3
- package/cli/storybook-dist/assets/{RBSlider.stories-BjTYZ3ri.js → RBSlider.stories-oUgo5K9d.js} +3 -3
- package/cli/storybook-dist/assets/{RBSpinner.stories-BWBvOGUY.js → RBSpinner.stories-DDpohbY2.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-tpthe98k.js → RBSwitch.stories-gW-GN7QO.js} +7 -7
- package/cli/storybook-dist/assets/{RBTab.stories-BijmaLIo.js → RBTab.stories-CN-PV3HH.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-Dojsz28q.js → RBTable.stories-BAZLpxtM.js} +29 -29
- package/cli/storybook-dist/assets/{RBTableRow.stories-CQajMiEa.js → RBTableRow.stories-CwjVrszB.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-BslRXoCw.js → RBTabs.stories-JuEo2sUL.js} +7 -7
- package/cli/storybook-dist/assets/{RBTextArea.stories-N44Q45Xp.js → RBTextArea.stories-D0_Pv4qZ.js} +3 -3
- package/cli/storybook-dist/assets/{RBTextField.stories-scpiy0Kx.js → RBTextField.stories-CCsOvZqV.js} +15 -15
- package/cli/storybook-dist/assets/{RBTextInput.stories-D_suHJaA.js → RBTextInput.stories-Cccn3VNm.js} +3 -3
- package/cli/storybook-dist/assets/{RBToast.stories-B73JQcKo.js → RBToast.stories-I5617t0K.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-CmujM833.js → RBToggle.stories-DsgKjGsJ.js} +4 -4
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-DhCAfiKu.js → RBToggleGroup.stories-BZEzSDuz.js} +5 -5
- package/cli/storybook-dist/assets/{RBTooltip.stories-BixEjKZy.js → RBTooltip.stories-4oSyAWsA.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-B7fbx2jk.js → SideNumberInput-DSKbd0bG.js} +743 -743
- package/cli/storybook-dist/assets/{entry-preview-BeR5Td7C.js → entry-preview-B35XREGs.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-D3pKf_6w.js → iframe-BB797lmL.js} +2 -2
- package/cli/storybook-dist/assets/{index-gXlU0WyC.js → index-BU2567EP.js} +3 -3
- package/cli/storybook-dist/assets/{preview-GwaHbm9r.js → preview-C7DaSWyc.js} +2 -2
- package/cli/storybook-dist/assets/preview-CbuRHit4.js +3 -0
- package/cli/storybook-dist/assets/preview-CigNHo-I.css +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels-Dp9zi-o3.css +1 -0
- package/cli/storybook-dist/assets/{react-resizable-panels.browser-D49JKwKo.js → react-resizable-panels.browser-ChIvlwo7.js} +8 -8
- package/cli/storybook-dist/iframe.html +2 -2
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/.mcp.json +8 -8
- package/cli/templates/common/README.md +1231 -1226
- package/cli/templates/common/assets/icon.svg +45 -45
- package/cli/templates/common/manifest.json +26 -26
- package/cli/templates/common/package.json +22 -22
- package/cli/templates/common/py/main.py +125 -125
- package/cli/templates/common/requirements.txt +4 -4
- package/cli/templates/common/tsconfig.json +13 -13
- package/cli/templates/common/ui/content.tsx +216 -216
- package/cli/templates/common/ui/style.css +6 -6
- package/cli/templates/common/vite.config.ts +10 -10
- package/cli/templates/cpp/cpp/main.cpp +73 -73
- package/cli/templates/readme-cpp/README.md +6 -1
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
- package/cli/vite.d.mts +25 -25
- package/cli/vite.mjs +292 -292
- package/package.json +4 -4
- package/cli/src/templates-embedded.mjs +0 -4
- package/cli/storybook-dist/assets/RBCommand.stories-BYkWjdMG.js +0 -73
- package/cli/storybook-dist/assets/RBDialog.stories-DcPRAan8.js +0 -38
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-5UC0ZA3H.js +0 -86
- package/cli/storybook-dist/assets/RBSheet.stories-DMTeF_hx.js +0 -39
- package/cli/storybook-dist/assets/preview-BYhMD55w.css +0 -1
- package/cli/storybook-dist/assets/preview-D-VpQB0_.js +0 -3
- package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +0 -1
|
@@ -1,112 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
|
|
7
|
-
const pluginRoot = resolve(process.argv[2]);
|
|
8
|
-
const port = Number(process.argv[3] || "4181");
|
|
9
|
-
const readyFile = process.argv[4] ? resolve(process.argv[4]) : "";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
process.chdir(pluginRoot);
|
|
13
|
-
|
|
14
|
-
const require = createRequire(resolve(pluginRoot, "package.json"));
|
|
15
|
-
|
|
16
|
-
const viteMod = await import(pathToFileURL(require.resolve("vite")));
|
|
17
|
-
const createServer = viteMod.createServer ?? viteMod.default?.createServer;
|
|
18
|
-
|
|
19
|
-
const BOOTSTRAP_PREFIX = "\0rb-bootstrap:";
|
|
20
|
-
const PREAMBLE_ID = "\0rb-preamble";
|
|
21
|
-
const bootstrapPlugin = {
|
|
22
|
-
name: "rb-plugin-bootstrap",
|
|
23
|
-
resolveId(id) {
|
|
24
|
-
if (id.startsWith("rb-bootstrap:")) return "\0" + id;
|
|
25
|
-
if (id === "rb-preamble") return PREAMBLE_ID;
|
|
26
|
-
},
|
|
27
|
-
load(id) {
|
|
28
|
-
if (id === PREAMBLE_ID) {
|
|
29
|
-
return `
|
|
30
|
-
import RefreshRuntime from "/@react-refresh";
|
|
31
|
-
RefreshRuntime.injectIntoGlobalHook(window);
|
|
32
|
-
window.$RefreshReg$ = () => {};
|
|
33
|
-
window.$RefreshSig$ = () => (type) => type;
|
|
34
|
-
window.__vite_plugin_react_preamble_installed__ = true;
|
|
35
|
-
`;
|
|
36
|
-
}
|
|
37
|
-
if (!id.startsWith(BOOTSTRAP_PREFIX)) return;
|
|
38
|
-
const ui = id.slice(BOOTSTRAP_PREFIX.length) || "content";
|
|
39
|
-
const entry = JSON.stringify(`muscat-ui-entry:${ui}`);
|
|
40
|
-
return `
|
|
41
|
-
import "rb-preamble";
|
|
42
|
-
import { createElement } from "react";
|
|
43
|
-
import { createRoot } from "react-dom/client";
|
|
44
|
-
import { RbContext } from "@rainbow-robotics/plugin-ui/sdk";
|
|
45
|
-
import { setHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
46
|
-
import { RbWs, createLazyRbSdk } from "@rainbow-robotics/plugin-ui/ws";
|
|
47
|
-
import App from ${entry};
|
|
48
|
-
|
|
49
|
-
// HMR passes the new module to the accept callback but doesn't rebind the
|
|
50
|
-
// static \`App\` import, so we track the latest at module scope. Without this,
|
|
51
|
-
// navigating away and back (a fresh mount() on this cached module) renders the
|
|
52
|
-
// stale App from first import. \`activeRender\` points at the mounted root so
|
|
53
|
-
// live edits re-render in place.
|
|
54
|
-
let latestApp = App;
|
|
55
|
-
let activeRender = null;
|
|
56
|
-
if (import.meta.hot) {
|
|
57
|
-
import.meta.hot.accept(${entry}, (m) => {
|
|
58
|
-
if (!m || !m.default) return;
|
|
59
|
-
latestApp = m.default;
|
|
60
|
-
activeRender?.(latestApp);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function mount(container, { socketUrl, pluginId, hostServices, data, onSave }) {
|
|
65
|
-
container.setAttribute("data-muscat-plugin-root", "");
|
|
66
|
-
container.setAttribute("data-muscat-plugin-id", pluginId);
|
|
67
|
-
setHostServices(hostServices);
|
|
68
|
-
// Lazy connect: don't block the first render on the WebSocket handshake
|
|
69
|
-
// (there may be no backend at all in dev, e.g. no robot reachable) — each rb
|
|
70
|
-
// method awaits this promise internally instead.
|
|
71
|
-
const wsPromise = RbWs.connect(socketUrl, pluginId);
|
|
72
|
-
const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
|
|
73
|
-
const root = createRoot(container);
|
|
74
|
-
let mounted = true;
|
|
75
|
-
const render = (Component) => {
|
|
76
|
-
if (!mounted) return;
|
|
77
|
-
root.render(createElement(RbContext.Provider, { value: rb }, createElement(Component)));
|
|
78
|
-
};
|
|
79
|
-
const boundRender = (Component) => queueMicrotask(() => render(Component));
|
|
80
|
-
activeRender = boundRender;
|
|
81
|
-
render(latestApp);
|
|
82
|
-
return () => {
|
|
83
|
-
mounted = false;
|
|
84
|
-
// Don't clobber a newer mount's render (host defers unmount a microtask).
|
|
85
|
-
if (activeRender === boundRender) activeRender = null;
|
|
86
|
-
root.unmount();
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
`;
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const server = await createServer({
|
|
94
|
-
root: pluginRoot,
|
|
95
|
-
configFile: resolve(pluginRoot, "vite.config.ts"),
|
|
96
|
-
mode: "development",
|
|
97
|
-
plugins: [bootstrapPlugin],
|
|
98
|
-
server: {
|
|
99
|
-
port,
|
|
100
|
-
strictPort: false,
|
|
101
|
-
cors: true,
|
|
102
|
-
hmr: { host: "localhost", protocol: "ws" },
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
await server.listen();
|
|
107
|
-
const address = server.httpServer?.address();
|
|
108
|
-
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
109
|
-
if (readyFile) {
|
|
110
|
-
writeFileSync(readyFile, JSON.stringify({ port: actualPort }), "utf8");
|
|
111
|
-
}
|
|
112
|
-
console.log(`[plugin-dev] vite dev server → http://localhost:${actualPort}`);
|
|
1
|
+
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const pluginRoot = resolve(process.argv[2]);
|
|
8
|
+
const port = Number(process.argv[3] || "4181");
|
|
9
|
+
const readyFile = process.argv[4] ? resolve(process.argv[4]) : "";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
process.chdir(pluginRoot);
|
|
13
|
+
|
|
14
|
+
const require = createRequire(resolve(pluginRoot, "package.json"));
|
|
15
|
+
|
|
16
|
+
const viteMod = await import(pathToFileURL(require.resolve("vite")));
|
|
17
|
+
const createServer = viteMod.createServer ?? viteMod.default?.createServer;
|
|
18
|
+
|
|
19
|
+
const BOOTSTRAP_PREFIX = "\0rb-bootstrap:";
|
|
20
|
+
const PREAMBLE_ID = "\0rb-preamble";
|
|
21
|
+
const bootstrapPlugin = {
|
|
22
|
+
name: "rb-plugin-bootstrap",
|
|
23
|
+
resolveId(id) {
|
|
24
|
+
if (id.startsWith("rb-bootstrap:")) return "\0" + id;
|
|
25
|
+
if (id === "rb-preamble") return PREAMBLE_ID;
|
|
26
|
+
},
|
|
27
|
+
load(id) {
|
|
28
|
+
if (id === PREAMBLE_ID) {
|
|
29
|
+
return `
|
|
30
|
+
import RefreshRuntime from "/@react-refresh";
|
|
31
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
32
|
+
window.$RefreshReg$ = () => {};
|
|
33
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
34
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
if (!id.startsWith(BOOTSTRAP_PREFIX)) return;
|
|
38
|
+
const ui = id.slice(BOOTSTRAP_PREFIX.length) || "content";
|
|
39
|
+
const entry = JSON.stringify(`muscat-ui-entry:${ui}`);
|
|
40
|
+
return `
|
|
41
|
+
import "rb-preamble";
|
|
42
|
+
import { createElement } from "react";
|
|
43
|
+
import { createRoot } from "react-dom/client";
|
|
44
|
+
import { RbContext } from "@rainbow-robotics/plugin-ui/sdk";
|
|
45
|
+
import { setHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
46
|
+
import { RbWs, createLazyRbSdk } from "@rainbow-robotics/plugin-ui/ws";
|
|
47
|
+
import App from ${entry};
|
|
48
|
+
|
|
49
|
+
// HMR passes the new module to the accept callback but doesn't rebind the
|
|
50
|
+
// static \`App\` import, so we track the latest at module scope. Without this,
|
|
51
|
+
// navigating away and back (a fresh mount() on this cached module) renders the
|
|
52
|
+
// stale App from first import. \`activeRender\` points at the mounted root so
|
|
53
|
+
// live edits re-render in place.
|
|
54
|
+
let latestApp = App;
|
|
55
|
+
let activeRender = null;
|
|
56
|
+
if (import.meta.hot) {
|
|
57
|
+
import.meta.hot.accept(${entry}, (m) => {
|
|
58
|
+
if (!m || !m.default) return;
|
|
59
|
+
latestApp = m.default;
|
|
60
|
+
activeRender?.(latestApp);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function mount(container, { socketUrl, pluginId, hostServices, data, onSave }) {
|
|
65
|
+
container.setAttribute("data-muscat-plugin-root", "");
|
|
66
|
+
container.setAttribute("data-muscat-plugin-id", pluginId);
|
|
67
|
+
setHostServices(hostServices);
|
|
68
|
+
// Lazy connect: don't block the first render on the WebSocket handshake
|
|
69
|
+
// (there may be no backend at all in dev, e.g. no robot reachable) — each rb
|
|
70
|
+
// method awaits this promise internally instead.
|
|
71
|
+
const wsPromise = RbWs.connect(socketUrl, pluginId);
|
|
72
|
+
const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
|
|
73
|
+
const root = createRoot(container);
|
|
74
|
+
let mounted = true;
|
|
75
|
+
const render = (Component) => {
|
|
76
|
+
if (!mounted) return;
|
|
77
|
+
root.render(createElement(RbContext.Provider, { value: rb }, createElement(Component)));
|
|
78
|
+
};
|
|
79
|
+
const boundRender = (Component) => queueMicrotask(() => render(Component));
|
|
80
|
+
activeRender = boundRender;
|
|
81
|
+
render(latestApp);
|
|
82
|
+
return () => {
|
|
83
|
+
mounted = false;
|
|
84
|
+
// Don't clobber a newer mount's render (host defers unmount a microtask).
|
|
85
|
+
if (activeRender === boundRender) activeRender = null;
|
|
86
|
+
root.unmount();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const server = await createServer({
|
|
94
|
+
root: pluginRoot,
|
|
95
|
+
configFile: resolve(pluginRoot, "vite.config.ts"),
|
|
96
|
+
mode: "development",
|
|
97
|
+
plugins: [bootstrapPlugin],
|
|
98
|
+
server: {
|
|
99
|
+
port,
|
|
100
|
+
strictPort: false,
|
|
101
|
+
cors: true,
|
|
102
|
+
hmr: { host: "localhost", protocol: "ws" },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await server.listen();
|
|
107
|
+
const address = server.httpServer?.address();
|
|
108
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
109
|
+
if (readyFile) {
|
|
110
|
+
writeFileSync(readyFile, JSON.stringify({ port: actualPort }), "utf8");
|
|
111
|
+
}
|
package/cli/src/ports.mjs
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
// Port contract helpers for plugin back ends.
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { usesHostNetwork } from "./zenohDev.mjs";
|
|
4
|
-
|
|
5
|
-
export const PLUGIN_PORT_MIN = 28600;
|
|
6
|
-
export const PLUGIN_PORT_MAX = 28999;
|
|
7
|
-
export const MEDIA_PORTS = [8554, 8889, 8189]; // MediaMTX: RTSP publish, WebRTC, WebRTC media
|
|
8
|
-
export const INTERNAL_DEV_PORTS = new Set([10000, ...MEDIA_PORTS]);
|
|
9
|
-
|
|
10
|
-
export function mediaPortFlags() {
|
|
11
|
-
return [
|
|
12
|
-
"-p", "127.0.0.1:8554:8554", "-p", "127.0.0.1:8889:8889",
|
|
13
|
-
"-p", "127.0.0.1:8189:8189/udp", "-p", "127.0.0.1:8189:8189/tcp",
|
|
14
|
-
];
|
|
15
|
-
}
|
|
16
|
-
export const RESERVED_BACKEND_PORTS = new Map([
|
|
17
|
-
[502, "Modbus TCP"],
|
|
18
|
-
[4840, "OPC-UA"],
|
|
19
|
-
[10000, "Muscat plugin gateway"],
|
|
20
|
-
[2222, "EtherNet/IP Implicit"],
|
|
21
|
-
[30004, "RTDE"],
|
|
22
|
-
[34964, "ProfiNET"],
|
|
23
|
-
[44818, "EtherNet/IP Explicit"],
|
|
24
|
-
[50051, "gRPC"],
|
|
25
|
-
]);
|
|
26
|
-
export const RESERVED_BACKEND_PORT_RANGES = [
|
|
27
|
-
{ from: 5170, to: 5179, name: "UI 517x servers" },
|
|
28
|
-
{ from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
|
|
29
|
-
{ from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function reservedPortReason(port) {
|
|
33
|
-
if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
|
|
34
|
-
const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
|
|
35
|
-
if (!range) return null;
|
|
36
|
-
return `${range.name} (${range.from}-${range.to})`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// function isPluginManagedPort(port) {
|
|
40
|
-
// return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
|
|
41
|
-
// }
|
|
42
|
-
|
|
43
|
-
function isInt(value) {
|
|
44
|
-
return Number.isInteger(value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validatePort(port, field, errors) {
|
|
48
|
-
if (!isInt(port)) {
|
|
49
|
-
errors.push(`${field}: must be a port number`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (port < 1 || port > 65535) {
|
|
53
|
-
errors.push(`${field}: port must be between 1 and 65535`);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const reserved = reservedPortReason(port);
|
|
57
|
-
if (reserved) {
|
|
58
|
-
errors.push(`${field}: ${port} is reserved for ${reserved}`);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
|
|
62
|
-
errors.push(
|
|
63
|
-
`${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function normalizePorts(ports, errors = []) {
|
|
69
|
-
if (ports === undefined || ports === null) return [];
|
|
70
|
-
if (!Array.isArray(ports)) {
|
|
71
|
-
errors.push(`"ports" must be an array`);
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
if (ports.length === 0) {
|
|
75
|
-
errors.push(`"ports" must not be empty when provided`);
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const normalized = [];
|
|
80
|
-
const seen = new Set();
|
|
81
|
-
ports.forEach((port, i) => {
|
|
82
|
-
validatePort(port, `ports[${i}]`, errors);
|
|
83
|
-
if (!isInt(port)) return;
|
|
84
|
-
if (seen.has(port)) {
|
|
85
|
-
errors.push(`"ports" entries must be unique (duplicate: ${port})`);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
seen.add(port);
|
|
89
|
-
normalized.push(port);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return normalized;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function validatePorts(ports, errors) {
|
|
96
|
-
normalizePorts(ports, errors);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function manifestPortRanges(manifest) {
|
|
100
|
-
return normalizePorts(manifest?.ports, []);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function formatPortRange(range) {
|
|
104
|
-
return String(range);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function formatPortRanges(ranges) {
|
|
108
|
-
return ranges.map(formatPortRange).join(", ");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function portInRanges(port, ranges) {
|
|
112
|
-
return ranges.includes(port);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function portEnvFlags(ranges) {
|
|
116
|
-
if (ranges.length === 0) return [];
|
|
117
|
-
return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* `docker run -p` flags for the plugin's declared ports, so macOS/Windows can
|
|
122
|
-
* reach the backend's listeners (native Linux uses --network host and already
|
|
123
|
-
* has them). Only valid on the container that owns the netns.
|
|
124
|
-
*/
|
|
125
|
-
export function publishPortFlags(ranges = []) {
|
|
126
|
-
return ranges.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Skips OS-assigned loopback listeners (127.0.0.1:0, e.g. the SDK's Zenoh session).
|
|
130
|
-
function listeningTcpPorts(container) {
|
|
131
|
-
const script = `
|
|
132
|
-
read -r eph_lo eph_hi < /proc/sys/net/ipv4/ip_local_port_range 2>/dev/null || { eph_lo=32768; eph_hi=60999; }
|
|
133
|
-
for f in /proc/net/tcp /proc/net/tcp6; do
|
|
134
|
-
[ -r "$f" ] || continue
|
|
135
|
-
tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
|
|
136
|
-
[ "$st" = "0A" ] || continue
|
|
137
|
-
hex_ip="\${local_address%%:*}"
|
|
138
|
-
port=$((0x\${local_address##*:}))
|
|
139
|
-
case "$hex_ip" in
|
|
140
|
-
0100007F|00000000000000000000000001000000)
|
|
141
|
-
[ "$port" -ge "$eph_lo" ] && [ "$port" -le "$eph_hi" ] && continue ;;
|
|
142
|
-
esac
|
|
143
|
-
echo "$port"
|
|
144
|
-
done
|
|
145
|
-
done | sort -n -u
|
|
146
|
-
`;
|
|
147
|
-
const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
|
|
148
|
-
encoding: "utf8",
|
|
149
|
-
});
|
|
150
|
-
if (r.status !== 0) return [];
|
|
151
|
-
return String(r.stdout || "")
|
|
152
|
-
.split(/\s+/)
|
|
153
|
-
.filter(Boolean)
|
|
154
|
-
.map((v) => Number(v))
|
|
155
|
-
.filter((v) => Number.isInteger(v));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000, allow = [] }) {
|
|
159
|
-
// Under --network host (or a local Muscat container's netns) /proc/net/tcp shows every
|
|
160
|
-
// host/robot service port, not just the plugin's — the contract can't be enforced.
|
|
161
|
-
if (usesHostNetwork() || process.env.MUSCAT_PK_MUSCAT_CONTAINER) return () => false;
|
|
162
|
-
const declared = ranges ?? [];
|
|
163
|
-
// muscat-pk's own dev plumbing shares this netns — not the plugin's doing.
|
|
164
|
-
const ignored = new Set([...INTERNAL_DEV_PORTS, ...allow.map(Number).filter(Number.isInteger)]);
|
|
165
|
-
let reported = false;
|
|
166
|
-
const timer = setInterval(() => {
|
|
167
|
-
// Every listening port that isn't declared is a violation — a port outside
|
|
168
|
-
// 28600-28999 can't be declared at all, so it must not be opened either.
|
|
169
|
-
// Otherwise it silently escapes both this check and rby2's install-time
|
|
170
|
-
// conflict check, and only collides once it's on a robot.
|
|
171
|
-
const unexpected = listeningTcpPorts(container)
|
|
172
|
-
.filter((port) => !ignored.has(port))
|
|
173
|
-
.filter((port) => !portInRanges(port, declared));
|
|
174
|
-
if (unexpected.length === 0) return;
|
|
175
|
-
|
|
176
|
-
clearInterval(timer);
|
|
177
|
-
reported = true;
|
|
178
|
-
const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
|
|
179
|
-
log.error(
|
|
180
|
-
`backend opened undeclared TCP port(s): ${unexpected.join(", ")}. ` +
|
|
181
|
-
`Every listening port must be in manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
|
|
182
|
-
`Declared: ${declaredText}`,
|
|
183
|
-
);
|
|
184
|
-
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}, intervalMs);
|
|
187
|
-
timer.unref?.();
|
|
188
|
-
|
|
189
|
-
return () => {
|
|
190
|
-
clearInterval(timer);
|
|
191
|
-
return reported;
|
|
192
|
-
};
|
|
193
|
-
}
|
|
1
|
+
// Port contract helpers for plugin back ends.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { usesHostNetwork } from "./zenohDev.mjs";
|
|
4
|
+
|
|
5
|
+
export const PLUGIN_PORT_MIN = 28600;
|
|
6
|
+
export const PLUGIN_PORT_MAX = 28999;
|
|
7
|
+
export const MEDIA_PORTS = [8554, 8889, 8189]; // MediaMTX: RTSP publish, WebRTC, WebRTC media
|
|
8
|
+
export const INTERNAL_DEV_PORTS = new Set([10000, ...MEDIA_PORTS]);
|
|
9
|
+
|
|
10
|
+
export function mediaPortFlags() {
|
|
11
|
+
return [
|
|
12
|
+
"-p", "127.0.0.1:8554:8554", "-p", "127.0.0.1:8889:8889",
|
|
13
|
+
"-p", "127.0.0.1:8189:8189/udp", "-p", "127.0.0.1:8189:8189/tcp",
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
export const RESERVED_BACKEND_PORTS = new Map([
|
|
17
|
+
[502, "Modbus TCP"],
|
|
18
|
+
[4840, "OPC-UA"],
|
|
19
|
+
[10000, "Muscat plugin gateway"],
|
|
20
|
+
[2222, "EtherNet/IP Implicit"],
|
|
21
|
+
[30004, "RTDE"],
|
|
22
|
+
[34964, "ProfiNET"],
|
|
23
|
+
[44818, "EtherNet/IP Explicit"],
|
|
24
|
+
[50051, "gRPC"],
|
|
25
|
+
]);
|
|
26
|
+
export const RESERVED_BACKEND_PORT_RANGES = [
|
|
27
|
+
{ from: 5170, to: 5179, name: "UI 517x servers" },
|
|
28
|
+
{ from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
|
|
29
|
+
{ from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function reservedPortReason(port) {
|
|
33
|
+
if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
|
|
34
|
+
const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
|
|
35
|
+
if (!range) return null;
|
|
36
|
+
return `${range.name} (${range.from}-${range.to})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// function isPluginManagedPort(port) {
|
|
40
|
+
// return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
function isInt(value) {
|
|
44
|
+
return Number.isInteger(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validatePort(port, field, errors) {
|
|
48
|
+
if (!isInt(port)) {
|
|
49
|
+
errors.push(`${field}: must be a port number`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (port < 1 || port > 65535) {
|
|
53
|
+
errors.push(`${field}: port must be between 1 and 65535`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const reserved = reservedPortReason(port);
|
|
57
|
+
if (reserved) {
|
|
58
|
+
errors.push(`${field}: ${port} is reserved for ${reserved}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
|
|
62
|
+
errors.push(
|
|
63
|
+
`${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizePorts(ports, errors = []) {
|
|
69
|
+
if (ports === undefined || ports === null) return [];
|
|
70
|
+
if (!Array.isArray(ports)) {
|
|
71
|
+
errors.push(`"ports" must be an array`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (ports.length === 0) {
|
|
75
|
+
errors.push(`"ports" must not be empty when provided`);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const normalized = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
ports.forEach((port, i) => {
|
|
82
|
+
validatePort(port, `ports[${i}]`, errors);
|
|
83
|
+
if (!isInt(port)) return;
|
|
84
|
+
if (seen.has(port)) {
|
|
85
|
+
errors.push(`"ports" entries must be unique (duplicate: ${port})`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
seen.add(port);
|
|
89
|
+
normalized.push(port);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validatePorts(ports, errors) {
|
|
96
|
+
normalizePorts(ports, errors);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function manifestPortRanges(manifest) {
|
|
100
|
+
return normalizePorts(manifest?.ports, []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatPortRange(range) {
|
|
104
|
+
return String(range);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatPortRanges(ranges) {
|
|
108
|
+
return ranges.map(formatPortRange).join(", ");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function portInRanges(port, ranges) {
|
|
112
|
+
return ranges.includes(port);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function portEnvFlags(ranges) {
|
|
116
|
+
if (ranges.length === 0) return [];
|
|
117
|
+
return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `docker run -p` flags for the plugin's declared ports, so macOS/Windows can
|
|
122
|
+
* reach the backend's listeners (native Linux uses --network host and already
|
|
123
|
+
* has them). Only valid on the container that owns the netns.
|
|
124
|
+
*/
|
|
125
|
+
export function publishPortFlags(ranges = []) {
|
|
126
|
+
return ranges.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skips OS-assigned loopback listeners (127.0.0.1:0, e.g. the SDK's Zenoh session).
|
|
130
|
+
function listeningTcpPorts(container) {
|
|
131
|
+
const script = `
|
|
132
|
+
read -r eph_lo eph_hi < /proc/sys/net/ipv4/ip_local_port_range 2>/dev/null || { eph_lo=32768; eph_hi=60999; }
|
|
133
|
+
for f in /proc/net/tcp /proc/net/tcp6; do
|
|
134
|
+
[ -r "$f" ] || continue
|
|
135
|
+
tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
|
|
136
|
+
[ "$st" = "0A" ] || continue
|
|
137
|
+
hex_ip="\${local_address%%:*}"
|
|
138
|
+
port=$((0x\${local_address##*:}))
|
|
139
|
+
case "$hex_ip" in
|
|
140
|
+
0100007F|00000000000000000000000001000000)
|
|
141
|
+
[ "$port" -ge "$eph_lo" ] && [ "$port" -le "$eph_hi" ] && continue ;;
|
|
142
|
+
esac
|
|
143
|
+
echo "$port"
|
|
144
|
+
done
|
|
145
|
+
done | sort -n -u
|
|
146
|
+
`;
|
|
147
|
+
const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
});
|
|
150
|
+
if (r.status !== 0) return [];
|
|
151
|
+
return String(r.stdout || "")
|
|
152
|
+
.split(/\s+/)
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.map((v) => Number(v))
|
|
155
|
+
.filter((v) => Number.isInteger(v));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000, allow = [] }) {
|
|
159
|
+
// Under --network host (or a local Muscat container's netns) /proc/net/tcp shows every
|
|
160
|
+
// host/robot service port, not just the plugin's — the contract can't be enforced.
|
|
161
|
+
if (usesHostNetwork() || process.env.MUSCAT_PK_MUSCAT_CONTAINER) return () => false;
|
|
162
|
+
const declared = ranges ?? [];
|
|
163
|
+
// muscat-pk's own dev plumbing shares this netns — not the plugin's doing.
|
|
164
|
+
const ignored = new Set([...INTERNAL_DEV_PORTS, ...allow.map(Number).filter(Number.isInteger)]);
|
|
165
|
+
let reported = false;
|
|
166
|
+
const timer = setInterval(() => {
|
|
167
|
+
// Every listening port that isn't declared is a violation — a port outside
|
|
168
|
+
// 28600-28999 can't be declared at all, so it must not be opened either.
|
|
169
|
+
// Otherwise it silently escapes both this check and rby2's install-time
|
|
170
|
+
// conflict check, and only collides once it's on a robot.
|
|
171
|
+
const unexpected = listeningTcpPorts(container)
|
|
172
|
+
.filter((port) => !ignored.has(port))
|
|
173
|
+
.filter((port) => !portInRanges(port, declared));
|
|
174
|
+
if (unexpected.length === 0) return;
|
|
175
|
+
|
|
176
|
+
clearInterval(timer);
|
|
177
|
+
reported = true;
|
|
178
|
+
const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
|
|
179
|
+
log.error(
|
|
180
|
+
`backend opened undeclared TCP port(s): ${unexpected.join(", ")}. ` +
|
|
181
|
+
`Every listening port must be in manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
|
|
182
|
+
`Declared: ${declaredText}`,
|
|
183
|
+
);
|
|
184
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}, intervalMs);
|
|
187
|
+
timer.unref?.();
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
clearInterval(timer);
|
|
191
|
+
return reported;
|
|
192
|
+
};
|
|
193
|
+
}
|