From cdc36aa058b848dc37b5093b9289024845ab1a95 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 12:41:13 +0800 Subject: [PATCH 01/52] web: replace electron runtime with ArkWeb + on-device Node.js (hqzing/ohos-node) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/, build/, package.json: switch from electerm desktop source to the electerm-web codebase (same base as electerm-android) — pure-node backend, no electron APIs - build/web/build.mjs: vite frontend + esbuild backend bundle into entry/src/main/resources/resfile/electerm (readable directly at runtime, no extraction) - scripts/prepare-node.sh: download hqzing/ohos-node v24.19.0, strip, install as entry/libs/arm64-v8a/libnode.so - entry module rewritten as pure ArkTS app: - Index.ets: Web component + start node backend via childProcessManager.startNativeChildProcess + HTTP poll + loadUrl - cpp/node_launcher.c: native child entry that execv()s libnode.so (memfd fallback if the lib dir is noexec), full boot log - cpp/node_ctl.c: NAPI kill(pid) for EntryAbility.onDestroy - scripts/build-web-app.sh: unsigned hvigor assembleApp + hap-sign-tool signing, entry module only (no web_engine) - .github/workflows/build-web.yml: CI for dev2 branch Co-Authored-By: Claude Fable 5 --- .github/workflows/build-web.yml | 279 ++ .gitignore | 7 +- build/bin/.yarnclean | 70 - build/bin/app.js | 13 - build/bin/build.js | 22 - build/bin/clean-empty-folders.js | 83 - build/bin/clean.js | 5 - build/bin/copy.js | 39 - build/bin/gen_logos.py | 73 - build/bin/install.js | 9 - build/bin/prepare.js | 111 - build/bin/pug.js | 48 - build/bin/release | 10 - build/bin/start.js | 6 - build/bin/vite-build.js | 7 - build/harmony/build.js | 349 --- build/logos/electerm-banner-logo.png | Bin 6080 -> 0 bytes build/logos/electerm-logo-square.png | Bin 44759 -> 0 bytes build/vite/.sample.env | 5 - build/vite/common.js | 31 - build/vite/conf.js | 87 - build/vite/def.js | 6 - build/vite/dev-server.js | 157 - build/vite/diagnostics-channel-stub.js | 55 - build/vite/package-lock.json | 12 - build/vite/package.json | 11 - .../reply/cache-v2-49f5662a4b05781cba4a.json | 1407 +++++++++ .../cmakeFiles-v1-e37c68776f2415d7fef8.json | 173 ++ .../codemodel-v2-1ac40039f16ae038c893.json | 69 + ...ectory-.-Release-f5ebdc15457944623624.json | 14 + .../reply/index-2026-08-28T04-39-21-0293.json | 89 + ...node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json | 155 + ...launcher-Release-968f49e15038ddbf7844.json | 155 + .../default/release/arm64-v8a/.ninja_deps | Bin 0 -> 6144 bytes .../default/release/arm64-v8a/.ninja_log | 6 + .../default/release/arm64-v8a/CMakeCache.txt | 421 +++ .../CMakeFiles/3.28.2/CMakeCCompiler.cmake | 74 + .../CMakeFiles/3.28.2/CMakeCXXCompiler.cmake | 85 + .../3.28.2/CMakeDetermineCompilerABI_C.bin | Bin 0 -> 14168 bytes .../3.28.2/CMakeDetermineCompilerABI_CXX.bin | Bin 0 -> 14216 bytes .../CMakeFiles/3.28.2/CMakeSystem.cmake | 15 + .../3.28.2/CompilerIdC/CMakeCCompilerId.c | 880 ++++++ .../3.28.2/CompilerIdC/CMakeCCompilerId.o | Bin 0 -> 3296 bytes .../CompilerIdCXX/CMakeCXXCompilerId.cpp | 869 ++++++ .../3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o | Bin 0 -> 3320 bytes .../CMakeFiles/CMakeConfigureLog.yaml | 386 +++ .../CMakeFiles/TargetDirectories.txt | 4 + .../arm64-v8a/CMakeFiles/cmake.check_cache | 1 + .../CMakeFiles/node_ctl.dir/node_ctl.c.o | Bin 0 -> 4000 bytes .../node_launcher.dir/node_launcher.c.o | Bin 0 -> 11312 bytes .../release/arm64-v8a/CMakeFiles/rules.ninja | 83 + .../arm64-v8a/additional_project_files.txt | 0 .../default/release/arm64-v8a/build.ninja | 194 ++ .../release/arm64-v8a/build_file_index.txt | 1 + .../release/arm64-v8a/cmake_install.cmake | 54 + .../release/arm64-v8a/compile_commands.json | 14 + .../arm64-v8a/configure_fingerprint.json | 1 + .../arm64-v8a/hvigor_native_config.json | 1 + .../arm64-v8a/metadata_generation_command.txt | 17 + .../release/arm64-v8a/native_work_dir.txt | 1 + .../default/release/arm64-v8a/output.log | 2 + .../release/hvigor/arm64-v8a/summary.cmake | 0 entry/build-profile.json5 | 6 + entry/oh-package-lock.json5 | 19 + entry/oh-package.json5 | 4 +- entry/oh_modules/libnode_ctl | 1 + entry/src/main/cpp/CMakeLists.txt | 12 + entry/src/main/cpp/node_ctl.c | 54 + entry/src/main/cpp/node_launcher.c | 306 ++ .../src/main/cpp/types/libnode_ctl/index.d.ts | 1 + .../cpp/types/libnode_ctl/oh-package.json5 | 6 + entry/src/main/ets/AbilityStage.ets | 42 +- entry/src/main/ets/BackendManager.ets | 29 + .../main/ets/entryability/EntryAbility.ets | 112 +- entry/src/main/ets/pages/Index.ets | 201 +- entry/src/main/ets/pages/NodeHandleWindow.ets | 65 - entry/src/main/module.json5 | 5 - .../main/resources/base/element/string.json | 4 + .../resources/base/profile/main_pages.json | 3 +- hvigor/hvigor-config.json5 | 12 +- local.properties | 2 + oh-package-lock.json5 | 20 + package-lock.json | 2529 +++++++++-------- package.json | 107 +- scripts/build-web-app.sh | 404 +++ scripts/prepare-electron-runtime.sh | 300 -- scripts/prepare-node.sh | 109 + scripts/prepare-web.sh | 117 +- src/app/app.js | 27 +- src/app/bootstrap.js | 130 - src/app/common/app-props.js | 28 - src/app/common/bookmark-zod-schemas.js | 4 +- src/app/common/build-run-scripts.js | 2 +- src/app/common/build-ssh-tunnel.js | 2 +- src/app/common/config-default.js | 19 +- src/app/common/constants.js | 5 +- ...and-file-count.js => count-folder-data.js} | 4 +- .../common/create-session-log-file-path.js | 2 +- src/app/common/default-setting.js | 13 +- src/app/common/default-user-name.js | 1 - src/app/common/fs-functions.js | 34 + src/app/common/get-json.js | 4 + src/app/common/is-ip.js | 16 + src/app/common/log.js | 87 +- src/app/common/parse-quick-connect.js | 455 --- src/app/common/pass-enc.js | 4 +- src/app/common/runtime-constants.js | 99 +- src/app/common/sanitize-filename.js | 2 +- src/app/common/time.js | 19 +- src/app/common/uid.js | 4 +- src/app/common/version-compare.js | 2 +- src/app/lib/ai.js | 68 +- src/app/lib/auth.js | 64 - src/app/lib/build-proxy.js | 2 +- src/app/lib/command-line.js | 98 - src/app/lib/conf.js | 27 + src/app/lib/create-app.js | 164 -- src/app/lib/create-window.js | 161 -- src/app/lib/custom-require.js | 46 +- src/app/lib/db.js | 19 +- src/app/lib/deep-link.js | 157 - src/app/lib/enc.js | 21 +- src/app/lib/error-page.js | 70 - src/app/lib/extensions.js | 17 + src/app/lib/fancy-console.js | 181 ++ src/app/lib/file-server.js | 25 - src/app/lib/font-list.js | 40 +- src/app/lib/fs.js | 255 +- src/app/lib/get-config.js | 49 - src/app/lib/get-constants.js | 84 + src/app/lib/get-port.js | 38 - .../lib/{glob-state.js => global-state.js} | 7 +- src/app/lib/init-app.js | 48 - src/app/lib/init-server.js | 57 - src/app/lib/init.js | 47 + src/app/lib/install-src.js | 32 +- src/app/lib/ipc-sync.js | 154 - src/app/lib/ipc.js | 279 -- src/app/lib/iterm-theme.js | 8 +- src/app/lib/jwt.js | 33 + src/app/lib/key-bind.js | 14 - src/app/lib/last-state.js | 46 - src/app/lib/locales.js | 65 - src/app/lib/lodash.js | 115 - src/app/lib/login.js | 18 + src/app/{common => lib}/lookup.js | 4 +- src/app/lib/nedb.js | 246 -- src/app/lib/npm.js | 36 +- src/app/lib/on-close.js | 61 - src/app/lib/open-file-with-editor.js | 120 - src/app/lib/proxy-agent.js | 16 +- src/app/lib/run-sync.js | 103 + src/app/lib/safe-storage.js | 97 - src/app/lib/serial-port.js | 22 +- src/app/lib/shortcut.js | 51 - src/app/lib/show-item-in-folder.js | 39 + src/app/lib/single-instance.js | 140 - src/app/lib/sqlite.js | 145 + src/app/lib/ssh-config.js | 10 +- src/app/lib/storage-key.js | 46 - src/app/lib/system-ca.js | 137 + src/app/lib/user-config-controller.js | 53 - src/app/lib/user-config.js | 26 + src/app/lib/view.js | 81 + src/app/lib/watch-file.js | 14 +- src/app/lib/webview-handler.js | 152 - src/app/lib/window-control.js | 73 - src/app/lib/window-drag-move.js | 45 - src/app/lib/window-restore.js | 205 -- src/app/lib/zod.js | 2 +- src/app/mcp/server/mcp.js | 2 +- src/app/mcp/server/streamableHttp.js | 4 +- src/app/mcp/server/tasks.js | 4 +- src/app/preload/preload.js | 55 - src/app/routes/file-transfer.js | 71 + src/app/routes/http.js | 33 + src/app/routes/ws.js | 356 +++ src/app/server/app-wrap.js | 15 - src/app/server/child-process.js | 71 - src/app/server/dispatch-center.js | 247 +- src/app/server/download-upgrade.js | 82 +- src/app/server/fetch.js | 13 +- src/app/server/fs.js | 6 +- src/app/server/ftp-client.js | 94 +- src/app/server/ftp-file.js | 11 +- src/app/server/ftp-transfer.js | 6 +- src/app/server/global-state.js | 24 +- src/app/server/rdp-proxy.js | 275 +- src/app/server/remote-common.js | 37 +- src/app/server/server.js | 94 +- src/app/server/session-api.js | 96 - src/app/server/session-base.js | 103 +- src/app/server/session-common.js | 2 +- src/app/server/session-ftp.js | 20 +- src/app/server/session-hop.js | 15 +- src/app/server/session-local.js | 180 +- src/app/server/session-log.js | 28 +- src/app/server/session-process.js | 274 -- src/app/server/session-rdp.js | 41 +- src/app/server/session-serial.js | 230 +- src/app/server/session-server.js | 615 ---- src/app/server/session-sftp.js | 26 +- src/app/server/session-spice.js | 29 +- src/app/server/session-ssh.js | 68 +- src/app/server/session-telnet.js | 35 +- src/app/server/session-vnc.js | 19 +- src/app/server/session.js | 54 +- src/app/server/sftp-file.js | 11 +- src/app/server/socks.js | 7 +- src/app/server/spice-proxy.js | 8 +- src/app/server/ssh-known-hosts.js | 16 +- src/app/server/ssh-proxy-command.js | 26 +- src/app/server/ssh-tunnel.js | 19 +- src/app/server/ssh2-alg.js | 10 +- src/app/server/sync.js | 19 +- src/app/server/telnet.js | 12 +- src/app/server/terminal-api.js | 105 +- src/app/server/transfer.js | 42 +- src/app/server/trzsz.js | 49 +- src/app/server/webdav-sync.js | 26 +- src/app/server/ws-dec.js | 31 - src/app/server/xmodem.js | 14 +- src/app/server/zmodem.js | 14 +- src/app/upgrade/db-defaults.js | 2 +- src/app/upgrade/index.js | 33 +- src/app/upgrade/{init-db.js => init-nedb.js} | 10 +- src/app/upgrade/version-upgrade.js | 8 +- src/{client => app}/views/index.pug | 11 +- src/app/widgets/load-widget.js | 30 +- src/app/widgets/widget-batch-op.js | 4 +- src/app/widgets/widget-local-file-server.js | 10 +- src/app/widgets/widget-local-ftp-server.js | 8 +- src/app/widgets/widget-mcp-server.js | 175 +- src/app/widgets/widget-rename.js | 53 +- src/client/{entry => entry-web}/basic.js | 38 +- src/client/entry-web/electerm.jsx | 10 + src/client/entry-web/worker.js | 200 ++ src/client/entry/electerm.jsx | 9 - src/client/entry/worker.js | 146 - src/client/file-select-dialog/file-item.jsx | 34 + .../file-select-dialog/file-select-dialog.jsx | 542 ++++ .../file-select-dialog.styl | 35 + src/client/harmony/language-select.jsx | 69 - src/client/harmony/language-select.styl | 50 - src/client/harmony/main.jsx | 10 - src/client/simple-auth/logout.jsx | 25 + src/client/simple-auth/logout.styl | 8 + src/client/simple-auth/web-login.jsx | 104 + src/client/statics/favicon.ico | Bin 0 -> 1150 bytes src/client/web-components/path.js | 57 + src/client/web-components/store-login.js | 51 + src/client/web-components/style-overide.styl | 36 + src/client/web-components/web-api.js | 135 + src/client/web-components/web-main.jsx | 14 + src/client/web-components/web-pre.js | 257 ++ src/client/web-components/web-store.js | 53 + 256 files changed, 12979 insertions(+), 9524 deletions(-) create mode 100644 .github/workflows/build-web.yml delete mode 100644 build/bin/.yarnclean delete mode 100644 build/bin/app.js delete mode 100644 build/bin/build.js delete mode 100644 build/bin/clean-empty-folders.js delete mode 100644 build/bin/clean.js delete mode 100644 build/bin/copy.js delete mode 100755 build/bin/gen_logos.py delete mode 100644 build/bin/install.js delete mode 100644 build/bin/prepare.js delete mode 100644 build/bin/pug.js delete mode 100755 build/bin/release delete mode 100644 build/bin/start.js delete mode 100755 build/bin/vite-build.js delete mode 100644 build/harmony/build.js delete mode 100644 build/logos/electerm-banner-logo.png delete mode 100644 build/logos/electerm-logo-square.png delete mode 100644 build/vite/.sample.env delete mode 100644 build/vite/common.js delete mode 100644 build/vite/conf.js delete mode 100644 build/vite/def.js delete mode 100644 build/vite/dev-server.js delete mode 100644 build/vite/diagnostics-channel-stub.js delete mode 100644 build/vite/package-lock.json delete mode 100644 build/vite/package.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.ninja_deps create mode 100644 entry/.cxx/default/default/release/arm64-v8a/.ninja_log create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt create mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake create mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake create mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin create mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin create mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_launcher.dir/node_launcher.c.o create mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja create mode 100644 entry/.cxx/default/default/release/arm64-v8a/additional_project_files.txt create mode 100644 entry/.cxx/default/default/release/arm64-v8a/build.ninja create mode 100644 entry/.cxx/default/default/release/arm64-v8a/build_file_index.txt create mode 100644 entry/.cxx/default/default/release/arm64-v8a/cmake_install.cmake create mode 100644 entry/.cxx/default/default/release/arm64-v8a/compile_commands.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json create mode 100644 entry/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt create mode 100644 entry/.cxx/default/default/release/arm64-v8a/native_work_dir.txt create mode 100644 entry/.cxx/default/default/release/arm64-v8a/output.log create mode 100644 entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake create mode 100644 entry/oh-package-lock.json5 create mode 120000 entry/oh_modules/libnode_ctl create mode 100644 entry/src/main/cpp/CMakeLists.txt create mode 100644 entry/src/main/cpp/node_ctl.c create mode 100644 entry/src/main/cpp/node_launcher.c create mode 100644 entry/src/main/cpp/types/libnode_ctl/index.d.ts create mode 100644 entry/src/main/cpp/types/libnode_ctl/oh-package.json5 create mode 100644 entry/src/main/ets/BackendManager.ets delete mode 100644 entry/src/main/ets/pages/NodeHandleWindow.ets create mode 100644 local.properties create mode 100644 oh-package-lock.json5 create mode 100755 scripts/build-web-app.sh delete mode 100755 scripts/prepare-electron-runtime.sh create mode 100755 scripts/prepare-node.sh delete mode 100644 src/app/bootstrap.js delete mode 100644 src/app/common/app-props.js rename src/app/common/{get-folder-size-and-file-count.js => count-folder-data.js} (89%) delete mode 100644 src/app/common/default-user-name.js create mode 100644 src/app/common/fs-functions.js create mode 100644 src/app/common/get-json.js create mode 100644 src/app/common/is-ip.js delete mode 100644 src/app/common/parse-quick-connect.js delete mode 100644 src/app/lib/auth.js delete mode 100644 src/app/lib/command-line.js create mode 100644 src/app/lib/conf.js delete mode 100644 src/app/lib/create-app.js delete mode 100644 src/app/lib/create-window.js delete mode 100644 src/app/lib/deep-link.js delete mode 100644 src/app/lib/error-page.js create mode 100644 src/app/lib/extensions.js create mode 100644 src/app/lib/fancy-console.js delete mode 100644 src/app/lib/file-server.js delete mode 100644 src/app/lib/get-config.js create mode 100644 src/app/lib/get-constants.js delete mode 100644 src/app/lib/get-port.js rename src/app/lib/{glob-state.js => global-state.js} (84%) delete mode 100644 src/app/lib/init-app.js delete mode 100644 src/app/lib/init-server.js create mode 100644 src/app/lib/init.js delete mode 100644 src/app/lib/ipc-sync.js delete mode 100644 src/app/lib/ipc.js create mode 100644 src/app/lib/jwt.js delete mode 100644 src/app/lib/key-bind.js delete mode 100644 src/app/lib/last-state.js delete mode 100644 src/app/lib/locales.js delete mode 100644 src/app/lib/lodash.js create mode 100644 src/app/lib/login.js rename src/app/{common => lib}/lookup.js (91%) delete mode 100644 src/app/lib/nedb.js delete mode 100644 src/app/lib/on-close.js delete mode 100644 src/app/lib/open-file-with-editor.js create mode 100644 src/app/lib/run-sync.js delete mode 100644 src/app/lib/safe-storage.js delete mode 100644 src/app/lib/shortcut.js create mode 100644 src/app/lib/show-item-in-folder.js delete mode 100644 src/app/lib/single-instance.js create mode 100644 src/app/lib/sqlite.js delete mode 100644 src/app/lib/storage-key.js create mode 100644 src/app/lib/system-ca.js delete mode 100644 src/app/lib/user-config-controller.js create mode 100644 src/app/lib/user-config.js create mode 100644 src/app/lib/view.js delete mode 100644 src/app/lib/webview-handler.js delete mode 100644 src/app/lib/window-control.js delete mode 100644 src/app/lib/window-drag-move.js delete mode 100644 src/app/lib/window-restore.js delete mode 100644 src/app/preload/preload.js create mode 100644 src/app/routes/file-transfer.js create mode 100644 src/app/routes/http.js create mode 100644 src/app/routes/ws.js delete mode 100644 src/app/server/app-wrap.js delete mode 100644 src/app/server/child-process.js delete mode 100644 src/app/server/session-api.js delete mode 100644 src/app/server/session-process.js delete mode 100644 src/app/server/session-server.js delete mode 100644 src/app/server/ws-dec.js rename src/app/upgrade/{init-db.js => init-nedb.js} (60%) rename src/{client => app}/views/index.pug (89%) rename src/client/{entry => entry-web}/basic.js (60%) create mode 100644 src/client/entry-web/electerm.jsx create mode 100644 src/client/entry-web/worker.js delete mode 100644 src/client/entry/electerm.jsx delete mode 100644 src/client/entry/worker.js create mode 100644 src/client/file-select-dialog/file-item.jsx create mode 100644 src/client/file-select-dialog/file-select-dialog.jsx create mode 100644 src/client/file-select-dialog/file-select-dialog.styl delete mode 100644 src/client/harmony/language-select.jsx delete mode 100644 src/client/harmony/language-select.styl delete mode 100644 src/client/harmony/main.jsx create mode 100644 src/client/simple-auth/logout.jsx create mode 100644 src/client/simple-auth/logout.styl create mode 100644 src/client/simple-auth/web-login.jsx create mode 100644 src/client/statics/favicon.ico create mode 100644 src/client/web-components/path.js create mode 100644 src/client/web-components/store-login.js create mode 100644 src/client/web-components/style-overide.styl create mode 100644 src/client/web-components/web-api.js create mode 100644 src/client/web-components/web-main.jsx create mode 100644 src/client/web-components/web-pre.js create mode 100644 src/client/web-components/web-store.js diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml new file mode 100644 index 0000000..45e5fb0 --- /dev/null +++ b/.github/workflows/build-web.yml @@ -0,0 +1,279 @@ +name: Build HarmonyOS Web APP (ArkWeb + on-device Node.js) + +# Web variant of the app — no electron-harmony runtime: +# ArkWeb (Web component) + hqzing/ohos-node running the electerm-web +# backend as a native child process (childProcessManager.startNativeChildProcess). +# +# Triggers on dev2 pushes. Uploads the signed .app as an artifact. + +on: + push: + branches: + - dev2 + workflow_dispatch: + +# Cancel previous runs on the same branch +concurrency: + group: build-web-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # hqzing/ohos-node release used as the on-device Node.js runtime. + # Keep in sync with the default in scripts/prepare-node.sh. + NODE_VERSION: '24.19.0' + +jobs: + build: + # HarmonyOS Command Line Tools are x64-only. + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + # ── Checkout ────────────────────────────────────────────────────────── + - name: Checkout electerm-harmony + uses: actions/checkout@v4 + + # ── Setup Node.js (for building the web app) ───────────────────────── + - name: Setup Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + + # ── Install system deps ─────────────────────────────────────────────── + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + unzip \ + jq \ + xz-utils \ + python3 \ + make \ + g++ + + # ── Setup JDK (for hap-sign-tool.jar) ──────────────────────────────── + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + # ── Step 1: Cache / download HarmonyOS Command Line Tools (~2 GB) ──── + - name: Compute Command Line Tools cache key + id: cmdkey + env: + OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} + run: | + if [ -z "${OHOS_CMDLINE_TOOLS_URL}" ]; then + echo "::error::OHOS_CMDLINE_TOOLS_URL secret is not set." + exit 1 + fi + HASH="$(echo -n "${OHOS_CMDLINE_TOOLS_URL}" | md5sum | cut -d' ' -f1)" + echo "key=cmdline-tools-${HASH}" >> "$GITHUB_OUTPUT" + + - name: Restore HarmonyOS Command Line Tools cache + id: cmdline_cache + uses: actions/cache/restore@v4 + with: + path: .cache/commandline-tools + key: ${{ steps.cmdkey.outputs.key }} + + - name: Download & extract HarmonyOS Command Line Tools + if: steps.cmdline_cache.outputs.cache-hit != 'true' + env: + OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} + run: | + set -euo pipefail + mkdir -p .cache + ZIP=".cache/commandline-tools.zip" + echo "Cache miss — downloading HarmonyOS Command Line Tools (~2 GB) ..." + curl -L --retry 10 --retry-all-errors --retry-delay 5 -C - \ + -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ + -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" + echo "Verifying archive integrity ..." + if ! unzip -t "$ZIP" >/dev/null 2>&1; then + echo "::error::Downloaded archive is corrupt; retrying once without resume." + curl -L --retry 10 --retry-all-errors --retry-delay 5 \ + -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ + -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" + unzip -t "$ZIP" >/dev/null 2>&1 || { echo "::error::Still corrupt after retry"; exit 1; } + fi + rm -rf .cache/commandline-tools + mkdir -p .cache/commandline-tools + unzip -o -q "$ZIP" -d .cache/commandline-tools + rm -f "$ZIP" + echo "Downloaded and extracted HarmonyOS Command Line Tools" + + - name: Save HarmonyOS Command Line Tools cache + if: steps.cmdline_cache.outputs.cache-hit != 'true' && always() + uses: actions/cache/save@v4 + with: + path: .cache/commandline-tools + key: ${{ steps.cmdkey.outputs.key }} + + - name: Configure Command Line Tools environment + run: | + set -euo pipefail + COMMANDLINE_TOOLS="$(pwd)/.cache/commandline-tools/command-line-tools" + if [ ! -d "$COMMANDLINE_TOOLS" ]; then + COMMANDLINE_TOOLS="$(cd "$(dirname "$(find .cache/commandline-tools -name ohpm -type f | head -1)")/.." && pwd)" + fi + # Fix: Project root package.json has "type": "module", which makes + # Node.js treat hvigorw.js as an ES module (breaks with "require is + # not defined"). Adding a CommonJS package.json to the tools dirs + # prevents Node from traversing up to the project root. + echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/hvigor/package.json" 2>/dev/null || true + echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/package.json" + echo "COMMANDLINE_TOOLS=$COMMANDLINE_TOOLS" >> "$GITHUB_ENV" + echo "OHOS_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" + echo "DEVECO_NODE_HOME=$COMMANDLINE_TOOLS/tool/node" >> "$GITHUB_ENV" + echo "DEVECO_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" + echo "$COMMANDLINE_TOOLS/bin" >> "$GITHUB_PATH" + echo "$COMMANDLINE_TOOLS/hvigor/bin" >> "$GITHUB_PATH" + echo "HarmonyOS Command Line Tools ready at $COMMANDLINE_TOOLS" + + - name: Configure ohpm registry + run: | + ohpm config set registry https://ohpm.openharmony.cn/ohpm/ || true + ohpm --version || true + + - name: Restore ohpm modules cache + id: ohpm_cache + uses: actions/cache/restore@v4 + with: + path: | + oh_modules + entry/oh_modules + ~/.ohpm + key: ohpm-web-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} + restore-keys: | + ohpm-web- + + # ── Step 2: Prepare the OpenHarmony Node.js runtime ─────────────────── + # hqzing/ohos-node prebuilt binary → entry/libs/arm64-v8a/libnode.so + - name: Restore Node runtime cache + id: node_cache + uses: actions/cache/restore@v4 + with: + path: .cache/node-runtime + key: ohos-node-${{ env.NODE_VERSION }} + + - name: Prepare Node.js runtime (hqzing/ohos-node) + run: ./scripts/prepare-node.sh + + - name: Save Node runtime cache + if: steps.node_cache.outputs.cache-hit != 'true' && always() + uses: actions/cache/save@v4 + with: + path: .cache/node-runtime + key: ohos-node-${{ env.NODE_VERSION }} + + # ── Step 3: Build web app (frontend + backend bundle → resfile) ────── + - name: Prepare web app + run: ./scripts/prepare-web.sh + env: + SERVER_SECRET: ${{ secrets.OHOS_SERVER_SECRET }} + + # ── Step 4: Decode signing materials ──────────────────────────────── + - name: Decode signing materials + env: + OHOS_KEYSTORE_B64: ${{ secrets.OHOS_KEYSTORE_B64 }} + OHOS_CERT_B64: ${{ secrets.OHOS_CERT_B64 }} + OHOS_PROFILE_B64: ${{ secrets.OHOS_PROFILE_B64 }} + run: | + mkdir -p signing + if [ -z "${OHOS_KEYSTORE_B64}" ] || [ -z "${OHOS_CERT_B64}" ] || [ -z "${OHOS_PROFILE_B64}" ]; then + echo "One or more signing material secrets are not set" + exit 1 + fi + echo "${OHOS_KEYSTORE_B64}" | base64 -d > signing/electerm.p12 + echo "${OHOS_CERT_B64}" | base64 -d > signing/electerm_publish.cer + echo "${OHOS_PROFILE_B64}" | base64 -d > signing/electermRelease.p7b + + for f in signing/electerm.p12 signing/electerm_publish.cer signing/electermRelease.p7b; do + if [ ! -s "${f}" ]; then + echo "::error::Failed to decode ${f} — check GitHub Secrets." + exit 1 + fi + echo " ✓ $(basename ${f}): $(du -h ${f} | cut -f1)" + done + + # ── Step 5: Set bundle name from secret ────────────────────────────── + - name: Configure bundle name + env: + BUNDLE_NAME: ${{ secrets.OHOS_BUNDLE_NAME }} + run: | + if [ -n "${BUNDLE_NAME}" ]; then + sed -i "s/\"bundleName\": \".*\"/\"bundleName\": \"${BUNDLE_NAME}\"/" \ + AppScope/app.json5 + echo "Bundle name set to: ${BUNDLE_NAME}" + else + echo "Using default bundle name from app.json5" + fi + cat AppScope/app.json5 + + # ── Step 6: Build & sign the APP ───────────────────────────────────── + - name: Build HarmonyOS web app + run: ./scripts/build-web-app.sh --release + env: + COMMANDLINE_TOOLS: ${{ env.COMMANDLINE_TOOLS }} + OHOS_SDK_HOME: ${{ env.OHOS_SDK_HOME }} + KEYSTORE_PASSWORD: ${{ secrets.OHOS_KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.OHOS_KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.OHOS_KEY_ALIAS }} + + - name: Save ohpm modules cache + if: steps.ohpm_cache.outputs.cache-hit != 'true' && always() + uses: actions/cache/save@v4 + with: + path: | + oh_modules + entry/oh_modules + ~/.ohpm + key: ohpm-web-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} + + # ── Step 7: Upload artifact ────────────────────────────────────────── + - name: Find APP file + id: find_app + run: | + APP_FILE=$(find build/outputs -name "*.app" -type f | head -1) + if [ -z "${APP_FILE}" ]; then + echo "::error::No .app file found!" + exit 1 + fi + APP_NAME=$(basename "${APP_FILE}") + ARTIFACT_NAME="${APP_NAME%.app}" + echo "app_path=${APP_FILE}" >> $GITHUB_OUTPUT + echo "app_name=${APP_NAME}" >> $GITHUB_OUTPUT + echo "artifact_name=${ARTIFACT_NAME}-web" >> $GITHUB_OUTPUT + echo "Found APP: ${APP_FILE} ($(du -h ${APP_FILE} | cut -f1))" + + - name: Upload APP artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.find_app.outputs.artifact_name }} + path: ${{ steps.find_app.outputs.app_path }} + retention-days: 30 + + # ── Summary ────────────────────────────────────────────────────────── + - name: Build summary + if: always() + run: | + echo "## Build Summary (web)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | \`hqzing/ohos-node v24.19.0 (libnode.so)\` |" >> $GITHUB_STEP_SUMMARY + echo "| Web app | \`electerm-web backend + ArkWeb frontend\` |" >> $GITHUB_STEP_SUMMARY + echo "| App version | \`$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo unknown)\` |" >> $GITHUB_STEP_SUMMARY + if [ -f "${{ steps.find_app.outputs.app_path }}" ]; then + echo "| APP file | \`${{ steps.find_app.outputs.app_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| APP size | \`$(du -h ${{ steps.find_app.outputs.app_path }} | cut -f1)\` |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitignore b/.gitignore index 7593fb6..b538d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,9 @@ src/client/electerm-react/ entry/src/main/resources/rawfile /src/client/electerm-react/ /data -.workbuddy \ No newline at end of file +.workbuddy +# --- Web (ArkWeb) build outputs --- +# electerm-web app bundled into the entry module resfile by build/web/build.mjs +/entry/src/main/resources/resfile/ +# node runtime download cache +/.cache/ diff --git a/build/bin/.yarnclean b/build/bin/.yarnclean deleted file mode 100644 index bf77825..0000000 --- a/build/bin/.yarnclean +++ /dev/null @@ -1,70 +0,0 @@ -# files -Makefile -Gulpfile.js -Gruntfile.js -.tern-project -.gitattributes -.editorconfig -.eslintrc -.jshintrc -.flowconfig -.documentup.json -.yarn-metadata.json -.travis.yml -appveyor.yml -LICENSE.txt -LICENSE -AUTHORS -CONTRIBUTORS -.yarn-integrity -*.md -*.ts -*.js.map -*.ts.map -*.jst -*.coffee -*.d.cts -*.d.mts -tsconfig.json -.nycrc -.nycrc.json -opslevel.yml -package-support.json -bench.js -tests.js - -# folders -__tests__ -test -tests -powered-test -docs -doc -website -images -assets -example -examples -coverage -.nyc_output -dist/esm -zmodem2/dist/cjs -zmodem2/dist/browser -zmodem2/dist/esm -trzsz2/dist/cjs -trzsz2/dist/esm -package-lock.json -.github -.circleci -scripts -samplejson -flash -third_party -tools -bench -benchmarks -spec -specs -fixture -fixtures -umd \ No newline at end of file diff --git a/build/bin/app.js b/build/bin/app.js deleted file mode 100644 index 26d4f5c..0000000 --- a/build/bin/app.js +++ /dev/null @@ -1,13 +0,0 @@ -const { exec } = require('shelljs') -const os = require('os') -const platform = os.platform() -console.log('platform:', platform) - -// Clear ELECTRON_RUN_AS_NODE so electron runs in full Electron mode -// (not pure Node.js mode where require('electron').app is undefined) -delete process.env.ELECTRON_RUN_AS_NODE - -const cmd = platform.startsWith('win') - ? 'node_modules\\.bin\\cross-env NODE_ENV=development node_modules\\.bin\\electron -r dotenv/config src\\app\\app' - : 'node_modules/.bin/cross-env NODE_ENV=development node_modules/.bin/electron -r dotenv/config src/app/app' -exec(cmd, { env: process.env }) diff --git a/build/bin/build.js b/build/bin/build.js deleted file mode 100644 index 28f708a..0000000 --- a/build/bin/build.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * build - */ - -const { exec, echo } = require('shelljs') - -echo('start build') - -const timeStart = +new Date() - -// echo('clean') -// exec('npm run clean') -echo('version file') -echo('js/css file') -exec('npm run vite-build') -echo('copy file') -exec('node ./build/bin/copy.js') -echo('html file') -exec('node ./build/bin/pug.js') - -const endTime = +new Date() -echo(`done build in ${(endTime - timeStart) / 1000} s`) diff --git a/build/bin/clean-empty-folders.js b/build/bin/clean-empty-folders.js deleted file mode 100644 index 1f94ebb..0000000 --- a/build/bin/clean-empty-folders.js +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs') -const path = require('path') - -/** - * Clean empty folders recursively in a given directory - * @param {string} dirPath - The directory path to clean - * @returns {number} - Number of empty folders removed - */ -function cleanEmptyFolders (dirPath) { - let removedCount = 0 - - if (!fs.existsSync(dirPath)) { - console.log('Directory does not exist:', dirPath) - return removedCount - } - - try { - const items = fs.readdirSync(dirPath) - - // First, recursively clean subdirectories - for (const item of items) { - const itemPath = path.join(dirPath, item) - const stats = fs.statSync(itemPath) - - if (stats.isDirectory()) { - removedCount += cleanEmptyFolders(itemPath) - } - } - - // After cleaning subdirectories, check if current directory is now empty - const remainingItems = fs.readdirSync(dirPath) - if (remainingItems.length === 0) { - // Don't remove the root node_modules directory itself - const nodeModulesPath = path.resolve(process.cwd(), 'work/app/node_modules') - if (path.resolve(dirPath) !== nodeModulesPath) { - console.log('Removing empty directory:', dirPath) - fs.rmdirSync(dirPath) - removedCount++ - } - } - } catch (error) { - console.error('Error processing directory ' + dirPath + ':', error.message) - } - - return removedCount -} - -/** - * Main function to clean empty folders in work/app/node_modules - */ -function main () { - const targetDir = path.resolve(process.cwd(), 'work/app/node_modules') - - console.log('Starting cleanup of empty folders in:', targetDir) - console.log('='.repeat(60)) - - if (!fs.existsSync(targetDir)) { - console.log('Target directory does not exist:', targetDir) - return - } - - const startTime = Date.now() - const removedCount = cleanEmptyFolders(targetDir) - const endTime = Date.now() - - console.log('='.repeat(60)) - console.log('Cleanup completed!') - console.log('Empty folders removed:', removedCount) - console.log('Time taken:', endTime - startTime + 'ms') - - if (removedCount === 0) { - console.log('No empty folders found.') - } -} - -// Run the script if called directly -if (require.main === module) { - main() -} - -module.exports = { cleanEmptyFolders, main } diff --git a/build/bin/clean.js b/build/bin/clean.js deleted file mode 100644 index 0319c06..0000000 --- a/build/bin/clean.js +++ /dev/null @@ -1,5 +0,0 @@ -const { rm } = require('shelljs') - -rm('-rf', [ - 'work' -]) diff --git a/build/bin/copy.js b/build/bin/copy.js deleted file mode 100644 index 39fd6fb..0000000 --- a/build/bin/copy.js +++ /dev/null @@ -1,39 +0,0 @@ -const { resolve } = require('path') -const { cp } = require('shelljs') -const from = resolve( - __dirname, - '../../node_modules/@electerm/electerm-resource/tray-icons/*' -) -const from0 = resolve( - __dirname, - '../../node_modules/electerm-icons/icons' -) -const to1 = resolve( - __dirname, - '../../work/app/assets/images/' -) -const to2 = resolve( - __dirname, - '../../work/app/assets/icons' -) -const arr = [ - { - from, - to: to1, - file: true - }, { - from: from0, - to: to2 - } -] - -for (const obj of arr) { - const { - file, from, to - } = obj - if (file) { - cp(from, to) - } else { - cp('-r', from, to) - } -} diff --git a/build/bin/gen_logos.py b/build/bin/gen_logos.py deleted file mode 100755 index da1979e..0000000 --- a/build/bin/gen_logos.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate HarmonyOS app icons (entry/src/main/resources/base/media/*) - from the source logos in build/logos. - -Currently the square logo is resized to 1024x1024 RGBA and written as - both `app_icon.png` and `start_icon.png`, which are the icons referenced - by entry/src/main/module.json5 (`$media:app_icon`). - -Requirements: - - Python 3.7+ - - Pillow (pip install Pillow) - -Usage: - python3 build/bin/gen_logos.py -""" - -import sys -from pathlib import Path - -try: - from PIL import Image -except ImportError: - sys.exit( - 'Pillow is required. Install it with: pip install Pillow' - ) - -# Project root is two levels up from this script (build/bin -> build -> root) -ROOT = Path(__file__).resolve().parent.parent.parent - -# Source logo (square, high-resolution) -SOURCE = ROOT / 'build' / 'logos' / 'electerm-logo-square.png' - -# Output directory for HarmonyOS media resources -MEDIA_DIR = ROOT / 'entry' / 'src' / 'main' / 'resources' / 'base' / 'media' - -# Target icon size (HarmonyOS expects 1024x1024 app icons) -ICON_SIZE = (1024, 1024) - -# Output file names generated from the square logo -OUTPUTS = ['app_icon.png', 'start_icon.png'] - - -def main() -> int: - if not SOURCE.exists(): - print(f'error: source logo not found: {SOURCE}', file=sys.stderr) - return 1 - - MEDIA_DIR.mkdir(parents=True, exist_ok=True) - - print(f'gen_logos: opening {SOURCE.relative_to(ROOT)}') - with Image.open(SOURCE) as img: - print(f' source size: {img.size} mode: {img.mode}') - - # Resize to the target icon size with high-quality resampling - resized = img.resize(ICON_SIZE, Image.LANCZOS) - - # HarmonyOS icons are expected to be RGBA - if resized.mode != 'RGBA': - resized = resized.convert('RGBA') - - for name in OUTPUTS: - out_path = MEDIA_DIR / name - resized.save(out_path, 'PNG') - print(f' wrote {out_path.relative_to(ROOT)} ' - f'{resized.size} {resized.mode}') - - print('gen_logos: done') - return 0 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/build/bin/install.js b/build/bin/install.js deleted file mode 100644 index bd211da..0000000 --- a/build/bin/install.js +++ /dev/null @@ -1,9 +0,0 @@ -const pkg = require('shelljs') - -const { echo, rm, cp } = pkg - -echo('install required modules') - -rm('-rf', 'src/client/electerm-react') -cp('-r', 'node_modules/@electerm/electerm-react/client', 'src/client/electerm-react') -echo('done install required modules') diff --git a/build/bin/prepare.js b/build/bin/prepare.js deleted file mode 100644 index f436459..0000000 --- a/build/bin/prepare.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * prepare the files to be packed - */ - -const pack = require('../../package.json') -const os = require('os') -const { resolve } = require('path') -const { version } = pack -const { mkdir, rm, exec, echo, cp } = require('shelljs') -const dir = 'dist/v' + version -const cwd = process.cwd() - -const platform = os.platform() -const isWin = platform === 'win32' - -pack.main = 'app.js' -delete pack.scripts -delete pack.standard -delete pack.files -delete pack.engines -delete pack.preferGlobal - -if (isWin) { - delete pack.dependencies['node-bash'] -} else { - delete pack.dependencies['node-powershell'] -} - -echo('start pack prepare') -// echo('install test deps') -// exec(`PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D -E playwright@1.28.1 --no-save && npm i -D -E @playwright/test@1.28.1 --no-save`) -const timeStart = +new Date() -rm('-rf', dir) -rm('-rf', 'dist/latest') - -mkdir('-p', dir) -mkdir('-p', 'dist/latest') -cp('-r', 'src/app', 'work/') -rm('-rf', 'work/app/user-config.json') -rm('-rf', 'work/app/localstorage.json') -rm('-rf', 'work/app/nohup.out') -rm('-rf', 'work/app/assets/js/index*') -rm('-rf', 'work/app/assets/js/*.txt') -rm('-rf', 'node_modules/cpu-features') - -require('fs').writeFileSync( - resolve(__dirname, '../../work/app/package.json'), - JSON.stringify( - pack, null, 2 - ) -) - -exec(`cd work/app && npm i --omit=dev && cd ${cwd}`) -rm('-rf', 'work/app/node_modules/.bin') -// Remove axios browser/ESM builds and unnecessary files (keep only lib/ and node CJS) -rm('-rf', 'work/app/node_modules/axios/dist/esm') -rm('-rf', 'work/app/node_modules/axios/dist/browser') -rm('-rf', 'work/app/node_modules/axios/dist/*.js') -rm('-rf', 'work/app/node_modules/axios/dist/*.map') -rm('-rf', 'work/app/node_modules/axios/dist/node/*.map') -rm('-rf', 'work/app/node_modules/axios/index.d.cts') -rm('-rf', 'work/app/node_modules/axios/lib') - -// Remove cpu-features after npm prune to prevent rebuild issues -rm('-rf', 'node_modules/cpu-features') -rm('-rf', 'work/app/node_modules/cpu-features') - -// Clean up node-pty platform-specific files to reduce bundle size -if (isWin) { - // On Windows, remove Unix-specific files - rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.test.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.test.js.map') - rm('-rf', 'work/app/node_modules/node-pty/build/pty.target.mk') - rm('-rf', 'work/app/node_modules/node-pty/build/spawn-helper.target.mk') - rm('-rf', 'work/app/node_modules/node-pty/build/binding.Makefile') - rm('-rf', 'work/app/node_modules/node-pty/build/gyp-mac-tool') -} else { - // On Linux/Mac, remove Windows-specific files - rm('-rf', 'work/app/node_modules/node-pty/lib/conpty_console_list_agent.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/conpty_console_list_agent.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsConoutConnection.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsConoutConnection.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.test.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.test.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.js.map') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.test.js') - rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.test.js.map') - rm('-rf', 'work/app/node_modules/node-pty/deps/winpty') -} - -// Remove all test files from node-pty to reduce bundle size -rm('-rf', 'work/app/node_modules/node-pty/lib/*.test.js') -rm('-rf', 'work/app/node_modules/node-pty/lib/*.test.js.map') -rm('-rf', 'work/app/node_modules/node-pty/lib/testUtils.test.js') -rm('-rf', 'work/app/node_modules/node-pty/lib/testUtils.test.js.map') - -// yarn auto clean -cp('-r', 'build/bin/.yarnclean', 'work/app/') -exec(`cd work/app && yarn generate-lock-entry > yarn.lock && yarn autoclean --force && cd ${cwd}`) -rm('-rf', 'work/app/.yarnclean') -rm('-rf', 'work/app/package-lock.json') -rm('-rf', 'work/app/yarn.lock') -require('./clean-empty-folders').main() - -const endTime = +new Date() -echo(`done pack prepare in ${(endTime - timeStart) / 1000} s`) diff --git a/build/bin/pug.js b/build/bin/pug.js deleted file mode 100644 index 6c56a67..0000000 --- a/build/bin/pug.js +++ /dev/null @@ -1,48 +0,0 @@ -// build html -/** - * build common files with react module in it - */ -const fs = require('fs') -const pug = require('pug') -const { resolve } = require('path') -const pack = require('../../package.json') -const deepCopy = require('json-deep-copy') - -const entryPug = resolve( - __dirname, - '../../src/client/views/index.pug' -) -const targetFilePath = resolve( - __dirname, - '../../work/app/assets/index.html' -) -const pugContent = fs.readFileSync(entryPug, 'utf-8') -const defaultAIPreset = { - baseURLAI: 'https://ai.electerm.org/api/ai', - apiPathAI: '/chat/completions', - modelAI: 'mistral-small-latest', - authHeaderNameAI: 'Authorization: Bearer', - id: 'ai.electerm.org', - nameAI: 'ai.electerm.org(default free)' -} - -// const AIDisclamer = 'AI-generated terminal commands can be inaccurate or unsafe, be careful' - -const data = { - version: pack.version, - siteName: pack.name, - isDev: false, - disableUpgradeCheck: true, - hideLocalTerminal: true, - defaultAIPreset, - disableAIFeature: false, - AIDisclamer: '本内容由 AI 生成,仅供参考', - supportSessionTypes: ['ssh', 'telnet', 'rdp', 'vnc', 'ftp', 'spice'] -} - -const htmlContent = pug.render(pugContent, { - filename: entryPug, - ...data, - _global: deepCopy(data) -}) -fs.writeFileSync(targetFilePath, htmlContent, 'utf8') diff --git a/build/bin/release b/build/bin/release deleted file mode 100755 index f24d0c2..0000000 --- a/build/bin/release +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -cd `dirname $0` -cd ../.. -git co main -git pull -git pull -git delete-branch build -git create-branch build -git push origin build -u -git co - diff --git a/build/bin/start.js b/build/bin/start.js deleted file mode 100644 index b49311b..0000000 --- a/build/bin/start.js +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -const { exec, cd } = require('shelljs') -const { resolve } = require('path') -const p = resolve(__dirname, '../vite') -cd(p) -exec('npm start') diff --git a/build/bin/vite-build.js b/build/bin/vite-build.js deleted file mode 100755 index fefd460..0000000 --- a/build/bin/vite-build.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -const { exec, cd } = require('shelljs') -const { resolve } = require('path') -const p = resolve(__dirname, '../vite') -cd(p) - -exec('npm run build') diff --git a/build/harmony/build.js b/build/harmony/build.js deleted file mode 100644 index 55130b9..0000000 --- a/build/harmony/build.js +++ /dev/null @@ -1,349 +0,0 @@ -/** - * Build the electerm HarmonyOS app. - * - * Step 0: Copy client source from @electerm/electerm-react npm package - * → src/client/ (gitignored, not in repo) - * Step 1: Run complete electerm build (npm run b) - * clean → compile (vite + copy + pug) → prepare-file (deps install + cleanup) - * Step 2: Apply HarmonyOS delta (main → bootstrap.js, remove native modules) - * Step 3: Copy work/app → web_engine resfile - * Step 4: Verify critical files - * - * This is a CJS file to stay consistent with build/bin/*.js. - */ -const { exec, cp, echo } = require('shelljs') -const { resolve, join, dirname } = require('path') -const fs = require('fs') -const pack = require('../../package.json') - -// Ensure we run from project root (build/bin/*.js rely on cwd) -process.chdir(resolve(__dirname, '../..')) -const ROOT = process.cwd() - -// Load .env so SERVER_SECRET is available for build-time injection -// (called after chdir to ensure .env is found in project root) -try { - require('dotenv').config() -} catch (_) { - // dotenv may not be installed yet during very early runs -} -const WORK_APP = resolve(ROOT, 'work/app') -const OUTPUT_DIR = resolve(ROOT, 'web_engine/src/main/resources/resfile/resources/app') - -const timeStart = Date.now() - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function rmrf (p) { - if (fs.existsSync(p)) { - fs.rmSync(p, { recursive: true, force: true }) - } -} - -function getDirSize (dir) { - let size = 0 - try { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const p = join(dir, entry.name) - if (entry.isDirectory()) size += getDirSize(p) - else size += fs.statSync(p).size - } - } catch {} - return size -} - -function formatBytes (bytes) { - if (bytes < 1024) return bytes + ' B' - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' - return (bytes / (1024 * 1024)).toFixed(1) + ' MB' -} - -// --------------------------------------------------------------------------- -// Step 0: Copy client source from @electerm/electerm-react -// --------------------------------------------------------------------------- -// Layout of src/client/: -// - electerm-react/ ← gitignored, vendored from the npm package at build -// - entry/, harmony/, views/ ← tracked in git (HarmonyOS-specific overrides) -// Vite config and pug.js reference src/client/entry/*.jsx and -// src/client/views/index.pug (tracked), while entry/basic.js and -// harmony/main.jsx import the vendored source via ../electerm-react/... -// So we only need to populate src/client/electerm-react/ before `npm run b`, -// and must NOT overwrite or remove the tracked overrides. -// --------------------------------------------------------------------------- -function prepareClientSource () { - echo('[harmony] step 0: prepare client source from @electerm/electerm-react') - const pkgClient = resolve(ROOT, 'node_modules/@electerm/electerm-react/client') - const srcClient = resolve(ROOT, 'src/client') - const vendored = resolve(srcClient, 'electerm-react') - - if (!fs.existsSync(pkgClient)) { - throw new Error( - 'node_modules/@electerm/electerm-react/client not found. ' + - 'Run npm install first.' - ) - } - - // Skip when the vendored source is already present (local dev keeps a - // real checkout here). Do NOT test src/client/entry/electerm.jsx — that - // file is tracked in git, so it always exists in CI and would wrongly - // short-circuit the copy, leaving ../electerm-react/... unresolved. - if (fs.existsSync(resolve(vendored, 'components'))) { - echo(' ✓ src/client/electerm-react/ already populated, skip copy') - return - } - - // Copy client/ from the npm package into src/client/electerm-react/. - // The tracked overrides under src/client/{entry,harmony,views}/ are - // intentionally left untouched. - fs.mkdirSync(srcClient, { recursive: true }) - cp('-r', pkgClient, vendored) - echo(' ✓ copied @electerm/electerm-react/client → src/client/electerm-react') - - // Verify critical vendored files referenced by the tracked overrides - const required = [ - 'components/main/index.jsx', - 'common/pre.js', - 'css/basic.styl' - ] - for (const f of required) { - if (!fs.existsSync(resolve(vendored, f))) { - throw new Error(`Missing required vendored file: src/client/electerm-react/${f}`) - } - } - echo(' ✓ client source verified') -} - -// --------------------------------------------------------------------------- -// Step 1: Run complete electerm build (npm run b) -// --------------------------------------------------------------------------- -// npm run b = npm run clean && npm run compile && npm run prepare-file -// clean → removes work/ -// compile → vite-build + copy icons + pug → work/app/assets/ -// prepare-file → cp src/app → work/app, create package.json, npm install, -// cleanup (axios, node-pty, cpu-features, yarn autoclean, -// clean-empty-folders) -// --------------------------------------------------------------------------- -function buildElecterm () { - echo('[harmony] step 1: run complete electerm build (npm run b)') - const result = exec('npm run b') - if (result.code !== 0) { - throw new Error(`npm run b failed with exit code ${result.code}`) - } - echo(' ✓ electerm build complete') -} - -// --------------------------------------------------------------------------- -// Step 2: Apply HarmonyOS-specific delta -// --------------------------------------------------------------------------- -// electerm's prepare.js produces work/app with: -// - main: 'app.js' → harmony needs 'bootstrap.js' -// - node-pty, serialport, cpu-features installed → harmony excludes them -// (source has try/catch guards for missing native modules) -// --------------------------------------------------------------------------- -function applyHarmonyDelta () { - echo('[harmony] step 2: apply HarmonyOS delta') - - // 2a. Rewrite package.json for HarmonyOS - const workPkg = JSON.parse( - fs.readFileSync(resolve(WORK_APP, 'package.json'), 'utf8') - ) - workPkg.main = 'bootstrap.js' - delete workPkg.dependencies['node-pty'] - delete workPkg.dependencies.serialport - delete workPkg.dependencies['cpu-features'] - fs.writeFileSync( - resolve(WORK_APP, 'package.json'), - JSON.stringify(workPkg, null, 2) - ) - echo(' ✓ package.json: main = bootstrap.js, native modules excluded') - - // 2b. Remove native module directories (not usable on HarmonyOS) - const nativeModules = ['node-pty', 'serialport', 'cpu-features'] - for (const mod of nativeModules) { - const modPath = resolve(WORK_APP, 'node_modules', mod) - if (fs.existsSync(modPath)) { - rmrf(modPath) - echo(` ✓ removed node_modules/${mod}`) - } - } - - // 2c. Inject SERVER_SECRET into safe-storage.js - // The backend code is copied as-is (not bundled by vite), so - // process.env.SERVER_SECRET is NOT available at runtime. - // We replace the default placeholder at build time so the - // production secret is baked into the output. - // JSON.stringify ensures the value is safely escaped for JS. - const safeStoragePath = resolve(WORK_APP, 'lib/safe-storage.js') - if (fs.existsSync(safeStoragePath) && process.env.SERVER_SECRET) { - let safeSrc = fs.readFileSync(safeStoragePath, 'utf8') - const escaped = JSON.stringify(process.env.SERVER_SECRET) - safeSrc = safeSrc.replace( - "'static-secret-string-safe-storage'", - escaped - ) - fs.writeFileSync(safeStoragePath, safeSrc, 'utf8') - echo(' ✓ safe-storage.js: SERVER_SECRET injected') - } else { - echo(' ⚠ safe-storage.js: using default secret (SERVER_SECRET not set)') - } - - // 2d. Remove .env (not needed in the packed app) - rmrf(resolve(WORK_APP, '.env')) - rmrf(resolve(WORK_APP, '.env.bak')) - - echo(' ✓ HarmonyOS delta applied') -} - -// --------------------------------------------------------------------------- -// Step 3: Copy work/app → web_engine resfile -// --------------------------------------------------------------------------- -function copyToResfile () { - echo('[harmony] step 3: copy work/app → web_engine resfile') - - const webEngineDir = resolve(ROOT, 'web_engine') - if (!fs.existsSync(webEngineDir)) { - throw new Error( - 'web_engine/ not found. Run ./scripts/prepare-electron-runtime.sh first.' - ) - } - - rmrf(OUTPUT_DIR) - const parentDir = dirname(OUTPUT_DIR) - fs.mkdirSync(parentDir, { recursive: true }) - cp('-r', WORK_APP, parentDir) - - echo(` ✓ copied to ${OUTPUT_DIR}`) - echo(` ✓ bundled size: ${formatBytes(getDirSize(OUTPUT_DIR))}`) -} - -// --------------------------------------------------------------------------- -// Step 4: Verify critical files -// --------------------------------------------------------------------------- -function verify (label, dir) { - echo(`[harmony] verify: ${label}`) - - const checks = [ - { path: 'assets/index.html', desc: 'index.html' }, - { path: 'bootstrap.js', desc: 'bootstrap.js' }, - { path: 'app.js', desc: 'app.js' }, - { path: 'package.json', desc: 'package.json' }, - { path: 'server/server.js', desc: 'server.js' }, - { path: 'lib/file-server.js', desc: 'file-server.js' } - ] - - let failed = false - for (const check of checks) { - const fullPath = resolve(dir, check.path) - if (!fs.existsSync(fullPath)) { - echo(` ✗ MISSING: ${check.path}`) - failed = true - } else { - echo(` ✓ ${check.desc}`) - } - } - - // Check assets/js/ has JS files - const jsDir = resolve(dir, 'assets/js') - if (!fs.existsSync(jsDir)) { - echo(' ✗ MISSING: assets/js/ directory') - failed = true - } else { - const jsFiles = fs.readdirSync(jsDir).filter(f => f.endsWith('.js')) - if (jsFiles.length === 0) { - echo(' ✗ MISSING: no .js files in assets/js/') - failed = true - } else { - echo(` ✓ assets/js/ (${jsFiles.length} files: ${jsFiles.join(', ')})`) - } - } - - // Check assets/css/ has CSS files - const cssDir = resolve(dir, 'assets/css') - if (!fs.existsSync(cssDir)) { - echo(' ✗ MISSING: assets/css/ directory') - failed = true - } else { - const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith('.css')) - if (cssFiles.length === 0) { - echo(' ✗ MISSING: no .css files in assets/css/') - failed = true - } else { - echo(` ✓ assets/css/ (${cssFiles.length} files: ${cssFiles.join(', ')})`) - } - } - - // Check assets/chunk/ has chunk files - const chunkDir = resolve(dir, 'assets/chunk') - if (!fs.existsSync(chunkDir)) { - echo(' ✗ MISSING: assets/chunk/ directory') - failed = true - } else { - const chunkFiles = fs.readdirSync(chunkDir) - echo(` ✓ assets/chunk/ (${chunkFiles.length} files)`) - } - - // Check package.json has main: bootstrap.js - const pkgPath = resolve(dir, 'package.json') - if (fs.existsSync(pkgPath)) { - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) - if (pkg.main !== 'bootstrap.js') { - echo(` ✗ package.json main should be "bootstrap.js", got "${pkg.main}"`) - failed = true - } else { - echo(' ✓ package.json main = bootstrap.js') - } - } - - // Check node_modules exists - if (!fs.existsSync(resolve(dir, 'node_modules'))) { - echo(' ✗ MISSING: node_modules/') - failed = true - } else { - echo(' ✓ node_modules/ exists') - } - - if (failed) { - echo(`\n[harmony] VERIFICATION FAILED for ${label}!`) - throw new Error(`Verification failed for ${label}`) - } - - echo(` ✓ ${label} verification passed`) -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- -function main () { - echo('[harmony] building electerm HarmonyOS app...') - echo(`[harmony] version: ${pack.version}`) - echo('[harmony] mode: reuse electerm build (npm run b) + harmony delta') - echo('') - - // Step 0: Prepare client source from npm package - prepareClientSource() - - // Step 1: Complete electerm build - buildElecterm() - - // Step 2: Apply HarmonyOS-specific changes - applyHarmonyDelta() - - // Verify work/app before copying - verify('work/app', WORK_APP) - - // Step 3: Copy to resfile - copyToResfile() - - // Verify resfile after copying - verify('resfile', OUTPUT_DIR) - - const elapsed = ((Date.now() - timeStart) / 1000).toFixed(1) - echo('') - echo(`[harmony] build complete in ${elapsed}s`) - echo(`[harmony] output: ${OUTPUT_DIR}`) - echo(`[harmony] total size: ${formatBytes(getDirSize(OUTPUT_DIR))}`) -} - -main() diff --git a/build/logos/electerm-banner-logo.png b/build/logos/electerm-banner-logo.png deleted file mode 100644 index 2fcaf6256a89480e442030730cacbbc21f127899..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6080 zcmds5cU05clmF7h28azrk>W!ErGzSh5IRanh~d#9C6G`O2%(7(LXsBYSsv@4?tpw51(%QsO zQBeRX6oSHWM7J;nT+oqU2uA224+1uTh{fY1Hxb?3@xerxG^pvHOYje{wEPPg7xdFm zpvjcN+yay!ipom<{+sLiK^;UqgZ_6l{+2q(K0E-ebOs%S4<>kk`{8xu7c*$Pe-^Y! z2xpH-SJPTAG?FMyf`J zCK@VQ$_6UxzcBue*FaTYQwyS@stEx%0%8KuS2k2JP**q5f*6|^niw1X;ysNEBD&!` z(7)tjLAgJ9RsN9|YDhr45%C0jJl^k@2iRcnM0^kiA0TOHry(he#Ns^hp+Rz++wi!C%3H#(@z-07C=1cP$10_AH(@(zg$z%nd~p zyC5R@7sa{5Z;0Sq_%89qJIqL$37e5^?z9>XNXnjq`indIn0oglrcvT5x=F1T)M!m(rGHa2H)=PX+83r)K}woRQ$@EdDo5GgUc@`3#va%s+M~3 zY+IdU4b09h!A7& z;xF|7Ev)~>(*6s*cJ>XM0Us?&z1myd;YW`i6-;Q?&U86MZ9SX6qUFv1sI;Z!biG8h z*VW=$WsUc<#iR+rjAO`rLA`Wd5|DF{KG`%ehY>Mm_F|Up`Scd~A0-sSwKd2{sy1`d z)G1SbQ|vch_HB>lXL+y?Qf~aFdGG9@C*?Op`IzQy1x=x+<;J-RogzQhDMAPX^|km{ z7TV1p4UHX-kI|qG8os|G??#g2-cFiDH1+XJGX zBUE5_B3ygZZyvL(#d4Vf^%Ca=_ZQ_-LU-AX!p)2&Q&uPVO4ba@UgdE~G7O6@y8m9z za;qCh@IEiInc~-5BqtYVH1U(QuTIaibLF`~PqDM=M;%J}7Tiy|UBmO|*xHb1*s)w< zi^V{A-VYL!{tTD$g()(GiBGCo7K=nblNCf6ltPeGrD7mtzEq|HL~zY#1zUbg%%5%uLm#%&TM3E~Y1O5k7SCr1K?-R4anY0c@tB6qt#qz za0l>>lF8fMR~8pxo^g9fq@>;{lQ+5}rHRD0Zoa0ac)8c79Jd;s$6`$TR0x-)qGr zHX6jMqp}VjAqNZRLDzCILL!Uxkr+R=WAeM_Nj0bg?*{L7(vh)4W|}>UbIab&C3ZGh zs~xXpdc%qe@4R7F?a;bMETzYSVV1zUhq>?3dL^nd@ab*kmf{QX|1zZR+a(dGdC6qmZeLj$TjRPg>^HyceP|DLFPT^L#c0L^iF5mG zetAfbwh9Dt^K3ws66Ip0(-9p}mY1=j_8!(`C)7S6i$61M*QAH}mdP)MC2Q|@`g~u) zz@OIM$Zm`Z(PW_uY+Xpv{le6z)2?Fu7~p7xh7kVG2wCqg}xzww`vdfOw2?S&BM=D9IQR z&x%^NQPkcYoRP51x;oksJ#;8?7GL#xK3iBQqm35!?CSV59a^at*`oBt^B%N`#&Ip&gOG~upu5Y@Zq_&;8#L)@PF=ZyW zjg&ZkO;->b^BwwfNYf0b8FJ6lk<1^wPi#)Be&mX1tlN@BtzLdFAx^o-_gi=CknAfy zsxzFUF)gp=0Lhn67TZXk%8K^g?%A+2w7vY)wpO>TtpRPwm?q_>43{M*Bj{)3HrYWx zlmQ<}WNf}IcnFga37!>i4rP!&%6e4CB690ViBSTJyM#Mv7F8j4arPCM!q`01&WeLAHd8W0groo0N4Ru`XGEH=M*-{xn_9eGS-Nh)2JLATo`_FYEZ^of-(>(usjxy9z- zF?Q@+kfZp)JqgY#y;BZ2St2F7r>srJ|8 z%*b4yks%I_hlh5X?+QCtNhM*8Z>+eDK564p2oZd6|NcpcVtlCgbGSED=0>?k(C3>k zL!P9#76qDl8HrgDg34wyhxTU(TOPiJq_m&%MQ&yHMvQSY9C{BYYqV-k#$ezQR3n!{ z+ZqwpBs`xUkmV{7Z5_&$Y_Yt6d+MrNCKnkk@zwl@=R(&@*Vif))`?;8F7@%?&P1Zw zmZJjY8;;`i+Wl@LAK&Y=Bf0BI*s}aI*L4N}vpP^W*{YUCEfYC1bmDcPAQN@1xc2Pn zfQwtAT~v!;mNYyfqKz#eOwX_v1XHAFLiX=rU1==e*#}iO(ED}(k9KjY4f{!YTJfVV z5@O(^?t+V?vk2<6X3_Ox5}5#IgfDfuzB%ln=Hx|&a_F(vwcB(ZxPTD1@G_?(bNI9I zCCcX8K;UybjYn&Z_cgWdmbqV3eFw~M!AU^WyYa-=qhOM&=Y4xKDYUes*taw>cCdkj zk`*&R{H{icIkt3N=QjpV9qmD>IObN_yTNar!~H(xiS$A+`E6bDTj7X27|=)%*|)(3 zoSX2!N?+}Kc8#7SOnuFl{@?WGsd)GGt9vszPpaYpDQU!+j9l=2Corh%k9F!`E2GRt`xR}5Fum^6t%R;L{Utzw~c zsyzv)u$8&5pJdE*@2x2%;e{fJA%4ToajfM`g~Oi`g;W`2{{14;$AP+?4-Z-{dVlP? zF?ukco}j~_P-!W`^lk$WvQgDZiguQ`PW&ZNuf>s^4ggTr+Y!4c_9DAZ!mW1p@mFC6 z3QRgW72`wRKD~~9Dg<0oGu;*~Me)u-Npf>aU1C{TGmkRn=Q$Qujq(r%dF_m5xCD^! zw#E!dNZHCX&!_K7SHp4scs1`WR}XJbUQf$|p=Oc^50&=Gj>Xgzhz+aNCpW6ksUY~@ zu}j@*`%_VIEL402a-KOz8_h`1PJd{CeWJLv?FJEoQpxUnNFD?}$FkN+*6u>*GF;UC z!aVj|B0W?J_6fW!p_zR<^-#*~I2r+I>hdYvSA+fL`+3wv%KEpORT`9QGZbzLcrGQsYu%5%%%1ol~ zqIdg=;qxiWPtxsv7d#9=KR@L?+!S|{pMx_XfxDFQBlUMB={L;!nMh+Q6wLJay_8y# zrbFF&>Pcd5aDf4>a-WX75AVTspFV=7ASJ{uL~cKrDhO4M*fx5p{CXW{ebcn%NWrP* z4e9Jsit&|jKmgkYVzjpsX(F@1IUYAeGk!vWmm4)iILlxZ}>%n9X%7<$Y z+k3W;1N_R7wkId_;nypD15 z4nuYMT&XS0M=yMzQkA$y4T@x^($2Zmmiio;c|T^Zi$cNItJ!n&NvMJ%zN!czrotHQ z$@E>)NkcgEGG-MW@_OM~h4gy=j4tUzPUVUR2lJk&f9`8Ne8W`zH7c1LcX8-(mJ@%^ zA4|6ksskxB58Ltv{C$9+-iBJezXqOoO`G+zUiV(rC#9Q(d)mIA9S^Xg+N`8Zyh_2W zjFQI-C~3A$>vX5C=d^iLvX41kIEAh+Ucb*z-+l!E zLi54AWVlv!VT}#>E&{H+W%4T}uX<)tRa`ff{`K1Kc$=b_a2q(>!`|Pvl$f!EH-gYeSIjNi0dGf3A9WN z&%8-m;J1QoWz$2;&Q9MVPHV}$i=VvTYP+8NME&P{o00yDbYx6H+nE^m6Y?7j*MNBa z#kj6E1bA4N-UW7TJpLVwd?5OSjHOWm$8S01p`I=A!w<841$zq8!nhDCq( zx8H34yvXzx7y6*&o#2He#2!dh0f4FDVS#~>W0|lsi);OAF?}1W7>Pq&zOa)q<4k)5 zeWto0J^bqgMyQGyLPX{;SE#zflUUXr9u=)baxBA9)1<0sv}T=g1GO+CMb9FOowxe5 zpAMDzY-7ewRoBv*CUOIMUp7ms|J!$>|1S?yde7L%Xf7Z^&Oh{E|-oWN;gE2C!y H=VJa1wU^l_ diff --git a/build/logos/electerm-logo-square.png b/build/logos/electerm-logo-square.png deleted file mode 100644 index 21d3124b9c2cc75fb036df85a5575639ae98275c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44759 zcmeEuWmweP_wS|zQE{*(WCT2jN{OVzAR&r`fP$n!clW?xq8!pKLGj_b?U~cDRZfk?bcD!-Z*4ast9Vp$pg0-Ep^1mC~IR0T2u#C^`h8>>( zFF&8PHFhuZY)2<`Gi__Lj4?v5nIOIZ1Z-gxAR2NL=B}X#p`=ArS#NVHpJh0Rb@)k<)U5atKAt$<{&B*4FAD4Nx<)b+UCdv$ey^Y6#ugxr?!pf`hF!p3Gly^Z(&|0Wnd9bHV~Zx#$_}-ZJv?qVlI@&z+N1;6HOtMvVQ> zTBH9Lp78-Oe3)|lAIkF25NIIw>%XcGfBb7a#x`Iv4qzG{0asKIN}#L1iolnV#4K7s-_b<#k88&B}Z)V<}Zy$vvjR9 z#s`BcSM?U1f4qv?u(v6Cc*$#}ysQYbdW5=*!p>~{MzZCKBM%{J85;60c)Al?12Sq9 zk~R5c&@s{09yoF#ZRi{{sgYK>80H{09y|4gO2G{{si02LFMB{~y4?-9Sc=_0&R? z@cB3K!y&D$t?|RdH+HGi)Ge)Lf@gi^PM;N# z7LoQz7xR!j+fjXljlp}pn09h1%VW>2ZLy}NMVrYB780H9?Vg2;R@$Dm+f<|2x)@e( z2#2uwY;ao#T*xgTtxNt zSf1W*DK9td@nIoaoDt7T;ZL;POx5b0?k&sncXe@bb&cF9eS|pQ*XJ@=*Zb?2scQcL zM2cFXHQ|7*m-QnK?GadzOzFa^rr#Ojx%sIbbe&FTCd>vI)!(E%d{YzE``dS;fbhB(a3*CGBJWNLy5~hDf zBjk-`ueV&m;S{%zh; zB-yY%m#Ab&f!rRGPiHHitE&21t;8J6Yip3}<)B}XA|)VjxIh+0Zf}lZCTkA?;-MigBf{E3YYz7_q`S!#0GZpT)e(SpzX1T79yhw;zUmr2` z$#dyXD=Cr17DkaP*S5udeSK~B<|~WVe@VCI?VUQlgKD$)(qD@dzIW{mkKSeeLhM?6 z;^&&seD!2=T4ci2aSL6~Bh+ge z`<@grS(v7}7m+wlV>NOX|NP8xkAnvnM;g{#%;bja@?1S~SHJBIHgKD0J?|p9G$flB z+O<%y{t}N6TRB!|x6`D5{Aguiv@7+^%=TR&+2=;*H`9vhTD_RaEYiMcy?YxqanzDvNLDhSO09Lj{kxSX{b94oq1k(tlBo@+O z6mbaejuA-WeAbd_ePNjNY~#U!=g;altlRr7Da!_bxh`0@J9ITyhq>6a>*(s9q(cEy zJE0XpU{_L7<>MXJ62Jaq)%3N~NWJ`Cy5yDIT>afPU8(jZ>tQE$k9G0E^f>bL*TZUR z{MV;`)KlEqw)04RtnCGUp(}gMXr4TI;xCo%=-8jTDsr_j%{B^;2)6~Wd)xCJ+PN^S zt1fP5w<+_Wlm6@hbIQy6=snZ9a#gNUD7H2g$luxROn_H z)(`TluT#XUbtF4YrO0wrL?10lO-&Q>eVZ{kp4IYVb6RzAZJ{aHP@M|-)yW>hPWa(* z9`8#?mij7tKbV#2@_mNN^6CcaCBpH2Z5g*#3yRjGtQT!;@Mr>R-x}9udAC|{IiganOuXcUhShxLkg9iDCQI?q`jDE%tHf06WT;}Ic5YQBM8?`gE@ zq?GZSZ+{;Wbmc6~grDnlw=SbF2XwpMfY2+=HfA0kv@GQ&^zXIL+8_?`CZ8yX8|ks-ZJ z1y>TaIqDxt9J#=aNG~yWMfft(_Hp&q_MmT5kSD>=>pQv#r*i@ynk~e?W`ej z`517t_ZD_`sfQmM-E#&OC$sa1rR(Gi(1y+}*%CJIRq4&;*(lbZMTWaj_bdNQ%!34= z`Ub)K{!EfCU)~1ZD$A;@JxJZ%tH~-lWmuFw8o9ZAMOO9>6Z%{yh*7~(Y1`%9_jCHz zUzbiWi;43{>f=an(ifYwCRWN>!%nu`J9iYQc!SB6wA2u<6l}jY_U{PMoXzNBo2Qg^ z9Y;KuV%U5(^Cq-*BbHs536(yHEO=a|5`M*>AvIBLUfGj9^i zndLsFR5w?Tdg6a1Z+`x?m+MK`nRiNA6StAvxhCIri~*ZVd7IW&R+?03Y3Pk0dFN97qmVWP%WcQ53) zyTi;>wd3h4i$$x;5w0v-97`sPYo)X6@?RvrQKp0Y6k zz^he^u{a@@eI)IL?@6>iQ{%P;;G;l2t+7vjtN_aup0bMEs7N2MO-mg+jy@$6CQV3B ztPF2%wDA2(Jc7tFKe>GU{9#F(a|$N!@o2gyBzD1TGt*Y8U~oG(!iSKlNv*H1S9p3@ z#J+9kS$e{ng@wh+ywAp}Pv?BY%8G{&!i$lovEPo5FR4&ai4?FLqh35up@`#?SR5Pg zS{u<&mEfg8oJTP}+|{^vv7-Bi@!nXKFi~d@6`M#r+CK0j{zTE*SD%jg1_+?AV?5?Q z@GQF%seCu?Y*P&tcKq@@9wIdzWx&r!;!1pbk-g^AUjRFP!Ibc`fLT|}_4@Q-4U?(~-8RtW&?}&>C6k?*_P>~bGoP?iyT7M8I($zDJmxM=N-`k{rt-hHg#=g(hp+}0Em`na#c0oyrS2NtdUdecX^K9%wt4rh{f*HN)a!XuVtGweRQohDl?+Y)m$Osjd1kHu|6WrG{a%!P{|leJiX@&VWP1g}vdlJNM< zESGAOkacr-kM}zZ^y6IbUnVkbpxa)v?Zaj;{_VNe{-qb!Q&2z8vRVamd2$L5%?m4hYke>ynLBCl6n$g zf(uw`$hveeX1)=y`A_n_-44J+ixPf+3KC$pHvxA0}S zi;9x!L2A-*`g`Z33wl?J)|-BTEW;hj`4HNp)?)K3wl+eVxX=L>YJqS&D1AS-4(hAv z)lbr95i5DFAp8+bpo%QlT3h$V*1QsP_1=`Z(}eKblq1Aqbr&NI*L&t|3zpwAA)KM! z%ntB8K5B4053|^vI%dzs&eis=wV|k@Uq63pircA;6_o%v zAXwt`@}PmoZ+A8+M>NYf$0Enl!a+Tk~U5 zUwgGISOcz`UZvemO&KLR*pc)rceB#9aDR&r4k_;027>P)K!|*PIL)+17Z|9zi`1uS zXt>#(`H}fV1>`%LT3TWJK1l<~HmvkJitHOtVx*rM0W3Z(WY%BaWUmEd1S8#zriS1&oKs zT$Vsg%B{L8_Z=qHhT3XBFYh98&H)6p(aU= zMMOx%fG|$E__|$V_V%;^oFGsE>I(`ab$<*ejF z4m7bGWf)I^K9yi=6X@T+e|lKjzUIrZ5{18=F47ZH&8^ISX58{LmR|;VZ)s^kE%vwL zRsB~jnd3fdMLsVm(apPO2b#UAbfHlNKL;VHfga$3$}D03G%HoooWO)Aa&-|Dw4gBZHomon2k9*3&viT6I^OCc;0$RQ9gSFiB?iF9(4M_v~edau4*6;9Nj{k$GFIyyrA z@Jtvzk|VpN-r&GM0rQnA8l~XH;dZbN&N|3or)pS1L0k25l2*E9WORa*Y0&HMAgRyO zLme6IYhNo(sv?4e!eX}(p8ZT){j|%n#R_f$IYFb}0c`8Xm~3w0=%^uj@Y|NB{^mF@ z;$w&-8BNvisMSikrk|6!mZ4>End3lyQM}inrJ+k16EaiAc>MHYyUTLg0N<-TGjm9t zAmH5*G3d&(*Ov!fNK=)JS4a#wee3Fc@g3I=?=T=t=LvQg$8kWG#r6Gr->ax9&kr%; zr@;yPG^}KRBDhM=q;QSiYVD9$viOkTv+#yZkpg`zIVu1tHV+Svb^Tn(j^K~SdF@Nw zNu^pE`UUfa%HSN>Ix;gpeYzZwo#NDAE+o`cls_5^PWmK6NbZ{sKeJ_BOCwR`eIqIR13dK=^yG!HFJy$9CTCku6_%cOx+Mq`Ma(aIhk-@uI> z;5DpEQ2oS&9*#pC)V%uKR!?_&fH^rKke@eutO8ceekO*&ao;hE=F9;9J;-xS*s}9W zN4|s*+VeAkuPFV-cqrtxMQc4fEI1P#X-C&fASgxz64hbBOUgk|e zf4&65E|T0l=0$XhTuRzzxY)WrD%?~8N3g(Fm`HtW(6tImigN+klZ@&LWsCEVRSxj# zb!6%nGNJA9Nl7m4>xaYCW_mtT~y8xsWN}qa~GB^&ys42lNp1CfJ=h>Fzf+VpLi-p~#tNIShAl`h4r+|B2V)|CP z+?&(bc%i|R_nN)>WKSWsE_7sMRm3_@FKs1;HcLcUnEs1B-TU_j&LszvbCxKz9#E0^ zSx8FYePU9GmkC$lpvzA#AzrxRzLw`N(m^lp?l zFD`c_nhE{D!Y-sb0HEZ#62k>tBbnbJ&c7jrnW~``(R|*OWlwDKrMit%kn~*4nF}2) zT=~j(Mo0)WudEuJ3{Dj)0lKL@Z!hgNX=+@9;11DfY0!nwk8OZ(hV>X!}#(2{i@PA=XdF} z(nJ-Fd>xGPgg^o2)r-M%zss!j^amsCdctWQr#U(_B0pE!g~=|x92+A~c)V-+b2DTX zGJd(QYxH>w26B@->^?F#Qhom9;KzV)viF}H$^fSX6#_*HTniI3lgq@@@H`Xek0d2(7vwnn z+HmdKfA>{JPorR{v{sgFfg!B(z}A_GJJl{P?>Lw&8lUpUvxchllWCNEfb!lNBEp2SH@b@cf%c0IZC;Kz3aCu^q|U4yKN3E7V{X&H28yC{Pn2dopj zZTj6Z@SS~k13B2X=-V)>^tVi$N8g&=tC zsrsOl>}FBhn_MvSXdkk3BNIFIvt+kW(J&!AcXhX=9DciykdS2htyN&nM0<827CjX; z0Tl-u5ZC9m^y-I9P9QsNSP5+mX4NroCt&0IdCSFu^-LztJKj{=B3+urT-TQ#02^;$ zYKgi74o3MT!PQIJ99pE)BO2 zE_AU_OR@T{%lznRNoApuNN`d z$sG;}3D$EScaAFh&aH>0j&eg1iUK}k6_f;@&9j#DECtUts8l4z_ll#>56FB0{+(3a zH0r9~fFVQ7)j@C-0i?{Ezc!Mdeg^EC&yejHj55@+=2JXV^EW6^74N;oin^&VrghVe zjE=}7#dDRaQq}j0{)|HT{PqqXiqBnM#?P@GN&bCb4AB$?nQ+8rxm+H*KX|Tz0%86i zA+JQJr_Aydupioak%ExwqrA1K|VivGyOB923>Ev~U+h%~9kG`%Ex4i;J#PFx+ZbuEVjUCy? zWM(Sd^|NT_-JDC4{s{Y!8wM;g&hs*i$gKb=2lboRKJG)D z%v*OVi_SJ2&i`8Jgz&9fj1?7*jc``UTk`^1PyC@(8FDCd)10%Cs2oH4Sj8bs$mfRr z1!gI@WpI8Q|!%cT$ zH~q|brm^KQ`g|VpCATl-vBQXP`p*_?FOwv4WMyLD@)CbY<^#^9r`6U`knWNd5Jk(s zuZ?WU%T9(#!dsJg_RvuS+g|)1BSPs`ecuIo;ikS=_6%>4?x=bXqyf62MSEUg_JK(J zLv;y@bFH&5=`uEn#1Z#F98ZbP_dZvRj0g?=K!vuGxr_*I`wr}{*3G{Ji?@tRZr~}y zj|$MzzpD~Y!Hp%zeA#o&Mdc~okI=TvO2gD1&VR?z7xUqVT!R_bv9RSgGE)hn#>S?6 zy6&k!G`W}#nM3CF;q5Bw6!_vT;n1c1U3G~o9tB+AQmngCL7GUBe@g;i_&5X|`-h`` z0At8?WPISeSW`b#ciQ#$vjtXqxRd~S8$~1YVRLrE+y>=)K4-4|Xb*-k;IfJ#d3F!a z!tRH*c9*-TU^4J}uSz2Yi5iP{VC%OJ@Jb#%%XOZ1t5AYOZ$sUc5|#0sff;44s1L1S7Nrs((9F~ zPtBFhd$~2cH?tv{5IaWqiiQ{s_C3O#AK<&jONplEKC4qgjs+!EJA*#uCfA(^M$xKAaC##2~=qM zi=-x41N0d@;nvEpPJcXBbm8J7(`b^g`S371t|fni^=2#hMQ^C&feY1}I<*~fM%VRn ztu+*Nw0;qK_rx;}O_qPRleIuV_?mTz_UQ5poW5x5tu`$);Tr;Sc-dO(Cai2-l zsVZP3j)FuNT4dD9k#__PN>o(z85H}zLH8yqZ|%dEr}8(}Bn3~Owy?C!xMwtAB-Eqt zp>UtBoP(p}3VmFvG1t++ZDUA-h-iyjpk{CLx?a2A(GHB+M2hN(6zqtY(E#YgHkpJCL&OFj0g%8 zcAW5HM1DjAM|xy`3P*QJ>IOKHl}mjFo}-=H3HWYfZ6tTKw(~1dX;pf@v=@k|l#mC> ze2|s-Ew5!Dm~Lexw|Vk#NR67W5tIf_Qo^2=o#()a1Zpg+5uR5$TzoxhqrWWBK%-4m zmc~rV#P4%IHAT1VU#d|dk&!V}=u?#jC&HORhf#VqN9ldFLzhZ)#zt#-Iiqj(RSgXe z%F@EI@bKxN*d)VQE^g#^4OEQe23^}9B1NtbglN#yPH4R{;(4pWvk27aW(ah!@KV&2 zzI#=~)*A(-Up%Y-ko*8cGi=ZFJUnvgN;5>d-1P7ET_&dJ++7kfa=RAGt8d`hzaKVN z#x@uI5fk10O}EH>r8UZ?U+7(RjOj6kk=+D6&+!+*@`^{EJlVxwnlYR<2u4r^L6(rn zEM)UVq-e7j^2OIPhCYK4L@DZkzLAj;Iee02#^83wXJzfSh1}=#!o(XgtSS8Td6#L> zr;|{fs;Uhyx;{K(RzL;7bwo0d{zC0Km*1jl_h6*=#Rq-UGfI4ZxE68eVRh((@6BU- zK}kB)2WIYf89Mg4^X*-adqV5Sz8;L6^+lZ?v%A310gO3Gr@Wo%>-B$d?kHYho0U~Rz)~nup{1_k+v(r29be*jPtGoza!3i$&o%#(JmCOJYpz)laVk2gN4H&bAIBM@@(X2lY?c}7Y#Kc>Ge!QEQJV~^M&h9<=h`0C!cq~yD&7 zYHfa6xqijpfCxHE1A#L>9=zMwj#^1bO9YPD_2ff~g0OFXOyBG4yvVnjhuo8^P~J;T zT2qx0|4i#KH$ohBEo9q~<=XRVj5z-So>{Q16vJzE?MoyijaJ3W2Q z#Alu3TB{g0x9?(6RA9)9mCl4+AO`i$RTUQ(75voTW>#kA`1kSLLbjF4+&D@^~Hq%U7-Sk`GFJ{_#Tnz=Z7Kk;;$U zpU-;?6|-s?rdtdyScCI2he~%xDL23s4fSJ?bhbd!`8X*z)6+ll3hl-K`yNQzxVbO8 z<;YqqSKn#Ryn6L{SJ9`_PI?D}(X-M@zCyd%46Osfb72yh=5F z+)tmT*C*M!j=c?KVh-~O@?R^W#fY{#;MU<3dMY0|q3KnfK$-Qh6qX!i)lzUGeE~!c!YizuJ)ukpT;jSr5u9fp_ zZB)rE-=Go$Y+`Ox*EMeSLaOWmjX|uHsl+GI)!5kAOUd#M+Bywu%>xC&yIsc` z#fpfl`T3dF8^&v#q(eEsF9 zw+OF*2?l;XnXNrHXGgfRTiCMnb!y-n!L(E-n6ZUz{p2C^!XpB(&_iVS3ZFn^z5=cs zEA|6EJtTB$Ds-9rtOwznY3>xKUcLonlB@JU z1Vz?mihtMp!e`JHW7sO4!%anhWa@iI6&`~lF+Rgj&z<%-gp>dCN6FOXH{i;3{+$1S z;?8{|Ba;zC(EP*8-NlWKU_Nl*i9d6m+~Clg-$5GM%Byf96$*KLlBn(vr)`|Zn+u@R z$%@Q~f6geMx}3B^L)iCcM=rp^`zc7xf0z=v^RNL_TAWbyi>++Rk4r4az2MO*5OMMk zA58D>WCr;5aVx(igddhjyhM$3LbjwSDrh#u4_x?n13{ZNl!VbgYke?=UM0VBLHLdA zC-nbrvu4k{|KClPdWBD*^wV2OpMANt=AS+SFswd025zF`FGEA3Zh?9-BoF}skdU$i zMUZe2*qH^rZ*KbDaRPqdZ3KP?+x&7y(avyRMQx~*W%sOvwNX<77AL$Okv2=AL+K1~ z&O>tm=)ne#M0i#PimiBkot6nsJr-#~iKnQ(mc5k!n0JS$2q^5HU!RZ4%EOL;cSv%A z(EX`3SaY|8MVxnRBs~WD1~6WMJOb1Pcnmxy?gfv4pf9lFu$L-tsnEyIKxI$LdmvyB z6Tg`s5J0)8MuC^n)*$)OZ@U8t^!ZuA(Xgf_?rrGf`y#mLRS+khQCmYa|GI_6Fv%C- znk&?s^}W6M4^`EaFaM}Dx1hhmmbkKN*#7gUwuZ*~qO^gbmSIZ|S2Z)!$f$*-|5Ahc z?c%Dc#N*pe9X}P$Z>*EPF`AK)v1@+5Y|)a+_j8v|qHY=HXKqb)Y8iMB|Bl#g)2f*R zg{5_6Y7*VB$o1>9RQUrBFgJS{1ke*Hbg$%ld9}X%$lzA9+pa;O;Rb4_%PuPv~K zc3oWA{p$3s=2*V$DG}Gr8EKylLDe}#xO;?W?!?6ETqGCxkk=i+!6g&r-~1j66SLKm zn`W&Q8j0L;TF?}!XpnYab{2t(9;3Q?n;|t~hisC-5|(H7Q0Sh2P*||*FP^LeE~lQ| zJ|y7M(+4TbFl2ga>f(SA$y4(~PdjD;jiSSU3qn{NAzZp6biY zlF`O@33R3J*BDskuz9*8Uqz z&?Q%zmnvFj+cJZ0zkxTAa-i|T$KJgIM=l1!y{VtKwl-R{mH{sn z)Y0~6ctrva`vMBrF;_zeXbsl@&m#(_-X%M`+_8UpnMjNPn!kNhU_|(H9l=LPe%PEm zc(RjJI6VNbo!)k9H#HsB^t%iy#|~OB$LlhN2-M(W{F?YKb8~~?t;2g9bvu?3i(My5 z-3J(b^-Ltj&s#TIrtHkTf^WJ7YA$vuTY_EC)Nx_HwF zLCXzxR#6-03?X)0SRxU$mAC?y?6(g~T1fplFgfw$53z=!zw1HL~s4Q(N1pd0BgoI`SdgB%W>hF7SMlXHdhi%dZjDHq~C z#ayPoi9H8o|DZJ~#myZ#Nr`9o@}!7ha zz1wij;qoA_I*zxAcK0KKmBSr2CZN^H=#c>9Zm0+@0k@P(Hc&pRr_x{*P6`kxcYF4^Dwd0(WpUT^4;WcMW zG@eUDiF3MYWv?aQlqiNJj>Wu@Jq+Ko8~4LsALlaUvcUa9eRI zTnH+Eqn&BNbm%TT#33Cw3y#{{y}9%bN^0DGIISz}CQVIEkHCNjFhRik5Q3$Wl2YEv zH(jXFJiGlJN)ZC^a?Aj<@C>ja5KusL#>d>TNANbs3ICemh!Z5}gqe}Hf(L(&xa$0c zI~Lx}oTp{~2t9l1oAW_wmtlE2q~w4CAp#p~Yl%2)6hh70JkW0WZUDX)GWq+5qDv-2b1?WV7^c*q~B+edGX z(Js!ciRexOcNy*L51T$^I%`1tdg%I<7Z6~}6D?i&rYP~@AlkcKQ-N<($TfFFI_~gI zg3JwkmH57WdJg|G>XEps5U869b}Q~fJH0pBn4we}d^;{7KAsWo6ykjR`1kKsP%*KC zjy6sOiqkcuVp(VWfiuFwen3uNZZ_bU-|);6Z>8`gEM={)0U|nJMhq`0VB5Y)i5u!U znOx`lv+l=_#74fbNc9jab&(P12!t_24~o6qJb6*BI}3ka4T4%X3B8q6(@i5L!@a=+g*sPz!9C zS)j}E@Fg% z(;JWv)MZJ%S$3_D0&jgIZ=J}lwN8n@1l(nbg<_2K^zf_`9!#S7A%Ga{!n|V+f>w&x6WNGxs<8jANjR2_O0hR7H%a~NN1H3bYu`Bo~CL}QB&viP^t^47vsW^@g z-c88kw;7ul!^J)(%F6fx_z4GYyg6;T@fza>fyLayYM-yR*ILA$eW29WWNw@XVU$or zP$QPhmt$*8eO#dW56{O=48hYwJl1Dm0nOKTZhdj>8dhON({a#O__Hs^D*@MyAE}Ju zFm7KFO-axO>4}B|La7mN`C5p+EH0frh6mF)4P{uo1g+B^WOr!L!0iG&cVs}npS_3x zb52inSsuR=A1@9M7U51(&Djrw%CIm5wdod0U&87FE8yjOK#5m+ct?jLU#=!?j}{ga z;rlhovN0gQAL`h`wvfTds7WEc(zIM1P!i;GRc#2vxfl%a=TEmPO?2Pk43zGX^@Spr znE=dTd-*bQqxG?LG8I8?e6i+?pTbaWf)r?z9l|bVBfmh#?6km-0&2k zfdcQ4ng&%RNTD+^hy4=b`IISmx~P@0K_}!FNBGQPhx~4X8ITc`&X?aT$j7CAIO^1? z6M&kaP2x+4w$5__o?$H_hT+b%mS=landRBBfOZ8lVST)&@_i-OXC*Fg2tecxH&w17v9Mt&oUDLZ?X_Jla^e9sq?CGiRM} z6yyiwoO$w>l*4(_BHO9B+5Bjjuj!Z!*}Qjr$dE3Z5AU7fYo1mC!YgW#O?PbY(?Ownc}{7XeNKdW z_9Iw^LT>=P$Hr=K(bBKv%>*dTY5TalsMI|0<~(-n*vXTt96k=xpN+VbB^Wa9WMbMbEq?ZwO1pK?8RbmQW4n&|5=eH`Uxrg>4849>mPQOiA1KGwg7O%;n{K!-I;w3-pq7w*}?uBG_eA%blAC6z}6 z(qG@ok)??~>Lzh-+ZicuHEPOq+erh)=mRQ|B9m-B!zZ&g z#wL=zdcy=f8t>~YjYSd5L%;lXa&mzf`xPiFADrID)G!gT&W?*N>^#C4?V=vvs8u*! zU9{Z3`Lm-VQQ?+_x%GOzR#6hw1IKLLXw+>6Uiw*WFZY@ERJU$9h$4>iIh$S5^6q=%=r1ip%Me@e2Ey72s46IC2dPB5-YL^C(>(dIkeyPdH8sc5 ztTviKjW)|`7yRA`x+fo+Sw+#fJd-<*Cucr{r& ziK?;sN{ilE&l){ers{kRmHoaPL*bITf9OlqEu8xLviT~jnUTMg+vZ#tLsfP#lyuK` zZ6-0OsaD4~?q}QZV|eoZV}U;P(<)gIY}HYI^suS3ZhBaHJ)dFfal22dP4!5!_beZ8 z{?Z><7n#Mu(72DUYl zL#q{UFtfDV_1ez_pUI~p28Ma2*R#6r@N3jaR5~;5ip^;~DDJkKm6i3)38cmtWr{fG zdOc6fw@R-*W3Y>pq{<_jGe^gSdk=)LBin|~VPO7=K8F1yquhxgk=%g2$jq)GiXkI2 za~QEyuwr}ykAyuBG+dg|f+#BbyL7L-+3|^Q-ICQ(Yca{l&S=|y67|Z|`DFL5R%o6f zyL_2<#m_GuV-!uz6yBXgp$0LNKuO%F^nvQu=ky#$|AQ2wnkSl|vh)H#DrdFWV8Q0I z%A_9@=(>*Q`3H2LW5mUY+9(=O0qJc{|EwV(mToBQbz7TM&ouF)6YH{alqAj&LDvZ` zo9xT9h?MnKotu#)>s;dkK6F0>RnCeiua&fqACt7H(bC7vOc(gH+QQ}l2y7D<2))eE z&`_I=jzJ6-h=wz8QBbOJ1C(ILVUP|BMd_Fm12%(lXD@L(h? zbvT`lCFHCoihBhoc1n583voqAsKPd+H+7wLN4G>#8a7)9yP#+WxU+z;=|qRW-nsyU zXc%h7h8Cu!SBxA_yaR~#`Lczz^z-H2$miqcXaxHBFr)lUlOpEat zk;K6zls0imxod#&W9&t0v~bDn+>QHT*d;~(VvUGk-@4DKg2^$*8mUDI?C7TtY1b*p zWWP)*s&k$rFO`O++PM?{HUQ1wX9Q{tIO)N{&D%7w5s=WqPb0i^#(O6%m(D{P0hmP+ zva_DS5zAzNdhjy>?^1Aru@UwI*9OBAO6n}Ft@mRekWRq7n**W#KIiV3)-gJIA)N5Q zV-JL?=*Tn7)yPC1hPsX41GuZp%3)Ki4663TOA2Vj?5VWm1>15Q9SmMMacMt5F-(`- zmVR7m@xwMkg778@M-{ILU*(a83O)K2 z{aSxkgC^ssf(`87VJ9H|J8-oarFZHF^&R3dsWw%&-Cj9mK@~cqjM_^iYySbxd%~y#J9*`mY zo@Fl;@bv)h=-S*5>iul)Od^Z9>J;$h2j;P{e=*|qvkS3*3?(|kr|v5zdbxMnryyiv zW-OBjC=RlVh`GMEe{P&4<~YbJu?|fVSgn5Qz@Ho2>D$%OVruE9CL9KJe2kti^BX(( z_F%-ETh@alkxWbr%|Xv|P`$=q)X>1JAJPE2VCG?%_4>uXv=Ac1eTt{K!s9S@-~He0 zj&rOnB4B1?=6;Tb^avzQ>D=5ph;bVC@!XAzf6i#UHW&%QMJz$_+=eFyCHox2{%}#v zG{P|n1Yvh+#(nFPYkxkGgSYyzQOi=~d%dXC`Gw*zj4x*z?!1tAO-c?ZkvkO>32mH; zB@0%9UyX||WKM)#M)l_Z>|h#&eHj$_b@sjG0>`nnJYrM<^Mpgjf{{?G!8|lBC-4Z| z-(lG+N&D2u7Nd*JRo>4jJEE)k(y;3U zjMX_p;|#+c)MXI05XuO+U<$F}hMh=@exUWZ3X zdwc)Ts67zj@BwI13%S%bOUqJTx&+Sy$?oSASCqpe@+a!?rzp>NZ{JUbEp(OTvjdo6 ztL^Bem5hNafu`LMbB_e3frEggh9HEEgt(5tqY)S>#-b{Gl?Qj=JbWV#HTq$08NQ*4 zKgA^m*#nB+_I{?pL41p4+A3o6np=0CnBIpzU*;A}8MbvD5Ze_9c+^G{9li3f zeH`}H^{tVGa52mP8ow;bU}NALsQZ@os$ecN`hWyb`U2>p2ARo#e@+u}Vx6gawI+#zvsm6f@30(wl!cy~n3OYdK zTLy?Z!ZAoK$4Z!k@IQ7(FIzXn?v0SG3n*#Nx5}HPfwHM7zIJpq&AZ0 zT%S(6jyz(AebqS%PqWr>Fd4r)x#RNJ)IwP{0vQr4Yuj5qwl`XsPPH@p=xT9*M>qS#j zQ;eUwVW;;)5ZKW-ovf*(6c%+6br-N4Vg}o{F!a5Ma8JQ4GU9ar2#F9^Uh=I6*@gp7 zV1j=q-qNZOWSoKVaymHiXpgFsdVJBwqK|7+8TLvzx)=C|Lq&TmUm$mR>dKjWl zdR-kONCsX~;_e z7UKK?s1#JC+g0KIB2?ZmEr7{(Umk+gr(Ws_hed`%0TBy?Bx*<@ATE-!c20JI3u?mJ zjoTaBn*RcKWNQz)7YL>VKLJK>N=m?j$X|y>0k~Ex1?o=opHHB;52Y%Yz$y1hZJ)0z zlM7@Am;YJA`;Hb5^#~GaA3Y7(eGBz4>_@Mm3Qead6^sRHybcHW89Y5Usm}qN8yLUl zseKH8KN&`Zd5q=uupuI(D=6Hr?1!hInvg)_0<8Xm`VzR>h26WLXuyfh<38{hu7~8z zCc8;c4FR*n1zZxxf-B+f{e=loYeh`0)lYychI22jE=+*z$z#tV!vTSU+s8~?WSG2LJV-5_+tzzuMKun{BX1xm@jkI?~qA^C5kn;qnlz-h4Vtr^kSf)`LX`AuBK z8s2~I6N~vGxtn&2NsvZED;~VXU1KLBz|V*@GJg0ca0*+0_bU}BAPW2`$dCVL1RMa2 z9Qs+H0X;&@wk!Rxo;98xNXF~H{wboEU&HUA%5wp6+y#E!I6MmkAwrWwPRI8tZ)ojd z*FS~b5yeGf?11v+zkfre>^o@Vm(#-H>ZkTOkxncVsnL&aJzzfeWc&vAi;JuG7e>&P z3X?Q;0Botgzs_R)emJ^&=fr{$AS%t9C!!AeW&t5vTdn5%*b(4vafHt3FPkFS?SRtd zAl1Qz7u?xk2WLm<|BLHenZkMJKl`|0Y7xz}UQmEF^RQsHuoDypp%Yo2rbQqoDB@y^ zsrntRVX(SlY-AD?2&4P9Aeimxe8a8bvC%)n=h96-7tFy9``el(!JEFu!dW0QrfMgG z%HKX%oNU-ye=a@E7Wywxfu4a1$Jodyz-}EZgvAxLVA3qJviN|f%v*st*oc+nxTg=IhqinS3EpHcLAcGU0nsuWI&D3S;IF5Qxu%t8YiC31V@6U9h4bqMadWrcu{Ur zkI5ps@CYbp1^`yNMop=%$wt07&3H|KQPC9h3vP#L2p?h9myC~|X>dbyJ~_)Yj1FQV z?623JWrM|r?&IUkZCgHx3DZi;(E)hR54QBq0HC;<5=a^T>4eG3eu$Yc`lOJR7XZV3 zP>kOv4m>VskJtp|v(wBcQAWRqL7Dau0}R5v!Q!3;6t8jVz@M7RE4(zcj8`s;fp$8|j}0-g-jmnt2-RWP2D zh@W!f+{+(zNpPVl7u&Axs@*fM4r4kT3E<}Hu82bNooaYtpS+X7;bISZ$;<6FQ75KO z|M8k+pzX~

tMhZwKrGmiKCwY;6JSo#pH8OV!a!sd%f8?#^d62ho2LiD(Vkes2j& z)?_GI6px;ZgH6n8d@y37;a6Zt>{TbDsS0zbH!h5B3LV_tTSdT~!?365)kSQrjoMOXg|EwYt@TMx zmV)dSd?aOShS+u*@t6+3R5_jArAFosGplRK5Tm;C@f&ebzpcb6Ym5%K;TfHM$4gdX@OeMN8Raivj|26=~4txMu2gH_$ij zR!tMUo-SN>=T?aHW#Q@K)jHiZuv7zqt^x5Kav5L+n07Rw?)nzk+FgV~kBa*3903*A z$Ea+&_e1dDp}S*TU}lz6d(JpiH5-fMyId8;zT{PUL9+-|j{ALrzBVIcOM0r`0(3u~ zN~MDs(?E=QKSjl|1VJ)rGrNZPUKf#i8b|e=PRxUVV>2x61d+mye*q$Gx^$+ph+piV zhVAjsJh1-5Np{V5PrilQ$ntXILdCZ7u+^)@orcg7e48c}1<5o?CizwRNz9jpO_#gz zRwUK2ieNr&X5T*SmIb>*yMt=7lC%LkqOx)0u%jGyXz&dn`D&IZB~CQ7g{W^v53AYA zRA(%W{kI#O!MAdac8X0Bfzqvx>i)a`wfAJkch~{9E(a0lPD*k>n1SUs_e7tLm+?8= z4U7rbqhwatpdBCe^l#wh7lg)9eS1Rxf&)@!r;*4Jhq`?@&ppr%-WO>k@zMVpL2149 zRk(tL3nhqRur_ig41~w?k;qY6ytS=aA(2AcRe{Sz1A`pJaCkvgF0Zwi)vN!ep6E-UkrN014nhhn4cao~|PdzhJaT8c#{ap&$O znQllh&Rv7U2y?KboH1R0uL7 zRW`{Dbh{EFZR#HRDGE!18+a3TnC;vMHXu~Dc4TZ67BK|c4YHuBmb|S|2aDQ=6ZVk~{Aq9xq&->`vs||YwCf_E~UuB!0vm;Xs#I~PAI4I9E zKL-tA+N=h|LQW5szRJ9HYZrAp`1GcGP|NQwS0TW{j+k2TiDm8wq~Bu1#v19w6y!~i zc}MK%2W^G?P`QDP(Le0pPwi$-`l|u)8?X%_`%$-i`PZP7+ZXDpB~x{d;y z2yo?T*jC=XweT~5iTkN8pGQ8HDfg{f`-80E!#BgDAQ=)B|E#uU1tYuLh%ES@)1%*x z0=_tWN!zCl_-66RlXh{Fg0>hi7}m`CO4P4ltbW3o(9DGjZ(e=qs2oV*)pcFV3#2V- zgXbC}3R{j3Ypg3tGXJNUXdEWA)o8r1w^I@=EREHtFr{ zUDx2ZiJE6VQ!U9N0&ZR~RDF!aq2eKB`ImF!+G3e=soRWJ-au#})=IYZWe$0Mbz4NF z-s-Q038H>tCI9=1lYeFUSdlD&^QH5xgP~Q&aN>`((sYMWH~O$SJ!~#*>(!z_c#=!Z z)Wr}AJ|x(LCBnrSVzsALn=GV3*4N0ooc z8-LG!qb4)KdCJ!u9uoXLiqZHi-?{lGCoS>h+ZBIPms)auyaSNFNsuV56sW0uo z?T2Rd@6VE?9+gKnf}v@{uOYN=m$WRH+d=c-8ugo>L<{@q0*^P>0t09H$Rt zoBw1Jn%+!i1mmh{bC`%V&ujPqAxlgJcV?5HMz>4|#T` zpg2VlqW$S-8jY`bDme8G z>~*~LMa2GlxWm!HyAeFo1Ll0QKkjtKNH#NN?@Ph?AF8u0Is${}F>~4M+uZ1o(zq=E z&}}ZKr#BN}b>pG@Z2zsXdb>lZI=(k8GX^HBKZ(gfXZ<%=tx$Gaptv#3zi zdVjrpcvx8Zna@hhlhZ}?ndnDcSeoZa9!e!Hg>!bYC|cog==6=f?`=%!lbOt6f$Hj- zVp--*@#0G(XG$*of=cOK2MwWh3R!qM4ve)c=JrAY#;Mean>v^ z-TP)vYkZZ`n);_sI9-b~dKEe)T5=Af0nM}YmVoaUj)b0+^7(rymdr+=*=<>L{MZ4c%8;Bje%p6qmXVlb{$MJ)sTC}CV|c46<~TRT0$ zoQ;F1`mgDt+*nJqiAE+aG~?q29U{0xVRqIwp9_~0(c;nq!N>h9uH=yb(~HqXI^pOB zjsuoPv>pv|82p_>QzX}v|DKvqcFG{sL3#U;n~xOrCJ@`~6k{W~Z(!StU1P-g2ra##Yb4usiI54QUdLS+to7&_zr{S$>e# z8E1ea`)G9{t=0=UIGm@-n}2sCO%k^a-B%nN?|1w?TsVeX{Y5)ABhfC`{&$lXt7*Xf zt=x3ZqulksN9#j%V~K!$2Nnj|UA->M^DkUQ2;~F7`JQOqq$XvHOmk2HEIjw%IkbIK zW!kZ^g?g8MH{Bym?)%pj?;h=+oxZM0`f-bqS@v|sg?K4!kBNvVWkEY{J~sS0&Y`l1 zH_)z>g;7@1L2>5_kHZ4Ys^3inET4y~k7n1rH;-l^@1E7o#ngkh8N^zb4SnanO(|N zuex;cGgjAM|Dfv?sTl34DSZV47GTb~2G5~w@!x4(dYh|0g~ba0?gc9x%$)ef@DtNC z{oU&ZdU-5%%~e*IY(O!QW2~)(U~BhupDbU_<(1yuaAPlP<{$6L%vu94Umfa8jm(HD zWC2xT9l&)u1wh@HPZ!{H{S6y~zcIkvNR9K^!_SR+0UC)rrrD^-H}G!l3W}cH8eX*5hNMz9sf&@wZDQAFq<{${z*Kp~)?} zvp0LSvwo3hZ#YwR*VUf$(&W1?MGRQ<*zd^$0qId%<80aVI1X1U6Z?@0_Qdj=l(BqQ zC%gO-Ay?(0=5(8Ga|zZD$x62M*7(Re)&BbS?OWMI!#1DG8SEm-fEQ z@#?7v2~Q1|w=Y!+UmyIWQ($=qNSy1JA- zGDo?@>W`@jxI{k{t1$glWcP((waUL%%E{;~-Tl2-Q%{edvoc`PVDC$R)Fz3YDpB;= zU>KU;HD>GY>sygw*VfK3E{Sh7N)g}TA095iG?8B~DJd>0Y9C@Z)Mjj4GF2+>FRkz? z{K5Wp2NrVn)Fn88g(H2%YLe}I#5Om0guZ@#wW~H1H%n2@+ica(2j_C|6W@|V<1%|~K}pH(Jb{lvZpRxrLaatQes(t%#Ez#4 zL`S`0e=%4U^GH!<(r~nODsr=EWMpLab!TJw7u4(5)4e7llltoF?kj?3X!w&odmt## zX`17`=J3(?#={4-Ch5nQzuGvrq*&#Ip883hjBG&*6g(_hleH3PwD%q@mew(tPc_vs zXPued&Jz|)KfW&W;=TY|eP`>=j}u|kan*xEZM+_7Ez=xgt9MKX;Mi}FM>;L4D$}IB zP-JPi(zNi^)na~&sT>U++&NegcniZx)__{vTJ}V1yo^)Hy^_~hZk5g@DI5M*z zr1nh~7H%=2>3pe48H=0ht54Zj2@*FFG#k%xQiq<3(#nc58S8VLT(bQ@-AJ!OFm6;H zT~R1Lzu!7*q>eGuRPTECFNJV^eK8K18em@DzVN4e`*#u1tw{?gni<5SuxHOF_sLO# zAE?!=TCU5;n&IrB2PIrd7jY4r@(P;naYoVJPmvCLSlg zao@Qk)7Fe0U$xA-5?giOmRZZ0(>~Hy!AyrMFJ*?Cr_Sm)ml#x zQETmS7+ z3=(VgE{J_MSJ*|*aT0alej$>e4$k8#gR~#3vohJ^6e!a53RMM^o=YXowJh!USHof6 z3Z8vs7ruivjBq|k3&KP3^dU~aT8F8v{zqOMUv^Tp#@N#**9=FxT7Y~jaZ#LQzk{Uc`ZDq6rmk_x3>oG_Nk&Bxt z>vA=c0`h(yJcky)=}XO6&aWCT{+*`xwHCSNlF`O`jA0E4J|nn(l$n~&nE4d#ekpK>h0U|Bf?FqHs@Gv)lS+mN7(z@{l)+Lz7*f@ z>`Q*GtK2GKcXzSnNq66yi-jL63ZMJm?hiU#u}wcXV?20~!_wR64eh3gsHh}@5z)}7 z!f$V8IQoC<+$1`JZ*|nwIsR`mcE+2-g-Y|pPq{Rsu8;@imd7-jLj@-TQQlg6>a)_# zXAcuZWqBUbcJc8yJiij#R=fbLf9-3y@zKE|+mIfpqk4UPr3%||i0!w33!Xjo*l!Jx zO~$x*@@jw1`x^5iLKe^U}Gs3Ri^ighlZC?isjiY zO8-&Tv>G%wp~A3{?qdV1cyaeAPp)@BLyn^L0M`3{rb5uhH&hN& z+0d|6Xv{-5{!F0A@qT+jV2;Ws{SP3D{?=9Csl3^JkqmxzO8B*E zOVo?Y27EWqU#>C4h~M$ehGj=S(jC+cLlGoZlzaLAZv4ZFq8Bs$@&=;c3ZN;4UA`?g zlEm?c4OqXiX2Ru9nYPo@9glKZW|87vGF=P`2-I2^D07VQ3Kv_Xeq=f>$8-(z_ROa) z{o_opPj{Xzt4p`p@RgDBnFB!tT!+9TD74VkvttqaE)-xoE%%ue;0xkN<6JO8l zDbM*=F0c#-!NZ^v%MGX{kl^)zIeu%5rT-uP1suxk!{#3-WQ>2~$o8y%w*X_%&cQAS zKEAHo~FB1b2}qT)?EJb`#hFm;C2LPvwvq@_Bl?PDPzG1oHyn8 z#P7+JA0p$_kNx;^GG0M}8KdnKZ#e^TjAuH@`@V$Dc^|AYDYWbKD%Wi4JPIE0Cy zk>c?H-HeE!2I|;&u0Y1nf30q$bqkMM##FUv@$Om6P0QjJP~1M@qEe8Xk^oZC{fUGN zwYK@iUTe3(u;e!RcEsy{2$s>7vN-MBUhI@1ZJWZHrRfF6Gdck=p_?*&e&oI>%k;@( z4HOe!TI!nr;DubH(C=0;GJKk)%rvMWbG%kna+@lAFp4{fuz(;;9ed`l8c)av7MR|{ zlAz^Qy*KaWQhiJ0KLy>=3zLT~=RPXFEOY3fsW_yMEvyZ35ln5dOiBp76^~ev!9z1mJ%!gIKQ; zS=eElsF?iDStq|!gx2p73pVfmr=j-xi3EYx-Z}%;28q2&`)jA&;U}RS?ax^Y4#R@E zwZ9b;VdIOxDf(`TY9sosPj;ZK}ksbtB`g;gcLq%A z)RUDCWpoadK)EFB!TRPs$~<3UrZDOCpu6JtZtAg{hcfp$`QI}(tbqh$%2@$I}^dr%s(`!J!PAfmW)SbiFECVLIi&P#cSp8YIZlJiwZR<+)sP zGhc4yd-B6b0}chTI`2x@=@t_ovSZ+7vPfH%(Bj+W0-Nox)&7F_7@v_*$f5)NLof*Z z3j=lxR4}K~_79!-T(2RKny5B?T4ttQO#Z1C%$l6ZP*(q0!6L6FfW$V%A>i`@#Y3 zBAoC{bz|n}RC_NHl<>N!OVJVWm+K2Vr-p`{t__)?aO%H!@8&khW&$a;1MA@S(!3Sd z?0y%toN^IYx!RE9@pUOm$lMJSUnlvq``#Stu&~3|SK$J7+t+deCf@LSsen{VnGCBs ze~75j&dO}O;IV*>^8OIfW2h{5O<76lfe6qE*+kRjlfQhe$m1<)n+ixZ)p~gRbqjkT z1#-RU&bFukf{s> z0iQ?Q7#E|(v11B8 zrkqpli{-7YVZV)0YJ@TVsjD&;D)0iz+Jw07L2M}5v7l{XUs`5VV)-)MSH*A*rXun- zrn5cI@Q#tduIu~Zb78YhkHGN(!f)WfXw=usLm7E{`|rBe@vgl56t-w}e(3H9SDm9P zdJzfZvOq;uHNkx38-Z9)UHZ{Vb27-sNy*53x}$41Sa&06nl^W*eM^7ssF?PM$=$kiO+Pl*_Ng>tCDl`B!SFslgs; z5C^n={5YO5=U2r11S6Z?k`QgLK+JgV2vnf*?yLH5(yAe1+R2>FGB!OO(^cZOrZv0p zq>FE~)+*d#o?kNL1EjwT3;T?YyaY!E-{>i})p?5`if*}bTEHv?#12ZBHEl%jc|R+z z)Ne>8{sLDDvh0HL^V(O21*lC5=lB=ygkD3CrYKTuwt|z&sU%Q&s&e8)92VZQ-W}B$ z>fRpJ0PL1@Mr-KXGrw1Y>;rCs+VUQHH|z=lj2opqq=nMG^=u9;36IkK$pRw!&4%!UdcHOd|ZB%cdVx_MYWv}dRGB_EiR8t zvjUdVJnlo*nAWt<6&sE>vMtK_uutdoeCkd5%*4dlY*Twl0tal7aKgb&xv&mY6<%2O zbUb3a<$3`MY91vcE{g|xpfPy)WFRY8{+ysW>}I@;_WkL!@e!X5csUAmoUn*e5f$~_ z<^(BOT6z+LCc&wqtisYFF7-w{!46HTBGd#bBNtM~YlaU8$&KaV{j4v5&MIy|`~4|( z^)%CgkBA{&?W;zSWqxetcyF%;vmf>cOB7H>2gxdbHF`I}tTWZhj#18b>aDij&QA3k z;`)5{#Lo~cpR95l2vM+a`HGg}r*d(EVzGFYW~)}B1Uf#0p~25DZPOYb3c4miBZ?=l zPggeFao5YJ#H$St|M~83h<*P`**I3OKP_kPO~&gZ<5^+XTc1lE3zGOZ^stZ9+=b!c z;WhIp?+X7XqWsgXVn^%!q5ARg@bK*1g}Aa=jlSQ>&ZmBj zM|?X12#p}jN%5#2u$zczEoduzTK(!k!S0#Ka27>+?@?0vE-E(NmMJ~E(rtef8&!_b z7^Qd~9#EA#@3sK(Nb8#9%*o*=YIq5Sn9L;(3@r%@Ptb=wR3GKW=qM3FkfJ~@{*9xn zC+ex~ApQ?bpRxLZAv~1^i7B&&>`e3VG6AjaMipJHRl0~|)h0#mf#^CfrZ*0Eh0AXl z>3EcMOkIW(RYsr7QEGU!qbhx0{_zE7FIiK8Qwzj_=$FPkP}jyaZAx`%V-GRU0zNqvvPa(HAS29k1AI#9pL&?0 zjCocjUYdw^b((OWc65NhCLxEVHbj1fNhk{|+yPcN;;p(+086Og6IdV>KB%eQi3e8l zkH1)gRAlV9N{PW3UEb)k1W^5eP#U}X!xRfBl5X`_)5@%UDSh&Pq2mR$(fEbVcKLyI zJF%bDepsqy8s~}?MM0864g;@NY}Fp^DwAHn{?i7^aAWZ0uL60fB$ zg})yE;XQu{niC<7gF#L+IQC)&@5tQQX?9<8pU3PA)aMaANS0~T^s6MKz;9dNPQ#PK zHuTxKrDbKLt0B~gSM3#z#@RVoZ`?it)+^9RChFUQq^$ND8X96)RB|+(Ls_3t*P5&e z=Vu}s@4ELV0DKN14N3(9UTsZf^s~v%e1vd@yK2NlBkw3t4S(%t?sja5sgbrgZGmMh zAYdL5TDI7%F2nfSZLh^+yu%$o)784Aq}W`_PAy%WIg{__jPNfgos3Onm`V`jHX>8u z*9QkY&9rN5JAR}d)p!os+FSKP9Fg1g9Bt5N_lS(o(R`F`!3gM7MX0-6PCOFjIbmmF zY_WU$*9Bc4M+nvNT}J~Otg?TPlWn@+=i<{!vz*0GavLoXn)8=3yR?*&c`Il^4;h=e z43stcuSWMcvDP4q_n#Mo>;%QdyVIU$jf^qJi(l-rjaxt+HXpq0Reycr(>E4#4-gCI zq6!Li`swa$lVwipFZ_!i7P_|z?BDhv`|B_?fNzr5pnk~Q4ZPB4%Per_v##oZMQ8#G z3RL#mi#a_S>D&*uT97~VIU?C7OzYomv%ktH846>}mPuC9f5?fKZJ>nTA>tI|+~o$b zOu(QTh!c@N#&?J(#7wj;k#=f#sB;)jTtF?tz1p^rG5SP9uQV!JN8@!B;>ikLC@MU8 z@}S<;0VVoOJpzdXyUtL;c+D?my6>5OYftT2psg?4j2EYYZoRCiCv@VK3xODR%71I^=7;4{0Q*7S@{S>~?9Zl05StDLdiLTOuOwN^59OKN{#-rJeHlIpA0ZKf_U><|7g?Thn> zdmRs${dFeUcB%+v7tW#mh4*Wg?!LyzwXKheio#AGUX$C0jRi`WRhSM-$E3|_)A{X- zQY$Dyn<%|UGlN6fMf4N|d1GGhJ`oay>oQWr4b`nq*_{7<0N$Td$b|**>!Mbse&28XnMR z`<`g%*8oGSDG3*tS6KtDya?vN5mDqJ-I~4>7EXRns&pJ;WaHW~WkOl{!UvrF#&Alo zs?!1=9wsV6k%scVnT<8{Rq}2Auv1Ma6@5;>$H<-(h=(7$zkeOL#QOp0=;1qH{Gh(| zcWdc-iEAB}=*YWK%#S$PzJ7k1K+JL0wB0BNZ7qG>q|mxD#jLVuHHA1N&ognyF4@@3 z%Wpb}iF=;xSJ)lzrE!R54Og$pUhcRi(~O&%6AkEn#?z&lCgBL1Kr6VsE=$%;|F=@t zxmVhvt18_~k{Uk(tHE*Ti63+zNQY$@y0l1M>*((`&oD;r1AdC;To<3wwmzr!Bn{uK zS`FWZD2l}LDB*O#xFE~wCy4u6kzG5q;mUb}#(Z10TxUE`D0;DT>sn4kJ{mYMH8mRn z9YlXW%J{4^Wt%xQny`R&Qbc6;F!D@+5L&E=5Gx5ljlas8yTCKGjge*5YYnU6-2&P< zQS=08jy)eYR#OXwGkhGbGlV>;ui)_EjzP>sI=mO3l8n?nh(SGu6cPpaD7GE`(%CJ8 zqMvnaXTG>^vw4JTwSu8A2-(0&E@eLf4bX^9VN&IofW)uW)IbN~ri%nJqzwjzMdtsG zFT(dZ6kD&MWBr{_NT!jP36*yTF7^iB45${KJ(r& z9Pl?;EF$(P=4BOv)Dnb4&V7FD7Bo9?Ywn#X2ojZIde>sIV0j4J_5kU@K?@!0fz}Xt zqxlUzJ5}C059T{R``7TCrZGHKs%Qa$)q$zk2MKz`?KiI9cpgFU1b;hrsUMKM2&PjmL`~)FlOQJR#tWxn17CrffSF zBxTlg`{uM9Kqs++EcbLdvS^L%xZb$l&nvBG5RYT7o~O9?k>YtsemG_ErX@F3Hl63D zTs{*^nY-J5q*JylzTlAVE_=K?r^o%5;)3;`J|%=HK~*;)zm1Xe*vZK6OCqeh!IoFc z4MPs;UA00Pu01aEpz;$gS9xoeg&`uK)hq(;{Fm%eaQ1a*IG2DGDnje+hImc0VVvbg zSgEZiK;Eni`7lIBp}uIQL=fJyfTCEtv&(x(Y_%k0@{S!l{+g5bDCCf5VUYZlqG1u) z6pI9O_8{O;`Fd4PL4>kb0@&|_1&r`V^QG&y30)D#of{? zyOroBbm+k@#?2GaS}9WO+#FDIX+K5Vi~Up2H)agR?|_$;M*^cRw|CQ(4zJpNvEMB` z+GDj`PXOH4=hPAlm~R~IMX+>>UcOpKVA3u9lo0CSb&&_$uN>05RzIKjAl-&hRUnvC zm3(C?`M?z;v8S@#XU@I^;jyB;v>lh|FLUn=iOJcv%ojSS_X8!5Zc!r_QAHI7v$lk- z4Kg6;%yXAeQ+#)6bPYPMwVM5c@qZ=j%HBe5kjKrmCf@k#wB1qO@ARM`3m`HNVNfcE zKHO8+o>!*6ZLL-P#8$239qg8u8ZEEk@sxrU-}RwRxdVX8=-e0aNEj2gR4wCG` z$+%v>bxkFXR3u}X+JG1Ex)iktGy_uMY`?f({q5b~?RY^*X6x4P8y>E_&$T>SFEDe5 zL#O|ff`TJQu__mU2-cjCkOAIw-FJ71fW*~bc;~k!*~Dn+J+Ys}B8=5eSm)3iu{_mB6`bpNw@SFm9j7;rFn72V=*sq0zf}u zhePwEHAceX8bpx{!|JS2y@!?l|-b>s7s6x(9NBr2ozFt9%u6Chv( z_Ndibkrig}yrr)F>c6q??udZQ_Q@EfV76r~?R?IwtNbNlm2cU73j9c2B1+fgU`5=O zc2po87La*E&q0ktPMd+{Z^XdA$VI~g!rOD^&1sVBupDdRkJC(z=`WjI_S~A{TzM@c z{;$=wS^E|~&*_YeiKvb<>-JSzBNeTmZkIc0aL5b3po@Lut6?Bhn{zpz2Y=Pk6)&!* zn5Bcx;#Ci?PT4+f=sJ&*wVE0nGO&9cp1bpm)*kKsit_~**l;?0;vfeY zsphL6yEdY$Uv2T)B9f^Sm3gHp`Y212ezluFvrHw=Z*VX^fKS%Ov@YJHee^|5a$k+l z8cx^mz=+Tw>I+d18%j&da1aCU7c=-X?U9d7cZk)KcNqDK6CXSBJQamco>swGJkNj^ zvNngj0(225lv@bV@Dxc?@^c)@aCEpFlHblSt%=b^#cE_>M=F1mM%v;C2C|!l2XZ%w zY^#5sE-P(R3I7TNc(4A=CNXKOKMt8)m^r6T>a3N=D{?bc^6>mourLI9e%&-1_pDwN zjf9J~%ev3I?f#bXst+GOIy#K?eE(jRAiEJsEjZE$9ZO=Uflnz1!S_}OLZ|MOCCfRc zq}2N(uFNf~G!t~0f8}UO^QX`Q1->hqMUiMBZXmtBHY3wvXtZyqRnG$_Yt02Xrb^5o zN?&v>r@*FK0eOE3&5?;I?L<@SjQFda)kxrT!qMm{7qJg9XfHCWNs`v`hC}no_5Nax z!J(ni-s0k7I0O%>Pk%B9Zc=6T6Ssxu9Z<1cw+1V>=v+IQBD-2Z| zn)|h)^sTD9Z*Of!y4n*zzsmCR$4~etj|261BX9w_`?@} { - const { name } = chunkInfo - if (/\.(png|jpe?g|gif|svg|webp|ico|bmp)$/i.test(name)) { - return `images/${name}` - } else if (name && name.endsWith('.css')) { - return `css/style-${version}[extname]` - } else { - return 'assets/[name]-[hash][extname]' - } - } - } - } - } -}) diff --git a/build/vite/def.js b/build/vite/def.js deleted file mode 100644 index 9be6654..0000000 --- a/build/vite/def.js +++ /dev/null @@ -1,6 +0,0 @@ -import { version } from './common.js' - -export default { - 'process.env.VER': JSON.stringify(version), - __DEFINES__: JSON.stringify('some value') -} diff --git a/build/vite/dev-server.js b/build/vite/dev-server.js deleted file mode 100644 index 51340b4..0000000 --- a/build/vite/dev-server.js +++ /dev/null @@ -1,157 +0,0 @@ -import logger from 'morgan' -import { viewPath, env, staticPaths, pack, isProd, cwd } from './common.js' -import express from 'express' -import { createServer as createViteServer } from 'vite' -import conf from './conf.js' -import copy from 'json-deep-copy' -import fs from 'fs' -import path from 'path' -import { spawn } from 'child_process' -import multer from 'multer' - -const devPort = env.DEV_PORT || 5570 -const host = env.DEV_HOST || '127.0.0.1' -const h = `http://${host}:${devPort}` -const defaultAIPreset = { - baseURLAI: 'https://ai.electerm.org/api/ai', - apiPathAI: '/chat/completions', - modelAI: 'mistral-small-latest', - authHeaderNameAI: 'Authorization: Bearer', - id: 'ai.electerm.org', - nameAI: 'ai.electerm.org(default free)' -} - -// const AIDisclamer = 'AI-generated terminal commands can be inaccurate or unsafe, be careful' - -const base = { - version: pack.version, - isDev: !isProd, - siteName: pack.name, - defaultAIPreset, - disableUpgradeCheck: true, - AIDisclamer: '本内容由 AI 生成,仅供参考', - hideLocalTerminal: true, - disableAIFeature: false, - supportSessionTypes: ['ssh', 'telnet', 'rdp', 'vnc', 'ftp', 'spice'] -} - -function handleIndex (req, res) { - const view = 'index' - res.render(view, { - ...base, - _global: copy(base) - }) -} - -function redirect (req, res) { - const { - name - } = req.params - const mapper = { - electerm: '/src/client/entry/electerm.jsx', - worker: '/src/client/entry/worker.js' - } - res.redirect(mapper[name]) -} - -async function createServer () { - const app = express() - - // Create Vite server in middleware mode and configure the app type as - // 'custom', disabling Vite's own HTML serving logic so parent server - // can take control - const vite = await createViteServer({ - ...conf, - server: { - middlewareMode: true, - hmr: { - port: 30085, - overlay: true - } - }, - appType: 'custom' - }) - - app.use( - logger('dev') - ) - app.use(express.json()) - app.use(express.urlencoded({ - extended: true - })) - staticPaths.forEach(({ path, dir }) => { - app.use( - path, - express.static(dir, { maxAge: '170d' }) - ) - }) - - const upload = multer({ dest: 'uploads/' }) - - app.get('/api/download', (req, res) => { - const filePath = req.query.path - if (!filePath) { - return res.status(400).json({ error: 'path is required' }) - } - try { - const stat = fs.statSync(filePath) - if (stat.isFile()) { - const fileName = path.basename(filePath) - res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`) - res.setHeader('Content-Type', 'application/octet-stream') - fs.createReadStream(filePath).pipe(res) - } else if (stat.isDirectory()) { - const dirName = path.basename(filePath) - const parentDir = path.dirname(filePath) - res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`) - res.setHeader('Content-Type', 'application/gzip') - const tar = spawn('tar', ['czf', '-', '-C', parentDir, dirName]) - tar.stdout.pipe(res) - tar.stderr.on('data', (data) => { - console.error('tar stderr:', data.toString()) - }) - tar.on('error', (err) => { - console.error('tar error:', err) - if (!res.headersSent) { - res.status(500).json({ error: err.message }) - } - }) - } else { - res.status(400).json({ error: 'path is not a file or directory' }) - } - } catch (err) { - console.error('download error:', err) - res.status(500).json({ error: err.message }) - } - }) - - app.post('/api/upload', upload.single('file'), (req, res) => { - const targetDir = req.body.path - if (!targetDir || !req.file) { - return res.status(400).json({ error: 'path and file are required' }) - } - try { - const destPath = path.join(targetDir, req.file.originalname) - fs.renameSync(req.file.path, destPath) - res.json({ success: true, path: destPath }) - } catch (err) { - console.error('upload error:', err) - res.status(500).json({ error: err.message }) - } - }) - - app.set('views', viewPath) - app.set('view engine', 'pug') - - // Use vite's connect instance as middleware. If you use your own - // express router (express.Router()), you should use router.use - app.use(vite.middlewares) - app.get(['/', '/index.html'], handleIndex) - app.get('/:dir/:name.:ext', redirect) - app.listen(devPort, host, () => { - console.log('cwd:', cwd) - console.log(`server started at ${h}`) - }) -} - -createServer() diff --git a/build/vite/diagnostics-channel-stub.js b/build/vite/diagnostics-channel-stub.js deleted file mode 100644 index c09ab70..0000000 --- a/build/vite/diagnostics-channel-stub.js +++ /dev/null @@ -1,55 +0,0 @@ -// Browser-safe stub for Node's `node:diagnostics_channel` module. -// -// Why this exists: -// @xterm/addon-ligatures (beta line) bundles lru-cache@11, which imports -// `channel`/`tracingChannel` from `node:diagnostics_channel` and calls them at -// module-load time (for optional metrics). In the Electron renderer / Vite dev -// server this is a browser context: Vite stubs Node builtins with a -// `browser-external` shim that throws on use, so `channel()` is undefined and -// the addon crashes on import. -// -// lru-cache only publishes metrics when `hasSubscribers` is true, which never -// happens here (we never subscribe). So all of these can be silent no-ops. - -function makeChannel () { - return { - publish () {}, - subscribe () { - return () => {} - }, - unsubscribe () {}, - bindStore (store) { - return store - }, - unbindStore () {}, - hasSubscribers: false - } -} - -export function channel () { - return makeChannel() -} - -export function tracingChannel () { - const ch = makeChannel() - return { - start: ch, - end: ch, - asyncStart: ch, - asyncEnd: ch, - error: ch, - trace (fn) { - return fn() - } - } -} - -export function hasSubscribers () { - return false -} - -export default { - channel, - tracingChannel, - hasSubscribers -} diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json deleted file mode 100644 index ef8dbda..0000000 --- a/build/vite/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "electerm", - "version": "1.29.5", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "electerm", - "version": "1.29.5" - } - } -} diff --git a/build/vite/package.json b/build/vite/package.json deleted file mode 100644 index e7b6715..0000000 --- a/build/vite/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "electerm", - "version": "1.29.5", - "main": "app.js", - "type": "module", - "scripts": { - "start": "npm run c", - "c": "node ./dev-server.js", - "build": "cross-env NODE_ENV=production vite build --config ./conf.js" - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json new file mode 100644 index 0000000..6bfce3d --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json @@ -0,0 +1,1407 @@ +{ + "entries" : + [ + { + "name" : "CMAKE_ADDR2LINE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-addr2line" + }, + { + "name" : "CMAKE_AR", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Archiver" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" + }, + { + "name" : "CMAKE_ASM_FLAGS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags for all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_ASM_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags for debug variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_ASM_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags for release variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_BUILD_TYPE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel ..." + } + ], + "type" : "STRING", + "value" : "Release" + }, + { + "name" : "CMAKE_CACHEFILE_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "This is the directory where this CMakeCache.txt was created" + } + ], + "type" : "INTERNAL", + "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a" + }, + { + "name" : "CMAKE_CACHE_MAJOR_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Major version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "3" + }, + { + "name" : "CMAKE_CACHE_MINOR_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Minor version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "28" + }, + { + "name" : "CMAKE_CACHE_PATCH_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Patch version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "2" + }, + { + "name" : "CMAKE_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to CMake executable." + } + ], + "type" : "INTERNAL", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake" + }, + { + "name" : "CMAKE_CPACK_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to cpack program executable." + } + ], + "type" : "INTERNAL", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack" + }, + { + "name" : "CMAKE_CTEST_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to ctest program executable." + } + ], + "type" : "INTERNAL", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest" + }, + { + "name" : "CMAKE_CXX_COMPILER_AR", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "LLVM archiver" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" + }, + { + "name" : "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "`clang-scan-deps` dependency scanner" + } + ], + "type" : "FILEPATH", + "value" : "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" + }, + { + "name" : "CMAKE_CXX_COMPILER_RANLIB", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Generate index for LLVM archive" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" + }, + { + "name" : "CMAKE_CXX_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_CXX_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for debug variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_CXX_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the CXX compiler during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "-Os -DNDEBUG" + }, + { + "name" : "CMAKE_CXX_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for release variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_CXX_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the CXX compiler during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "-O2 -g -DNDEBUG" + }, + { + "name" : "CMAKE_CXX_STANDARD_LIBRARIES", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Libraries linked by default with all C++ applications." + } + ], + "type" : "STRING", + "value" : "-lm" + }, + { + "name" : "CMAKE_C_COMPILER_AR", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "LLVM archiver" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" + }, + { + "name" : "CMAKE_C_COMPILER_CLANG_SCAN_DEPS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "`clang-scan-deps` dependency scanner" + } + ], + "type" : "FILEPATH", + "value" : "CMAKE_C_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" + }, + { + "name" : "CMAKE_C_COMPILER_RANLIB", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Generate index for LLVM archive" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" + }, + { + "name" : "CMAKE_C_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for debug variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the C compiler during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "-Os -DNDEBUG" + }, + { + "name" : "CMAKE_C_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags for release variant builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the C compiler during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "-O2 -g -DNDEBUG" + }, + { + "name" : "CMAKE_C_STANDARD_LIBRARIES", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Libraries linked by default with all C applications." + } + ], + "type" : "STRING", + "value" : "-lm" + }, + { + "name" : "CMAKE_DLLTOOL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "CMAKE_DLLTOOL-NOTFOUND" + }, + { + "name" : "CMAKE_EDIT_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to cache edit program executable." + } + ], + "type" : "INTERNAL", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ccmake" + }, + { + "name" : "CMAKE_EXECUTABLE_FORMAT", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Executable file format" + } + ], + "type" : "INTERNAL", + "value" : "ELF" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Linker flags to be used to create executables." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXPORT_COMPILE_COMMANDS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "ON" + }, + { + "name" : "CMAKE_EXTRA_GENERATOR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of external makefile project generator." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_FIND_PACKAGE_REDIRECTS_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake." + } + ], + "type" : "STATIC", + "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/pkgRedirects" + }, + { + "name" : "CMAKE_FIND_ROOT_PATH", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a" + }, + { + "name" : "CMAKE_GENERATOR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator." + } + ], + "type" : "INTERNAL", + "value" : "Ninja" + }, + { + "name" : "CMAKE_GENERATOR_INSTANCE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Generator instance identifier." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_GENERATOR_PLATFORM", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator platform." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_GENERATOR_TOOLSET", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator toolset." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_HOME_DIRECTORY", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Source directory with the top level CMakeLists.txt file for this project" + } + ], + "type" : "INTERNAL", + "value" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" + }, + { + "name" : "CMAKE_INSTALL_PREFIX", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Install path prefix, prepended onto install directories." + } + ], + "type" : "PATH", + "value" : "/usr/local" + }, + { + "name" : "CMAKE_INSTALL_SO_NO_EXE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Install .so files without execute permission." + } + ], + "type" : "INTERNAL", + "value" : "0" + }, + { + "name" : "CMAKE_LIBRARY_OUTPUT_DIRECTORY", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a" + }, + { + "name" : "CMAKE_LINKER", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" + }, + { + "name" : "CMAKE_MAKE_PROGRAM", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Linker flags to be used to create modules." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_NM", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-nm" + }, + { + "name" : "CMAKE_NUMBER_OF_MAKEFILES", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "number of local generators" + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_OBJCOPY", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objcopy" + }, + { + "name" : "CMAKE_OBJDUMP", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objdump" + }, + { + "name" : "CMAKE_OHOS_ARCH_ABI", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "arm64-v8a" + }, + { + "name" : "CMAKE_PLATFORM_INFO_INITIALIZED", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Platform information initialized" + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_PROJECT_DESCRIPTION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "" + }, + { + "name" : "CMAKE_PROJECT_HOMEPAGE_URL", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "" + }, + { + "name" : "CMAKE_PROJECT_NAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "electerm_web_runtime" + }, + { + "name" : "CMAKE_RANLIB", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Ranlib" + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" + }, + { + "name" : "CMAKE_READELF", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-readelf" + }, + { + "name" : "CMAKE_ROOT", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to CMake installation." + } + ], + "type" : "INTERNAL", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Linker flags to be used to create shared libraries." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SKIP_INSTALL_RPATH", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If set, runtime paths are not added when installing shared libraries, but are added when building." + } + ], + "type" : "BOOL", + "value" : "NO" + }, + { + "name" : "CMAKE_SKIP_RPATH", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If set, runtime paths are not added when using shared libraries." + } + ], + "type" : "BOOL", + "value" : "NO" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STRIP", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-strip" + }, + { + "name" : "CMAKE_SYSTEM_NAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "OHOS" + }, + { + "name" : "CMAKE_TAPI", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "CMAKE_TAPI-NOTFOUND" + }, + { + "name" : "CMAKE_TOOLCHAIN_FILE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake" + }, + { + "name" : "CMAKE_UNAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "uname command" + } + ], + "type" : "INTERNAL", + "value" : "/usr/bin/uname" + }, + { + "name" : "CMAKE_VERBOSE_MAKEFILE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If this value is on, makefiles will be generated without the .SILENT directive, and all commands will be echoed to the console during the make. This is useful for debugging only. With Visual Studio IDE projects all commands are done without /nologo." + } + ], + "type" : "BOOL", + "value" : "FALSE" + }, + { + "name" : "HMOS_SDK_NATIVE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native" + }, + { + "name" : "OHOS_ARCH", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "arm64-v8a" + }, + { + "name" : "OHOS_SDK_NATIVE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native" + }, + { + "name" : "PACKAGE_FIND_FILE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake" + }, + { + "name" : "UNIX", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "FROCE" + } + ], + "type" : "BOOL", + "value" : "TRUE" + }, + { + "name" : "_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "linker supports push/pop state" + } + ], + "type" : "INTERNAL", + "value" : "TRUE" + }, + { + "name" : "electerm_web_runtime_BINARY_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a" + }, + { + "name" : "electerm_web_runtime_IS_TOP_LEVEL", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "ON" + }, + { + "name" : "electerm_web_runtime_SOURCE_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" + }, + { + "name" : "node_ctl_LIB_DEPENDS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Dependencies for the target" + } + ], + "type" : "STATIC", + "value" : "general;libace_napi.z.so;" + }, + { + "name" : "node_launcher_LIB_DEPENDS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Dependencies for the target" + } + ], + "type" : "STATIC", + "value" : "general;libchild_process.so;" + } + ], + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json new file mode 100644 index 0000000..51616cc --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json @@ -0,0 +1,173 @@ +{ + "inputs" : + [ + { + "path" : "CMakeLists.txt" + }, + { + "isGenerated" : true, + "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" + }, + { + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake" + }, + { + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake" + }, + { + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/sdk_native_platforms.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeSystemSpecificInitialize.cmake" + }, + { + "isGenerated" : true, + "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake" + }, + { + "isGenerated" : true, + "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeSystemSpecificInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeGenericSystem.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeInitializeConfigs.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeLanguageInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang-C.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/CMakeCommonCompilerMacros.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/GNU.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/CMakeCommonCompilerMacros.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCommonLanguageInclude.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeLanguageInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang-CXX.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCommonLanguageInclude.cmake" + } + ], + "kind" : "cmakeFiles", + "paths" : + { + "build" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a", + "source" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" + }, + "version" : + { + "major" : 1, + "minor" : 0 + } +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json new file mode 100644 index 0000000..f57235c --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json @@ -0,0 +1,69 @@ +{ + "configurations" : + [ + { + "directories" : + [ + { + "build" : ".", + "jsonFile" : "directory-.-Release-f5ebdc15457944623624.json", + "minimumCMakeVersion" : + { + "string" : "3.6.0" + }, + "projectIndex" : 0, + "source" : ".", + "targetIndexes" : + [ + 0, + 1 + ] + } + ], + "name" : "Release", + "projects" : + [ + { + "directoryIndexes" : + [ + 0 + ], + "name" : "electerm_web_runtime", + "targetIndexes" : + [ + 0, + 1 + ] + } + ], + "targets" : + [ + { + "directoryIndex" : 0, + "id" : "node_ctl::@6890427a1f51a3e7e1df", + "jsonFile" : "target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json", + "name" : "node_ctl", + "projectIndex" : 0 + }, + { + "directoryIndex" : 0, + "id" : "node_launcher::@6890427a1f51a3e7e1df", + "jsonFile" : "target-node_launcher-Release-968f49e15038ddbf7844.json", + "name" : "node_launcher", + "projectIndex" : 0 + } + ] + } + ], + "kind" : "codemodel", + "paths" : + { + "build" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a", + "source" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" + }, + "version" : + { + "major" : 2, + "minor" : 6 + } +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json new file mode 100644 index 0000000..3a67af9 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json @@ -0,0 +1,14 @@ +{ + "backtraceGraph" : + { + "commands" : [], + "files" : [], + "nodes" : [] + }, + "installers" : [], + "paths" : + { + "build" : ".", + "source" : "." + } +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json new file mode 100644 index 0000000..0818490 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json @@ -0,0 +1,89 @@ +{ + "cmake" : + { + "generator" : + { + "multiConfig" : false, + "name" : "Ninja" + }, + "paths" : + { + "cmake" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake", + "cpack" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack", + "ctest" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest", + "root" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28" + }, + "version" : + { + "isDirty" : false, + "major" : 3, + "minor" : 28, + "patch" : 2, + "string" : "3.28.2", + "suffix" : "" + } + }, + "objects" : + [ + { + "jsonFile" : "codemodel-v2-1ac40039f16ae038c893.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + }, + { + "jsonFile" : "cache-v2-49f5662a4b05781cba4a.json", + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } + }, + { + "jsonFile" : "cmakeFiles-v1-e37c68776f2415d7fef8.json", + "kind" : "cmakeFiles", + "version" : + { + "major" : 1, + "minor" : 0 + } + } + ], + "reply" : + { + "cache-v2" : + { + "jsonFile" : "cache-v2-49f5662a4b05781cba4a.json", + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } + }, + "cmakeFiles-v1" : + { + "jsonFile" : "cmakeFiles-v1-e37c68776f2415d7fef8.json", + "kind" : "cmakeFiles", + "version" : + { + "major" : 1, + "minor" : 0 + } + }, + "codemodel-v2" : + { + "jsonFile" : "codemodel-v2-1ac40039f16ae038c893.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + } + } +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json new file mode 100644 index 0000000..e894c80 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json @@ -0,0 +1,155 @@ +{ + "artifacts" : + [ + { + "path" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_ctl.so" + } + ], + "backtrace" : 1, + "backtraceGraph" : + { + "commands" : + [ + "add_library", + "target_link_libraries", + "include_directories", + "include", + "project" + ], + "files" : + [ + "CMakeLists.txt", + "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake", + "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" + ], + "nodes" : + [ + { + "file" : 0 + }, + { + "command" : 0, + "file" : 0, + "line" : 11, + "parent" : 0 + }, + { + "command" : 1, + "file" : 0, + "line" : 12, + "parent" : 0 + }, + { + "command" : 4, + "file" : 0, + "line" : 2, + "parent" : 0 + }, + { + "file" : 2, + "parent" : 3 + }, + { + "command" : 3, + "file" : 2, + "line" : 6, + "parent" : 4 + }, + { + "file" : 1, + "parent" : 5 + }, + { + "command" : 2, + "file" : 1, + "line" : 25, + "parent" : 6 + } + ] + }, + "compileGroups" : + [ + { + "compileCommandFragments" : + [ + { + "fragment" : "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -D__MUSL__ -O2 -DNDEBUG -fPIC" + } + ], + "defines" : + [ + { + "define" : "node_ctl_EXPORTS" + } + ], + "includes" : + [ + { + "backtrace" : 7, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/sysroot/usr/include" + } + ], + "language" : "C", + "sourceIndexes" : + [ + 0 + ], + "sysroot" : + { + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" + } + } + ], + "id" : "node_ctl::@6890427a1f51a3e7e1df", + "link" : + { + "commandFragments" : + [ + { + "fragment" : "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack", + "role" : "flags" + }, + { + "backtrace" : 2, + "fragment" : "-lace_napi.z", + "role" : "libraries" + }, + { + "fragment" : "-lm", + "role" : "libraries" + } + ], + "language" : "C", + "sysroot" : + { + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" + } + }, + "name" : "node_ctl", + "nameOnDisk" : "libnode_ctl.so", + "paths" : + { + "build" : ".", + "source" : "." + }, + "sourceGroups" : + [ + { + "name" : "Source Files", + "sourceIndexes" : + [ + 0 + ] + } + ], + "sources" : + [ + { + "backtrace" : 1, + "compileGroupIndex" : 0, + "path" : "node_ctl.c", + "sourceGroupIndex" : 0 + } + ], + "type" : "SHARED_LIBRARY" +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json new file mode 100644 index 0000000..1b96900 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json @@ -0,0 +1,155 @@ +{ + "artifacts" : + [ + { + "path" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_launcher.so" + } + ], + "backtrace" : 1, + "backtraceGraph" : + { + "commands" : + [ + "add_library", + "target_link_libraries", + "include_directories", + "include", + "project" + ], + "files" : + [ + "CMakeLists.txt", + "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake", + "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" + ], + "nodes" : + [ + { + "file" : 0 + }, + { + "command" : 0, + "file" : 0, + "line" : 6, + "parent" : 0 + }, + { + "command" : 1, + "file" : 0, + "line" : 7, + "parent" : 0 + }, + { + "command" : 4, + "file" : 0, + "line" : 2, + "parent" : 0 + }, + { + "file" : 2, + "parent" : 3 + }, + { + "command" : 3, + "file" : 2, + "line" : 6, + "parent" : 4 + }, + { + "file" : 1, + "parent" : 5 + }, + { + "command" : 2, + "file" : 1, + "line" : 25, + "parent" : 6 + } + ] + }, + "compileGroups" : + [ + { + "compileCommandFragments" : + [ + { + "fragment" : "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -D__MUSL__ -O2 -DNDEBUG -fPIC" + } + ], + "defines" : + [ + { + "define" : "node_launcher_EXPORTS" + } + ], + "includes" : + [ + { + "backtrace" : 7, + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/sysroot/usr/include" + } + ], + "language" : "C", + "sourceIndexes" : + [ + 0 + ], + "sysroot" : + { + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" + } + } + ], + "id" : "node_launcher::@6890427a1f51a3e7e1df", + "link" : + { + "commandFragments" : + [ + { + "fragment" : "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack", + "role" : "flags" + }, + { + "backtrace" : 2, + "fragment" : "-lchild_process", + "role" : "libraries" + }, + { + "fragment" : "-lm", + "role" : "libraries" + } + ], + "language" : "C", + "sysroot" : + { + "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" + } + }, + "name" : "node_launcher", + "nameOnDisk" : "libnode_launcher.so", + "paths" : + { + "build" : ".", + "source" : "." + }, + "sourceGroups" : + [ + { + "name" : "Source Files", + "sourceIndexes" : + [ + 0 + ] + } + ], + "sources" : + [ + { + "backtrace" : 1, + "compileGroupIndex" : 0, + "path" : "node_launcher.c", + "sourceGroupIndex" : 0 + } + ], + "type" : "SHARED_LIBRARY" +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.ninja_deps b/entry/.cxx/default/default/release/arm64-v8a/.ninja_deps new file mode 100644 index 0000000000000000000000000000000000000000..0a9ec3e161ae921fa859ea7cb980c80239e7d6a4 GIT binary patch literal 6144 zcmd6rTW=IM7>1`nDW#lS3WY+U&=L+i1}SjY3zSfS5U5hc6&Hi!@n%fC_Q>`|yWBJv z{UP~999p1-ay~;LP|Eqx@_KhC2?)5TGTugdlF26H_dD31KYPt8CDcKh^4!jz5k*lC z=-Wew`1e9`OO#G|!kmm#VJ3cIvHlld{rF$9&vIkQp+-tlK1{ge%yCnzWYpBOY7(xT zX%cG~sZpT_%kxRY;QfCvj`QPe&vPjlbwVpk`uXqparPv%?*m#(IR6;BjVbi~ZENVYJSVQ1bK9EJk3#z_6e1l(525SV zahY`kIc}*vfY#3?Yf)-gwyn1!g{n6yI@6X62$VZ2<#@3Fh8-havZIK_eb`WX-R#t` zVw(LGBG*{M6pQH|w92m0RZd;qv=`9bZsxS(^H_lR)|J2%inBXi^f%X5G*CQl!-lfw za8(;8s!dY({Ul%Y;mm+-Syadq2D$AkVvg=<_y+oFy#BzQIwqu0gl# z{g4}@G-lORXpQ*1mDsbtYdU$ia+$gS^Z{9HaCFs_B>xiJ3W)D&lD18AW~)r`j3nXs{J`S(Wv|Ex1YVP>9& zR@uE_TdTd7oC+GxwvWGu@Udx0twvjyBELbeTYE`i!^uECTSr@G*Ardzxv=3lYzVcV z6q>&V(8S$^zXN2bNq#Gdet~A$ec`)$mC1G-gB_*U4)|VN7953M>HVNeg@1y+Nv t!5Xj@d;`7(>p%}!4>o{}U=yf-7!a@-Yyp$cMWY=XcbWxz`~Rmlz5*il!BGGJ literal 0 HcmV?d00001 diff --git a/entry/.cxx/default/default/release/arm64-v8a/.ninja_log b/entry/.cxx/default/default/release/arm64-v8a/.ninja_log new file mode 100644 index 0000000..3a75757 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/.ninja_log @@ -0,0 +1,6 @@ +# ninja log v6 +3 317 1787891931146197160 CMakeFiles/node_launcher.dir/node_launcher.c.o fc4caae051097ca9 +1 193 1787891898261950083 CMakeFiles/node_ctl.dir/node_ctl.c.o 34746826cf4d1478 +2 655 1787891961332981498 /Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_ctl.so 423c0b9631ec54d +1 713 1787891961332550252 CMakeFiles/node_launcher.dir/node_launcher.c.o fc4caae051097ca9 +714 829 1787891962044813141 /Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_launcher.so 3422c9625d62cd06 diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt b/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt new file mode 100644 index 0000000..3125a89 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt @@ -0,0 +1,421 @@ +# This is the CMakeCache file. +# For build in directory: /Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a +# It was generated by CMake: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Path to a program. +CMAKE_ADDR2LINE:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-addr2line + +//Archiver +CMAKE_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar + +//Flags for all build types. +CMAKE_ASM_FLAGS:STRING= + +//Flags for debug variant builds. +CMAKE_ASM_FLAGS_DEBUG:STRING= + +//Flags for release variant builds. +CMAKE_ASM_FLAGS_RELEASE:STRING= + +//Choose the type of build, options are: None Debug Release RelWithDebInfo +// MinSizeRel ... +CMAKE_BUILD_TYPE:STRING=Release + +//LLVM archiver +CMAKE_CXX_COMPILER_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar + +//`clang-scan-deps` dependency scanner +CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS:FILEPATH=CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND + +//Generate index for LLVM archive +CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib + +//Flags for all build types. +CMAKE_CXX_FLAGS:STRING= + +//Flags for debug variant builds. +CMAKE_CXX_FLAGS_DEBUG:STRING= + +//Flags used by the CXX compiler during MINSIZEREL builds. +CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags for release variant builds. +CMAKE_CXX_FLAGS_RELEASE:STRING= + +//Flags used by the CXX compiler during RELWITHDEBINFO builds. +CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Libraries linked by default with all C++ applications. +CMAKE_CXX_STANDARD_LIBRARIES:STRING=-lm + +//LLVM archiver +CMAKE_C_COMPILER_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar + +//`clang-scan-deps` dependency scanner +CMAKE_C_COMPILER_CLANG_SCAN_DEPS:FILEPATH=CMAKE_C_COMPILER_CLANG_SCAN_DEPS-NOTFOUND + +//Generate index for LLVM archive +CMAKE_C_COMPILER_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib + +//Flags for all build types. +CMAKE_C_FLAGS:STRING= + +//Flags for debug variant builds. +CMAKE_C_FLAGS_DEBUG:STRING= + +//Flags used by the C compiler during MINSIZEREL builds. +CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags for release variant builds. +CMAKE_C_FLAGS_RELEASE:STRING= + +//Flags used by the C compiler during RELWITHDEBINFO builds. +CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Libraries linked by default with all C applications. +CMAKE_C_STANDARD_LIBRARIES:STRING=-lm + +//Path to a program. +CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND + +//Linker flags to be used to create executables. +CMAKE_EXE_LINKER_FLAGS:STRING= + +//Flags used by the linker during DEBUG builds. +CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during MINSIZEREL builds. +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during RELEASE builds. +CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during RELWITHDEBINFO builds. +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//No help, variable specified on the command line. +CMAKE_EXPORT_COMPILE_COMMANDS:UNINITIALIZED=ON + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/pkgRedirects + +//No help, variable specified on the command line. +CMAKE_FIND_ROOT_PATH:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a + +//Install path prefix, prepended onto install directories. +CMAKE_INSTALL_PREFIX:PATH=/usr/local + +//No help, variable specified on the command line. +CMAKE_LIBRARY_OUTPUT_DIRECTORY:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a + +//Path to a program. +CMAKE_LINKER:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld + +//No help, variable specified on the command line. +CMAKE_MAKE_PROGRAM:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja + +//Linker flags to be used to create modules. +CMAKE_MODULE_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of modules during +// DEBUG builds. +CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of modules during +// MINSIZEREL builds. +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of modules during +// RELEASE builds. +CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of modules during +// RELWITHDEBINFO builds. +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_NM:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-nm + +//Path to a program. +CMAKE_OBJCOPY:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objcopy + +//Path to a program. +CMAKE_OBJDUMP:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objdump + +//No help, variable specified on the command line. +CMAKE_OHOS_ARCH_ABI:UNINITIALIZED=arm64-v8a + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=electerm_web_runtime + +//Ranlib +CMAKE_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib + +//Path to a program. +CMAKE_READELF:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-readelf + +//Linker flags to be used to create shared libraries. +CMAKE_SHARED_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of shared libraries +// during DEBUG builds. +CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of shared libraries +// during MINSIZEREL builds. +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELEASE builds. +CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELWITHDEBINFO builds. +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//If set, runtime paths are not added when installing shared libraries, +// but are added when building. +CMAKE_SKIP_INSTALL_RPATH:BOOL=NO + +//If set, runtime paths are not added when using shared libraries. +CMAKE_SKIP_RPATH:BOOL=NO + +//Flags used by the linker during the creation of static libraries +// during all build types. +CMAKE_STATIC_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of static libraries +// during DEBUG builds. +CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of static libraries +// during MINSIZEREL builds. +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELEASE builds. +CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELWITHDEBINFO builds. +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_STRIP:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-strip + +//No help, variable specified on the command line. +CMAKE_SYSTEM_NAME:UNINITIALIZED=OHOS + +//Path to a program. +CMAKE_TAPI:FILEPATH=CMAKE_TAPI-NOTFOUND + +//No help, variable specified on the command line. +CMAKE_TOOLCHAIN_FILE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake + +//If this value is on, makefiles will be generated without the +// .SILENT directive, and all commands will be echoed to the console +// during the make. This is useful for debugging only. With Visual +// Studio IDE projects all commands are done without /nologo. +CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE + +//No help, variable specified on the command line. +HMOS_SDK_NATIVE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native + +//No help, variable specified on the command line. +OHOS_ARCH:UNINITIALIZED=arm64-v8a + +//No help, variable specified on the command line. +OHOS_SDK_NATIVE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native + +//No help, variable specified on the command line. +PACKAGE_FIND_FILE:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake + +//FROCE +UNIX:BOOL=TRUE + +//Value Computed by CMake +electerm_web_runtime_BINARY_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a + +//Value Computed by CMake +electerm_web_runtime_IS_TOP_LEVEL:STATIC=ON + +//Value Computed by CMake +electerm_web_runtime_SOURCE_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/src/main/cpp + +//Dependencies for the target +node_ctl_LIB_DEPENDS:STATIC=general;libace_napi.z.so; + +//Dependencies for the target +node_launcher_LIB_DEPENDS:STATIC=general;libchild_process.so; + + +######################## +# INTERNAL cache entries +######################## + +//ADVANCED property for variable: CMAKE_ADDR2LINE +CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_AR +CMAKE_AR-ADVANCED:INTERNAL=1 +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=28 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=2 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest +//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR +CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS +CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB +CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS +CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG +CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL +CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE +CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO +CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_STANDARD_LIBRARIES +CMAKE_CXX_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_AR +CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_CLANG_SCAN_DEPS +CMAKE_C_COMPILER_CLANG_SCAN_DEPS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB +CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS +CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG +CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL +CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE +CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO +CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_STANDARD_LIBRARIES +CMAKE_C_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_DLLTOOL +CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 +//Path to cache edit program executable. +CMAKE_EDIT_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ccmake +//Executable file format +CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS +CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG +CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE +CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Ninja +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=/Users/zxd/dev/electerm-harmony/entry/src/main/cpp +//Install .so files without execute permission. +CMAKE_INSTALL_SO_NO_EXE:INTERNAL=0 +//ADVANCED property for variable: CMAKE_LINKER +CMAKE_LINKER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS +CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG +CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE +CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_NM +CMAKE_NM-ADVANCED:INTERNAL=1 +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//ADVANCED property for variable: CMAKE_OBJCOPY +CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_OBJDUMP +CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_RANLIB +CMAKE_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_READELF +CMAKE_READELF-ADVANCED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS +CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG +CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE +CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH +CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_RPATH +CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS +CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG +CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE +CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STRIP +CMAKE_STRIP-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_TAPI +CMAKE_TAPI-ADVANCED:INTERNAL=1 +//uname command +CMAKE_UNAME:INTERNAL=/usr/bin/uname +//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE +CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 +//linker supports push/pop state +_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE + diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake new file mode 100755 index 0000000..1f37c04 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake @@ -0,0 +1,74 @@ +set(CMAKE_C_COMPILER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang") +set(CMAKE_C_COMPILER_ARG1 "") +set(CMAKE_C_COMPILER_ID "Clang") +set(CMAKE_C_COMPILER_VERSION "15.0.4") +set(CMAKE_C_COMPILER_VERSION_INTERNAL "") +set(CMAKE_C_COMPILER_WRAPPER "") +set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "17") +set(CMAKE_C_EXTENSIONS_COMPUTED_DEFAULT "ON") +set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23") +set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes") +set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros") +set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert") +set(CMAKE_C17_COMPILE_FEATURES "c_std_17") +set(CMAKE_C23_COMPILE_FEATURES "c_std_23") + +set(CMAKE_C_PLATFORM_ID "Linux") +set(CMAKE_C_SIMULATE_ID "") +set(CMAKE_C_COMPILER_FRONTEND_VARIANT "GNU") +set(CMAKE_C_SIMULATE_VERSION "") + + + + +set(CMAKE_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") +set(CMAKE_C_COMPILER_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") +set(CMAKE_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") +set(CMAKE_C_COMPILER_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") +set(CMAKE_LINKER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld") +set(CMAKE_MT "") +set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") +set(CMAKE_COMPILER_IS_GNUCC ) +set(CMAKE_C_COMPILER_LOADED 1) +set(CMAKE_C_COMPILER_WORKS TRUE) +set(CMAKE_C_ABI_COMPILED TRUE) + +set(CMAKE_C_COMPILER_ENV_VAR "CC") + +set(CMAKE_C_COMPILER_ID_RUN 1) +set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m) +set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC) +set(CMAKE_C_LINKER_PREFERENCE 10) +set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE) + +# Save compiler ABI information. +set(CMAKE_C_SIZEOF_DATA_PTR "8") +set(CMAKE_C_COMPILER_ABI "ELF") +set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_C_LIBRARY_ARCHITECTURE "") + +if(CMAKE_C_SIZEOF_DATA_PTR) + set(CMAKE_SIZEOF_VOID_P "${CMAKE_C_SIZEOF_DATA_PTR}") +endif() + +if(CMAKE_C_COMPILER_ABI) + set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_C_COMPILER_ABI}") +endif() + +if(CMAKE_C_LIBRARY_ARCHITECTURE) + set(CMAKE_LIBRARY_ARCHITECTURE "") +endif() + +set(CMAKE_C_CL_SHOWINCLUDES_PREFIX "") +if(CMAKE_C_CL_SHOWINCLUDES_PREFIX) + set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_C_CL_SHOWINCLUDES_PREFIX}") +endif() + + + + + +set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include") +set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "unwind;-l:libunwind.a;c;-l:libunwind.a") +set(CMAKE_C_IMPLICIT_LINK_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos") +set(CMAKE_C_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake new file mode 100755 index 0000000..799bb9c --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake @@ -0,0 +1,85 @@ +set(CMAKE_CXX_COMPILER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++") +set(CMAKE_CXX_COMPILER_ARG1 "") +set(CMAKE_CXX_COMPILER_ID "Clang") +set(CMAKE_CXX_COMPILER_VERSION "15.0.4") +set(CMAKE_CXX_COMPILER_VERSION_INTERNAL "") +set(CMAKE_CXX_COMPILER_WRAPPER "") +set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "14") +set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT "ON") +set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") +set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") +set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") +set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") +set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") +set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") +set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") + +set(CMAKE_CXX_PLATFORM_ID "Linux") +set(CMAKE_CXX_SIMULATE_ID "") +set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT "GNU") +set(CMAKE_CXX_SIMULATE_VERSION "") + + + + +set(CMAKE_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") +set(CMAKE_CXX_COMPILER_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") +set(CMAKE_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") +set(CMAKE_CXX_COMPILER_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") +set(CMAKE_LINKER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld") +set(CMAKE_MT "") +set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") +set(CMAKE_COMPILER_IS_GNUCXX ) +set(CMAKE_CXX_COMPILER_LOADED 1) +set(CMAKE_CXX_COMPILER_WORKS TRUE) +set(CMAKE_CXX_ABI_COMPILED TRUE) + +set(CMAKE_CXX_COMPILER_ENV_VAR "CXX") + +set(CMAKE_CXX_COMPILER_ID_RUN 1) +set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;m;mm;mpp;CPP;ixx;cppm;ccm;cxxm;c++m) +set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) + +foreach (lang C OBJC OBJCXX) + if (CMAKE_${lang}_COMPILER_ID_RUN) + foreach(extension IN LISTS CMAKE_${lang}_SOURCE_FILE_EXTENSIONS) + list(REMOVE_ITEM CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${extension}) + endforeach() + endif() +endforeach() + +set(CMAKE_CXX_LINKER_PREFERENCE 30) +set(CMAKE_CXX_LINKER_PREFERENCE_PROPAGATES 1) +set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE) + +# Save compiler ABI information. +set(CMAKE_CXX_SIZEOF_DATA_PTR "8") +set(CMAKE_CXX_COMPILER_ABI "ELF") +set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_CXX_LIBRARY_ARCHITECTURE "") + +if(CMAKE_CXX_SIZEOF_DATA_PTR) + set(CMAKE_SIZEOF_VOID_P "${CMAKE_CXX_SIZEOF_DATA_PTR}") +endif() + +if(CMAKE_CXX_COMPILER_ABI) + set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_CXX_COMPILER_ABI}") +endif() + +if(CMAKE_CXX_LIBRARY_ARCHITECTURE) + set(CMAKE_LIBRARY_ARCHITECTURE "") +endif() + +set(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX "") +if(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX) + set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_CXX_CL_SHOWINCLUDES_PREFIX}") +endif() + + + + + +set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include") +set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "unwind;c++;c++abi;unwind;m;-l:libunwind.a;c;-l:libunwind.a") +set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos") +set(CMAKE_CXX_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin new file mode 100755 index 0000000000000000000000000000000000000000..d5da485fd4a4f9f59e0145e3d861431ce761c2b7 GIT binary patch literal 14168 zcmds8eQ;dWb-(Yu{nDo;*|GeEB^w*ehgK4nZP|RuwrnhaV*>$ulE0oFJc!Hz;&UyFj zKE0I+#36t5j@~=>{_Z*Fp8I|8*{8eLZC-C0hLT{Z?<-0@Uhg0sI^(4#9RcZ3?Wz** zi_{_&K-$Ncli%SGZrVzba^l7@`T>6 z8$zh)+3|WlM(y%Zc6LxBAV_;|*K((*Py{c%+<}!)Y5VITr$sc!$0H=qI&^z051psg z2$lMY+&s{xSl$-SWx}nRpSp*5sXexB*tS!{ zoF;q*9xC#>tv9M6^V~yU|EtsQANt-maeMvV=D~@d`jIGQaeMoeLD=zo->LqOe|o!`FzE+9 zy?{7!(sp`SG46ip>EU#cyBV7k7w^{Ukr9heCO~S@!`cX$ua+JV?X#SWXZ5@i+^5^+IS>A*=1=Ge zApHHa9X$*C(u2#rpndneHZfCOSI8ihlkZ8;tEMaU6VMLOJSfd)3dS8f)+pny`NrHznv%S}(VBA# z`$dd^&Zf@lmJO8`)O^$YhR^J*Y-rg~4e-@}qfCc?Gw3Ol!)q1s*K}%ru-A0D+1=&3 zi)EbT$6)vUq`0%XkqFt@hUZr3cegX;*^8_fFHrkmn2%@FQo{|6){6%oR5cC1y6cb8 zw=?d((0XzF8TI_RJI!O~4yoZIhZ;UOddU7DWOST8y0+ms#}}yKv7kCTverKSTCn3R z>1eEg4$$rzhoUWNUbh+^c|fVJoV#<)G3Xo~hwhV>{Zq1G{1BC496PsG4P$+HEVO$4 z*^r^$8LNM2&e3NYjstrxG?H7HP-;2Zg*9=;(P!-Am#F0emy?U zTLkzR`i#bf$UZl!C!fe+ymBbhjCy-OM>_gMP9LAum|hHJZ5`BdvZfx<>!z$rA}?pF z4$WBMDNokel(rA^i9=e`|M`t$k1QUthAh$%y?qtVvvaka)KiWLvQP3F2Ojhfhe}Ol zzmtQI>~lK%QzP58fgeT10HfAeym-3bTAZz~sh_*Jc5YoDhdwf=fx!wJlgdK?ybWH`ZIBxuM0O>F6J`Tv?%2 zfhz;GG-kX87&l&+tIv}OZP3v*{o2`7wu$j%kxf3YO>6=&3R_^hQBgq&m{G+lDAA5& zsAyH0VUV5W3t;XFWK~dBMa4CURytLv`i#05)vb1{AQgv=X=H}iVs0+Ct(wnrittGh z{)@P20y9CJ2A$AOY@B1kZ{K8LRoY>e7ep13c>K!XL2cm9>cjw=yvY-8S{ya{ETh_I z4iEZ_YPys8&Eb5&sM*EQ`$8e3_Vb9Uz)g4s{r*re==TM>2}uxAhIL^Rx|s@#0x+;= zqCBNC(c7ByiDWir_v4^D)J*q~=42X&+`eQi8Og_UxS^y6206pdWxMVEOg0~nHFsz8 z&HBDV@nkrsns3a-v$^K}T%sq^ozG;4ns3i!_vHE`-SOr-lCelSZs&$@e(tmTll^h1 zjpIa}ibrzs=1juQ#rD|C!!6;K=I%^ie=-$knn}mgdAmCk1J6Ld8BBZ)YFrYF@4KY2 zI~7UyHts{I$xOPjbs5Cl8keL}`}&&tvzgoC-TB7W)@9MZ%u%i5!D?UyKHzscu+w2H#`D_l`bz7G&O6Hj3Qc?Jn;&I6#_ckPJy z(8jWqDn+BT36E(l$6HmHBg*mx^N1RKRqA5Dub1E@=7`}B+)UB1IbsGnR5%#i!%4hC ze$yx;@$%a#r7A+9pug-7DSMSUVpZOQNR=;i3dHEER=fP=taP_I;#2+#<{LCe{MD6D zkVtjN90?TF0oj0us<9N7b2aV)aE7Wu1IM)ZzESBT)5!|ob$A)dej8}#{o zp+G388X!ixzDl|_qaD_P;deE{(*GYun7W3V3{CX7H1`nz-$zpwN6jD*Iu@9`HyRmB z)mjGaZ$_UFLy$IP+FdV&1`6enJc#&lJb#V&G0?rBuYo>-{0=!?;=iiQN=z$-g%9I zomgI4!`&*}oX=!Z-3i>qo841f){ATtmLzhg(9(n_iradct!&+4D0OCHJQWawr%cfLz8-MAESoxdUi%NwRMsrtCGS3NY~&0CAiM5o6_t7LtLCepW0TqIho!Q&#r3Rytz~Fs_H&%!*)=^Fn(QK zMFevjy8*=-Oq%egk+2Y-=AVxgLQ~J*-}cJbt2cdn@kM36C70cBHF2l%d&2+nRP4z| z{{5Fvy?6a*r>Qbin$oH^{8iGvHxvj6DoU%&U7hgq}qw{?->pH*%ync)wj zYW~2XfOXSGe{I0J>psL$)Vu1S-}oYzMduM}+^PGU z>blL<-?SO1pY6N6ZPlB#UznLVT7K)bKYqEB`nzWD8?%nx{I?^Cu{ZWU|6dhfq7%x; z@3?ost!}=$^6kW)*Pnm-!*4$@+Pm+I+>V#)OV)H(uXSjw-ofaw_3?|M{ZhUE%G)h( z?fg#b1F5@I@Vdy4xG0%=R;}ST%BL4ciOfZe62HIv2YzD{H{b7Q$NL03*C$=)WP3a> zON*P`d8u(-B@jXVDIPC|4=TQ$rSD#?)`fpm&LWEJ2ildLNuAr zo!=hrvWG4<=To=e*N*o|yWiR7XoE5pEu`385M(K&9f*Rgfs%I6;@gq`N6dUjUS_0| z!r@cTMoAYXo!A8+G(9N<3xx-Z#md%4i#H~svVwUlbjnZ@1Pi4Gi{-(qz?ut!%15VY z(l?IaL!of5JI-}ri||m8rJ2?Z3LB~$#mUZT;gePP6r-hl8ZIG@Iq!^nGaP?iRpEDS z$*<-}RpF>1I91O%a|_~7I)y7G-ZKZIH?>#c{J?SNwUEi&!fisbF6PKQNfzkk9CNf4 zKtx(OQCT>$NFHB&m3TJ=#qS;*ZQxr%5mjmp5(b8(^MjoJ?E+XAGZk@>q2C#Ctt|{( z!FO_`^!G&C)|~qcM+&dcacn^$Es-ckW+{gXsuW+c(atn=+#$t@`HK82?y#2=rS;th z+^0&%!vWx?swx?eblt~=zkqWRulw;Bs$R*_lg1f^qj>0AFGooR@gi>k@yijlLuZ@@ zrEOOjCuQ}7QpF7^mcCrm@t7W}}nkT`zs zA};(@Y(UW-!RuI0osy%s=$Xy@MO^sjGd@cl5I|G98PoiFl4&vX5>78r3yKiOni-#? zT0HnN#!Khjm5k3T__0N7HRGl8?mEU73R{70VSGM2qzWJ+I~i|a&x{+N0YAQB7W+%u z=N9l!Ha`1UPnDA2nMGF?xS`6`K7F;3nSZawzoPL*E%28be_Z3vdWY{|Sbw>?RpX-n zgvLd`;7@5>^b7us#znv2&oW-xo~IeFREeN)I`jqLleNRYYJTth8fAQzBDb15yw2&_ z>JXMPcX*5OIs6;28-JVe(sA`bp!$q8) zugKfz4wrGdbiHi>K3PAmV*aXwVaUv|XnHNW6e_Dzh}DfwMW@ZF5h(&JiC z{C>{(Z1pbM+a2PJFHpPOAmX<%K1V(1!844Pj*~p&^Hjfwe~9tYadJ1~#eNsDKVW=5 zd-Mb&dJp5J?R*95W17YGl=1T*@tf0?d@m-thJn|io#lHs!ACSM{(+dzctqo}{?PtM z;k%5No(I0ic_|2n8B$?_B@XS8z9hV} zy%~t%<*++tL~}V98IKP1+L3598{a1>{&Y&`z*0NY)02zi)>ech8i|p4_(`ab8^KXV zza0q!|Bl49)dgkYyA$ zTe+E9+tu1Fkv;LXYn|Wrva8p0;f4>ZTHq}1$=JousqAR7*T(HM8A+F9rIPu4Dn20> ztdc3*iA>?jjj}9a!4j}-A zR&_l0%I>^n>*_6CYgNIM?|hWVBgJvve`=pBV%!ve$l&gBeK9wsm)!f)f%8MbBx~ev z1JUt-9xSgMMs-A}_B-fYhqsVGgznpP#1nbBZwi%tUS#AvENRm3mY?c-U*VViPtr|@ z(2vYgeuv)wg`U)mR0h>k%6IPfO40YY@8O#sk;kFN37RgNm@LR^SkZU8^lP2S%Xc?I zH+bYd>iPaaCX2j$=Oa|U^U-R!4iYE21rO;I`B7bfQ2CBY%JtU&vs!))Cfc7mr=r$CM}kh+Dq!os^dMo>yz! z#vo4TU%D&mX;bhI$p063+sm8&bI9_yRiG5)y#A~%-&>wn{xQf<-KBgv&&vNr$p1gk z{G}lBqW1(as=vs~_uC2WX>BJVR}guDo1M(efR_sHs=}?>zGI)8(j^_wsww zry)<6*e`ykF)i=->vZ9ox0q7u1tiFRZxH$SJei)P8ik-2BVDg*2SS@?zQZ}}J0JA$bx zzA~AvvjP^^zvS&<-O$}*BkG{p7lrMg_563;_mWoeI?!R1`+r2Zk$5? zXIkFzw-%Ln<#$aXzg;`55=BvtdB=Y*STfjPFkVSmwq+ZGWXr~~W!c!6_%Zadt6fQpSG!_& zWh@}^E0a1y2`IFM=_JK&nZaftWm-D1C_|j$zS1%=v?Qbh)Zr1}{GrIRX=XbuxdEN8)_Gtgw_I0LVC<&JOzM|CWY6of78R3YIfHbQnRfhLv zYMu%q9bnAKZ*~ZBop96~O~V?uKuPYb&iB)qnop>dkSNJb6iyd8IYL#XlP0+!9>Jx4 zLd&Q?2tvJbRL3(VoUP7WIhxk%@(cCLsd9&3tr|_G9d6LD$dNj-H>~XqYkNYE>V^<1 zdbYlqk5jvRW;v-55Tre~Xu1C&qX>R_l>;lG()Kq%PK#)c&qqj}b?Ek1J~~^g5i0c) zx!IcEPv>blp=<+KT`C!?OT`!U_UBTIqS0(;&(g+7E)!|c{AHlTOYO0F{pM{N<}~3` z@sP>uJGQHbKl$c&UthiQ?pLnKezN+>_A}>qWQ2VTrEZ@*234vCSnW9Yk_qtZfL}5W zf5!y;RFBE3Le`%7`9#+4%0_z=;O@!* zjpjw7b6=FA(Nyw|1Tx}@@dQZi!i9FOCz?&f(bb9oq8#M=we$d4Ki$D!(%+fjQ*}F? z_2Exz{v-N>5dH%*96k5>aOsB!eYo2XdR`DcGEW@U{07P?ZAcc5dvN)52!FzZ>#7l~ zPI_=}AHAe;6U@~263%*Xuiaq}?zQ`#2lv|j*n@lRwsoxAydsDG%XHcCzI=9febdcK zb+m0>7h91ZuoKx#BECD@w%WeFV{Kb&M~gx(%ftrq2|JTTpYLvOyYa^Mwf5SM*0z=o z$&bd8yRnubsf=0H$_1cQp2{kvT0tMf$M`hpHB&Ihfi{EYLFxOZVBE2FwKDFSW6Ua} zPn737nuAVb{fHCLx~R3Hetp@+m49x2H(<7w)zq)A0Ql-Fl;sJhwo&200)$`}?G*6sAtOk!BuKD=*Vf*8-(R}XsnwpawpQ{Fki`2Pe zYwVM+6*ZqD9kr#<0oqyVP_#wmn^w)E_bc_s=kHv70y+mrp!<|%|Afjga+quwC(f@? zgP1R#2;Z>oT-Z?W4_7}l^Z2thCxM*_&&@CIQEDlb3v=YuF}j zT+}?|maS#U@*~PA+iqQ!;nL=zf7Tf6oKXkYo!g=Fs9ussbvcz9soXwJ9jMJlYShr# zS!(F#rRvdcsYOP4^Eu7)ZnKhBM+Rv^M}X1{VaKeOF#HA8kZE_gC2ZBPgLEy?p)>S z#e=nG%}=Oq@^D@nc`+xC(^;esdD454{tHj~4y6Cyll~gguQ_SQ>ex5N4u>vHC4IY2 z&(Re9$l*65BfMgOkD-e+E=1PB_w>goau~lHiZ!ENF3_=#K9SSgYxS|79A&QEr{!dh zJ*DSSnWscv_Fm1JvD9Zz=G>GnAHFXR`!)UWw@!IvGLboDo{s3{%-!$K(sEKy*+$5^ z$#Wif(0?4tG?n#Fw!!YYr}Muoa*HnDXHl_lR2lQ29c@#tn?bfsj3JJcAp_}k33I@YPMZrL*laL-l ziZL&2L^qSM7ytunDC{YfiQQV4?@4Ckb{{sv19fyxsY|A@3GPkClhJ%4hZ9V?e;;Sq zxooH1m&xW6@w(1zzD}QKD4vYuRNeMmBAcu0%k^|cJM)?BK;7+`?4Dd-v@=n6M=~Bw zC+yq+_Se03U$QR&wF&IFQ;BFUQJ3klbMZa)(nx)zzOFOV+m}owm}b(6bl&dF#KF^_ zuLBbwlWG^l6ML_y?My|}-L-pRHJM4*HY|a7W9@=eYH#nNzHH{!L}$LXpD5i~ z#f#(dXe0h&4a*V@jfwcu#+^r{QrY zkY|yg<~#_>ebQu$^uk$Dn;OW zybKjw2@+a^SJ2271%iQaC|sm!AV#|Wgmkr|9oB;3cQwM&{~tz}x|W&@P4uKR_fY`f zM^hC>%|0NsO)wqDK*my4mO<;A(Hp=Jqy?E)*DIlcLJ1@fA$}6iUn713^fu7fKp#bZ zGalim{`e}MkMLN?r5T|lc#Xd&f1&x7im|hwpohnk7&ty`aJvgh^c|UQPFS3g;C6T*@rY1ZwoaWOk zM>7X!&~%Z_iyvkY&uoh_JsFzhSWZEG_QHw_%YCW-91O!lkjvOT(R4gT?gE-zlI%N( zD|CXtS#?^2m`H#8)A`}3)s-Jf-;}P@n@VvGeQ8T!dosNzkzG;S-rlNL zRduf}!xm7(Fn&`}P6XdJRs)LFnDoIvhlGXrq~IK+5Ega)>#Z*jzk1W-^Diq7ELeEs zD&kJ%_eB2lnfTL>zV*g4AKmbkNvhbCrnD-IV0lS+RWKAf6r2&-7@Qf}5G>vRvN2eT zs2P4EXoNPJm9W_h{nRYWK*ueTK}FKVF%D8$hkvkY(+4kn`p~zUhfY1R;<3p%Z4{ew zMzBhajT_;RGRWywY|2V+O%r(dGY!%ND+aTOtF5o^{ML`A+wy^b&Ky)L;>+77QH@SvPG6R)wsEPX&uQLe>q309y4Q1dSsmHiu5JWRO@tQ!Ks3 zBmI~AHv}hztXCD}HwS~z_nrNRpxk;#Z3tNpAU-u@{X&tg4c4DJS?^QUy@GR+SMnN~RP>iF}I~CBb0H-vy10+I z74HkITwipaljZS(N?KU#E=Y~%AVJ$aXr%=|no8vd`VzTF57vI({SV}_nT&hx@UGwe zxh#$?ovD5t6k^GI?!wk^PkHEIb0Ky6eXV$3wELZ9?i!4DQ_)0<)doSPLRx_+$Q&qX z2Q9oE`5)uUcjV=ZbW%#m*C^?Nq!YUsgg#FSMXvFpf>~MmX!6E~sH~#A6+UgKQG%}3 zqJlkm6&jOwBZJP*`74D^7P#3!luwrx_i~r{OWg zDbD*N{tU-oSML7qE%_B3DR;LTf>ZUJJ+~kZrCqpE;yrUPdQ*Ejzp!z{eOg0>eaaX(W#izDm5Cg5q}&j@Ixgp@1s28VQ3AL)xz>C1_)& z0xmN2J0y;^?!Xm%8%M_e-bl-ubDrUd`?{H9bKSH=VjP*S94e?%w1iPR)2HJNDNf8$ znl1sm7%|g3}Ju zP*tuUSSAw3&t1TUznlwDD39QiS#AR^lsuVK%O z7rzGIn_(9EOWJ2A_{STcx3QjbCBHk1uD!qwRigUz4+QeN#UE+>8yc4x;W3Rrtnpf{ z=*JpAt#M}_#CQV^--4@Ov#XzLT=Wb6mc~WD;J?(k=og&488TZ6M)X6*%hW#K{Pqd( z@!H42RK*@vAAgwf>5ANK-cZi?40V`kA!jnDXYy~w1r%Ny#>bAI*^JLtw|JQmpUe2z z@pBpDbJ&aH6JJwSL+yw(focq!^N z9)1zdt>T=V3wM#|NZ24#mv*~YFD?m!6DL;k!xkL7ToE@3j)LbI1@%@>yG?=WT58WUfN?4d zw1Hcwfps)+9UHij8d$!8o2j8mt=SaalUTE3#~SC?y==?swn%4RA2m_EvUjyNETutV zd+uftdbNWVZeQ84WVemOYch%(d&pnTQl9)&GM`T+JS z*^oP`=c)KkH;-;d@Og(x!Be8U)$bYS9)gg!FMK+X96i(kxUKJa_?6wdv!i8G+ZyG1 z{GH2WEP771X&7ubV)G z&fm0E6L~p*3YGO;WMqFVY0~eNpXh$D@XNX<=|zapkIrIW?$roAr5VWv)l=*{=Y8z6 zfPI8JJ|d6Jj1x4SH#+IcYgo}eU;6b<ha2+I0 zbQ2!ZDfZ=FlTf+WBzFDv|B9Ah4N5`oH3{A4z-)ugB!2lQFz@)0^NY}!mKP%Blr-Js z@XAkgk6rks9+JM@C-0uu_4(nnFls907x>SpH-#pL zxNpDxXOQESmwS7sw0yHG=|ueU|KyXGpE9xa=HTb|r=J5CVN!ni2{f$b9six9i{5_u z7l7#|DlieBmjB3?=}W4!6UhHoI}RFs1J*D9s|n<%zE3-< zdJ3W(^N-(d$or;P=6gxYH|cp~KG04)A}{NA2J*fpX8Fo1n5cuk0{fC`U;_EUD;@dU Jeaijv{{#Ma@_hgR literal 0 HcmV?d00001 diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake new file mode 100755 index 0000000..add56f6 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Darwin-25.5.0") +set(CMAKE_HOST_SYSTEM_NAME "Darwin") +set(CMAKE_HOST_SYSTEM_VERSION "25.5.0") +set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") + +include("/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake") + +set(CMAKE_SYSTEM "OHOS-1") +set(CMAKE_SYSTEM_NAME "OHOS") +set(CMAKE_SYSTEM_VERSION "1") +set(CMAKE_SYSTEM_PROCESSOR "aarch64") + +set(CMAKE_CROSSCOMPILING "TRUE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c new file mode 100644 index 0000000..0a0ec9b --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c @@ -0,0 +1,880 @@ +#ifdef __cplusplus +# error "A C++ compiler has been selected for C." +#endif + +#if defined(__18CXX) +# define ID_VOID_MAIN +#endif +#if defined(__CLASSIC_C__) +/* cv-qualifiers did not exist in K&R C */ +# define const +# define volatile +#endif + +#if !defined(__has_include) +/* If the compiler does not have __has_include, pretend the answer is + always no. */ +# define __has_include(x) 0 +#endif + + +/* Version number components: V=Version, R=Revision, P=Patch + Version date components: YYYY=Year, MM=Month, DD=Day */ + +#if defined(__INTEL_COMPILER) || defined(__ICC) +# define COMPILER_ID "Intel" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# if defined(__GNUC__) +# define SIMULATE_ID "GNU" +# endif + /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, + except that a few beta releases use the old format with V=2021. */ +# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) +# if defined(__INTEL_COMPILER_UPDATE) +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) +# else +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) +# endif +# else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) + /* The third version component from --version is an update index, + but no macro is provided for it. */ +# define COMPILER_VERSION_PATCH DEC(0) +# endif +# if defined(__INTEL_COMPILER_BUILD_DATE) + /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ +# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) +# endif +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) +# define COMPILER_ID "IntelLLVM" +#if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +#endif +#if defined(__GNUC__) +# define SIMULATE_ID "GNU" +#endif +/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and + * later. Look for 6 digit vs. 8 digit version number to decide encoding. + * VVVV is no smaller than the current year when a version is released. + */ +#if __INTEL_LLVM_COMPILER < 1000000L +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) +#else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) +#endif +#if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +#endif +#if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +#elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +#endif +#if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +#endif +#if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +#endif + +#elif defined(__PATHCC__) +# define COMPILER_ID "PathScale" +# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) +# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) +# if defined(__PATHCC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) +# endif + +#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) +# define COMPILER_ID "Embarcadero" +# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_C) +# define COMPILER_ID "SunPro" +# if __SUNPRO_C >= 0x5100 + /* __SUNPRO_C = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# endif + +#elif defined(__HP_cc) +# define COMPILER_ID "HP" + /* __HP_cc = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) + +#elif defined(__DECC) +# define COMPILER_ID "Compaq" + /* __DECC_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) + +#elif defined(__IBMC__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__open_xl__) && defined(__clang__) +# define COMPILER_ID "IBMClang" +# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) +# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) +# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) + + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 +# define COMPILER_ID "XL" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(__clang__) && defined(__cray__) +# define COMPILER_ID "CrayClang" +# define COMPILER_VERSION_MAJOR DEC(__cray_major__) +# define COMPILER_VERSION_MINOR DEC(__cray_minor__) +# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TASKING__) +# define COMPILER_ID "Tasking" + # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) + # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) +# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) + +#elif defined(__ORANGEC__) +# define COMPILER_ID "OrangeC" +# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) + +#elif defined(__TINYC__) +# define COMPILER_ID "TinyCC" + +#elif defined(__BCC__) +# define COMPILER_ID "Bruce" + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) +# define COMPILER_ID "LCC" +# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) +# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) +# if defined(__LCC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) +# endif +# if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define SIMULATE_ID "GNU" +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif +# endif + +#elif defined(__GNUC__) +# define COMPILER_ID "GNU" +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(_ADI_COMPILER) +# define COMPILER_ID "ADSP" +#if defined(__VERSIONNUM__) + /* __VERSIONNUM__ = 0xVVRRPPTT */ +# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) +# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) +# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) +# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + +#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) +# define COMPILER_ID "SDCC" +# if defined(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) +# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) +# else + /* SDCC = VRP */ +# define COMPILER_VERSION_MAJOR DEC(SDCC/100) +# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) +# define COMPILER_VERSION_PATCH DEC(SDCC % 10) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +# elif defined(_ADI_COMPILER) +# define PLATFORM_ID "ADSP" + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +# elif defined(__ADSPSHARC__) +# define ARCHITECTURE_ID "SHARC" + +# elif defined(__ADSPBLACKFIN__) +# define ARCHITECTURE_ID "Blackfin" + +#elif defined(__TASKING__) + +# if defined(__CTC__) || defined(__CPTC__) +# define ARCHITECTURE_ID "TriCore" + +# elif defined(__CMCS__) +# define ARCHITECTURE_ID "MCS" + +# elif defined(__CARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__CARC__) +# define ARCHITECTURE_ID "ARC" + +# elif defined(__C51__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__CPCP__) +# define ARCHITECTURE_ID "PCP" + +# else +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#if !defined(__STDC__) && !defined(__clang__) +# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) +# define C_VERSION "90" +# else +# define C_VERSION +# endif +#elif __STDC_VERSION__ > 201710L +# define C_VERSION "23" +#elif __STDC_VERSION__ >= 201710L +# define C_VERSION "17" +#elif __STDC_VERSION__ >= 201000L +# define C_VERSION "11" +#elif __STDC_VERSION__ >= 199901L +# define C_VERSION "99" +#else +# define C_VERSION "90" +#endif +const char* info_language_standard_default = + "INFO" ":" "standard_default[" C_VERSION "]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +#ifdef ID_VOID_MAIN +void main() {} +#else +# if defined(__CLASSIC_C__) +int main(argc, argv) int argc; char *argv[]; +# else +int main(int argc, char* argv[]) +# endif +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} +#endif diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o new file mode 100644 index 0000000000000000000000000000000000000000..e8717290bb4e80438abce4e1c20b9974b50881a7 GIT binary patch literal 3296 zcmb`JO=uHA6vrpFYD(2wzfdX~&_flvWZSff;vrH$p|+w3N+{_jyV2NWQ@7g^MNm-C zqkU)#-+{x@liaPDizWL36=DnFr-sHy7v16-Z zF~KD!UWlN@REVjyft{6RR%`?1@-6XpdADeM=o5{P-QwqRk7&e&xEGIQe}3(|+2|^l zGhceLjd-l9`^!J{;atsm#r^*^=Ld)j;=jMpk$-=3{Mf|7Vy!w;t~g$9v|_mzCxwJF z70WNxylQT&?9SB*T3+!|j_g_Aa&61A^R`p6<|=;9OjD0j_Z@e(TytlC_f3pX1{(=h zi{Jdbs9DC$|ZD$JUR53Z6DWo#{*vUpRV%N@y*!5kp zzID3=vhBnd@qJ`Nf|{Y%cOaeO`*NrQKnMKk<+MDMVUD^Ev09glLsB2+Y01}w^n;Sa z3De4eeV>u2_uL#z~iayDMbur(9gP?4Nbp*Y^`M~{9d9o>&dOll- zQp8cO@-aoP?%rudukwnb-$XXPX2{+O^Q-(eag1O66z&qocvSva(ev67Jt2;Il@k`2 zdONwHyqRxR7@ExYx8WmgIPc3&>R%0hSE*XVhioMbNP$(y1;cYHmSJ1IWt81gEsrT= z&bsL2!#SWI-(*0eJ^N*Vv8Z4`wu>wCUcfcMoZzW3V30jx4urLh{pY8a~&Rzp|~Vr9bg z%@x2LCN%ffjikE0e`|Nb=Mdbl`hVTV|I-NncpK^d54Z8ZxI+Jj+W23H@Q=5f?jJwb zsP(^Dq5q9-{4YiL$NNzCk3YN7m7YwN$o~cCQww1^2Kg&^n5jBbKakX^Yd_O?U7jJ( quTBa!$Iti^7*Xf%Tg3YDKIQRz{?>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_CC) +# define COMPILER_ID "SunPro" +# if __SUNPRO_CC >= 0x5100 + /* __SUNPRO_CC = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# endif + +#elif defined(__HP_aCC) +# define COMPILER_ID "HP" + /* __HP_aCC = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_aCC/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_aCC/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_aCC % 100) + +#elif defined(__DECCXX) +# define COMPILER_ID "Compaq" + /* __DECCXX_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECCXX_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECCXX_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECCXX_VER % 10000) + +#elif defined(__IBMCPP__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__open_xl__) && defined(__clang__) +# define COMPILER_ID "IBMClang" +# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) +# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) +# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) + + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ >= 800 +# define COMPILER_ID "XL" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(__clang__) && defined(__cray__) +# define COMPILER_ID "CrayClang" +# define COMPILER_VERSION_MAJOR DEC(__cray_major__) +# define COMPILER_VERSION_MINOR DEC(__cray_minor__) +# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TASKING__) +# define COMPILER_ID "Tasking" + # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) + # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) +# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) + +#elif defined(__ORANGEC__) +# define COMPILER_ID "OrangeC" +# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) +# define COMPILER_ID "LCC" +# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) +# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) +# if defined(__LCC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) +# endif +# if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define SIMULATE_ID "GNU" +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif +# endif + +#elif defined(__GNUC__) || defined(__GNUG__) +# define COMPILER_ID "GNU" +# if defined(__GNUC__) +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# else +# define COMPILER_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(_ADI_COMPILER) +# define COMPILER_ID "ADSP" +#if defined(__VERSIONNUM__) + /* __VERSIONNUM__ = 0xVVRRPPTT */ +# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) +# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) +# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) +# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +# elif defined(_ADI_COMPILER) +# define PLATFORM_ID "ADSP" + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +# elif defined(__ADSPSHARC__) +# define ARCHITECTURE_ID "SHARC" + +# elif defined(__ADSPBLACKFIN__) +# define ARCHITECTURE_ID "Blackfin" + +#elif defined(__TASKING__) + +# if defined(__CTC__) || defined(__CPTC__) +# define ARCHITECTURE_ID "TriCore" + +# elif defined(__CMCS__) +# define ARCHITECTURE_ID "MCS" + +# elif defined(__CARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__CARC__) +# define ARCHITECTURE_ID "ARC" + +# elif defined(__C51__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__CPCP__) +# define ARCHITECTURE_ID "PCP" + +# else +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#if defined(__INTEL_COMPILER) && defined(_MSVC_LANG) && _MSVC_LANG < 201403L +# if defined(__INTEL_CXX11_MODE__) +# if defined(__cpp_aggregate_nsdmi) +# define CXX_STD 201402L +# else +# define CXX_STD 201103L +# endif +# else +# define CXX_STD 199711L +# endif +#elif defined(_MSC_VER) && defined(_MSVC_LANG) +# define CXX_STD _MSVC_LANG +#else +# define CXX_STD __cplusplus +#endif + +const char* info_language_standard_default = "INFO" ":" "standard_default[" +#if CXX_STD > 202002L + "23" +#elif CXX_STD > 201703L + "20" +#elif CXX_STD >= 201703L + "17" +#elif CXX_STD >= 201402L + "14" +#elif CXX_STD >= 201103L + "11" +#else + "98" +#endif +"]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +int main(int argc, char* argv[]) +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o new file mode 100644 index 0000000000000000000000000000000000000000..4577e9e20c9c2ea3d7535bfe83a259b6bae16be7 GIT binary patch literal 3320 zcmb`J!EYN?5Qk^$7E)T;w513srL|BcQlKku;y5nlU=-R?6cPkNO0kNy_O2Tfdu=wG z$e>ECL=QQ(m#Qa}1AhP)1QwTGkScMZJyhaWZs`RHiAyhpnYT09*SBkA1e@f&+3)@4 z&Ahj7XLIe`{P|s$1#(&NJtTKR0s7A-c14*LcoCJ`U%~zD6EJu*0fXQ6!ISNSFc<^4 zIc8O!Jf65d*xPEAw+>bYW7gh%TmNB<=8ETqTmRRbk1#LH|JL1H?cv3R^NVjbx}9FD z?T6L5wi{fz3<~zzZq)3Co$7omSnD%%!^U!z?bXN)JU8@ep5JuW+ELXh@`&G${9v`! z4OVx?78foj8##jYR9>}pKcwlZ1$8+yc5D<1Vqj!ge0TAq{G{ivpUO9A9eMW3lin`o zC)@4y&gov*{mgGfdB>Tlm!`_z^t9)>Mf|Nhv%XXGz0w)aE7xa>jlxX1UM!!AlO5-r ztuH?W>*{f9V*d#s_7d}Z)W35e&$p@W8j{`c$98G}K@NY+>ykQ*Y5p||EfqQTn9>|` zut&R#&2ws=G_NTBOiF)NaXfLxRZ{#pg912;?@#eJ6hDyS1aeybB6wBlWtW8lyrTG_ z1hDn8;zv^aM63V&8y?f}I4Q}JZI!Y?SEtXKGi;>mi2zkr;s zBbcwg3OP5&4PJ=lN`IJb>WXZWUgz`7X?}e-KG5_!Z)^Hw6#zZvM{ypV-_Z237D5}$ zDIcA0GN*iwvQ6DLY?EH+BB4^8KBaipnVUYsUy}*A7hW2|WnYf*_#T{pm#(!VtX^Pa zN$mIm8lm5IZO@HdyA?FMH7YD??iIh5u5rV)W~EFir2xa+`|gTAx3o0J75SoPH+ntP zcU20tPv)H|l?T^0o{4g{J8mmT%`8Pxfs&+xm9@Tl;x@Ug@fc zb(ml6PjOtoj9c}MCze!#SY6SbG<&yZlldL~{geMaLvQ+ji{r#9hHjaKc}|$lJFQdd zh}4VsaGWH1-KVL>*iYVXVstUy=~tMUd)Q~8#8b}LnD>5NzcnQOFB$Qv zj+^o6xn!;X&WQ2Hhs58?h)-WbGyZLk|5VLIw#o6$bIlkP@kRfR6Ekbc0~TIlopj search starts here: + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include + End of search list. + [2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -o cmTC_f3d66 && : + OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) + Target: aarch64-unknown-linux-ohos + Thread model: posix + InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin + "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_f3d66 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o + + exitCode: 0 + - + kind: "message-v1" + backtrace: + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:127 (message)" + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed C implicit include dir info: rv=done + found start of include info + found start of implicit include info + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + end of search list found + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + implicit include dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + + + - + kind: "message-v1" + backtrace: + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:159 (message)" + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed C implicit link information: + link line regex: [^( *|.*[/\\])(ld\\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] + ignore line: [Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r'] + ignore line: [] + ignore line: [Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_f3d66] + ignore line: [[1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa --noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -MF CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o.d -o CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCCompilerABI.c] + ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] + ignore line: [Target: aarch64-unknown-linux-ohos] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] + ignore line: [clang: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]] + ignore line: [ (in-process)] + ignore line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCCompilerABI.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o.d -MT CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -x c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCCompilerABI.c] + ignore line: [clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0] + ignore line: [ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + ignore line: [End of search list.] + ignore line: [[2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -o cmTC_f3d66 && :] + ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] + ignore line: [Target: aarch64-unknown-linux-ohos] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] + link line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_f3d66 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld] ==> ignore + arg [--sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot] ==> ignore + arg [-pie] ==> ignore + arg [-EL] ==> ignore + arg [--fix-cortex-a53-843419] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [-zmax-page-size=4096] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--hash-style=both] ==> ignore + arg [--enable-new-dtags] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [aarch64linux] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/lib/ld-musl-aarch64.so.1] ==> ignore + arg [-o] ==> ignore + arg [cmTC_f3d66] ==> ignore + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] + arg [--build-id=sha1] ==> ignore + arg [--warn-shared-textrel] ==> ignore + arg [--fatal-warnings] ==> ignore + arg [-lunwind] ==> lib [unwind] + arg [--no-undefined] ==> ignore + arg [-znoexecstack] ==> ignore + arg [--gc-sections] ==> ignore + arg [CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o] ==> ignore + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-lc] ==> lib [c] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] + implicit libs: [unwind;-l:libunwind.a;c;-l:libunwind.a] + implicit objs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + implicit dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] + implicit fwks: [] + + + - + kind: "try_compile-v1" + backtrace: + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:57 (try_compile)" + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + checks: + - "Detecting CXX compiler ABI info" + directories: + source: "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK" + binary: "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK" + cmakeVariables: + CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS: "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" + CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm" + CMAKE_CXX_COMPILER_TARGET: "aarch64-linux-ohos" + CMAKE_CXX_FLAGS: "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__" + CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm" + CMAKE_C_COMPILER_TARGET: "aarch64-linux-ohos" + CMAKE_EXE_LINKER_FLAGS: "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections " + CMAKE_POSITION_INDEPENDENT_CODE: "TRUE" + CMAKE_SYSROOT: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" + HMOS_SDK_NATIVE: "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native" + OHOS_ARCH: "arm64-v8a" + OHOS_SDK_NATIVE: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native" + buildResult: + variable: "CMAKE_CXX_ABI_COMPILED" + cached: true + stdout: | + Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK' + + Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_5b7f7 + [1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -MF CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp + OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) + Target: aarch64-unknown-linux-ohos + Thread model: posix + InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin + clang++: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument] + (in-process) + "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCXXCompilerABI.cpp -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdeprecated-macro -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -x c++ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp + clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0 + ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include" + #include "..." search starts here: + #include <...> search starts here: + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos + /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include + End of search list. + [2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_5b7f7 && : + OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) + Target: aarch64-unknown-linux-ohos + Thread model: posix + InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin + "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_5b7f7 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -lc++ -lc++abi -lunwind -lm /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o + + exitCode: 0 + - + kind: "message-v1" + backtrace: + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:127 (message)" + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed CXX implicit include dir info: rv=done + found start of include info + found start of implicit include info + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + end of search list found + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1] + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + implicit include dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + + + - + kind: "message-v1" + backtrace: + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:159 (message)" + - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed CXX implicit link information: + link line regex: [^( *|.*[/\\])(ld\\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] + ignore line: [Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK'] + ignore line: [] + ignore line: [Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_5b7f7] + ignore line: [[1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa --noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -MF CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp] + ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] + ignore line: [Target: aarch64-unknown-linux-ohos] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] + ignore line: [clang++: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]] + ignore line: [ (in-process)] + ignore line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCXXCompilerABI.cpp -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdeprecated-macro -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -x c++ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp] + ignore line: [clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0] + ignore line: [ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] + ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] + ignore line: [End of search list.] + ignore line: [[2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_5b7f7 && :] + ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] + ignore line: [Target: aarch64-unknown-linux-ohos] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] + link line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_5b7f7 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -lc++ -lc++abi -lunwind -lm /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld] ==> ignore + arg [--sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot] ==> ignore + arg [-pie] ==> ignore + arg [-EL] ==> ignore + arg [--fix-cortex-a53-843419] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [-zmax-page-size=4096] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--hash-style=both] ==> ignore + arg [--enable-new-dtags] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [aarch64linux] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/lib/ld-musl-aarch64.so.1] ==> ignore + arg [-o] ==> ignore + arg [cmTC_5b7f7] ==> ignore + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] + arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] + arg [--build-id=sha1] ==> ignore + arg [--warn-shared-textrel] ==> ignore + arg [--fatal-warnings] ==> ignore + arg [-lunwind] ==> lib [unwind] + arg [--no-undefined] ==> ignore + arg [-znoexecstack] ==> ignore + arg [--gc-sections] ==> ignore + arg [CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o] ==> ignore + arg [-lc++] ==> lib [c++] + arg [-lc++abi] ==> lib [c++abi] + arg [-lunwind] ==> lib [unwind] + arg [-lm] ==> lib [m] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-lc] ==> lib [c] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] + arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos] + collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] + implicit libs: [unwind;c++;c++abi;unwind;m;-l:libunwind.a;c;-l:libunwind.a] + implicit objs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] + implicit dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] + implicit fwks: [] + + +... diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt new file mode 100644 index 0000000..c8c4bcd --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt @@ -0,0 +1,4 @@ +/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_launcher.dir +/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir +/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/edit_cache.dir +/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/rebuild_cache.dir diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o new file mode 100644 index 0000000000000000000000000000000000000000..5408e91fc1327a324a4d3455a4bcee6f2235ff38 GIT binary patch literal 4000 zcmbVOO>9(E6h5zy(xO030YMNu{DibbUON4uC2=vNfHhQT3M6VYkJtBJ+mZS6&1+M@ zL=46an@)6RRERFxY9kAcu*2ZSm9lYROq^CpFeV1mO}ilVoIB@qZr{wr;BDufbHDTR zopbMfKkpnLI8kR9Aj5!fp*#~5;QSM1J)zVDY%6(A;k1Citt)qpdyg*H&n$Mr@}oYZ z&+`X z>;wD5^^^WEchx;1{BcJn>-n}Drr<&{l{$r!$4&5G{-^s-pNX}4{zO~MO*xs-*n}VC zli5rx-fgv8U9r|wY9f6o7i8b`-7pr9cMl)#==C}~J-AtAEkLYLs-W@#pML}soy*4YU)JvU) ztDX?Mrt(+%QLh@_$mFD*c@Duf$9K zvTNnALFHeGpH%qfvLkCJ6fUQuY(A>+<}$$AHz=F6{SqeHhsrDff7ig{HSkL{aIXe# zW1ZD_$7}e%Q3DTa{T?i{N;RsIe3AZLE`!8ha3bTEn-pPWhWOT<%NC zuj8%k(;LZijPYHFZ`bn3{SoCIsF5es@<l(gO!*d$GOT*vO zaC-L0m|}(c)&0NJ@ZI8!@+Rxib-2ZNbUlA%obp#&2YU7EPY>}Sd=tm&LU6hBWjzU( zGZXcfvq8A@UXT9xeq0^xtP!r)GpONf@>_wQa;#(~8QM+|I2XYR{mBq4H=9oT8B~;= zFzY3Bf{c?(+MYj>%=k9GeR6&fCjEQ~PG`OGly3+AXfhxA0gUHUzMm`EM*YxshwWr$ zB&*(;VB7i7aWB~J*adsUNv6QcWJBM2@zl9P%D~M2m^~6WXf!cWROTIU=ccLGn3#YU|lYgq%f?PfFiQooon?}EJTEf=hO7=+F+%8O}|`#r0d zEx*8uzjLu(P8Cx`FS6g6yBPIsh+mtS74)L6$g95tAKV-QxeH z#PK{-zC8hM@nQHJ?fQsJ@flzGc3Q7Hp`M7$Wh&1@nSX@}<*XGi?+x*f#?uERVF__y z`G603KhNnNp+4n|%YAqNv1w85k2smQugKAE8pM{~SB^vm^CSXF=kn|07sXl$%0!@P_=vMyi`~MC1Pj Dvw6)`ub(Rznk|j1c zJA2NXbL^9wc~sgfYFHWx6)K7GVBcC90C?1rJznJvEa8Kds}=+D0ApD*&IXFvE^yy;1m zUZ&LXDu+s!D|KbTD6ejCq@JIx+;O(bs7-r};`FYP+NNEGYPwYE81ppOrgs^|O}mrRz+D^)sGC;S%xpD|qH;L6DM0+s&SGBwd=_?q0yRQ;5(dVIGd z<;T8xLydPSHDMa7-!^bO^dVlpP;_M)+G}b@o}U7p7mPI)j_scmr2ORv{<0C}%o~}B zi@@>YQq?$H;+rpbto(4&r8;LTeP{pWKkvQc(w04E+>Vm_svOJjb32w*yFbs=W8d#Z+OvICj#bv!S*NPMWUL+E^-cA7bUxGR`KIs0 z_B?gRr4nCy2JEVOL!}+5l;5m!A9F%X_9&G`8yAe#)xXJP?t<8)RVpiU2Cr{}SUFSv zUt+t!H~$&bIFFv9J&oras(uFA%!7W1a4f_YIq|yUsg(cQ_1QQ=?2kjboV{-N=COa; zP}NW$hd;{(ZcoF!V86%M*W}KFxq$hah8#gGJ-K75 z=+&Lm&!l!#LM|}pG}wr|Dlh9R=i72CU&_X(3GZ43_GmV$&odKFc&o{!SkD@z)t>^#jOd^Ax{rlO=GlC82XbNmq<7`w+;jhkvwZVX zf9Wm#*q602I}ggG>H)K2Hw}8R|MyN4d?>HxcuzqtF3Qjx%vyeySFg(RrCIJ20{LwY_c6)aRkV}?)7so}PTZZH+Y#oq!>1v?Sr+CH{jhwhNU1dAp5~#Z zb4u6<>wx=yT%aDm*P%MEmH5(EjkVPykV8{OsWopS&f;_;UtM`3lj$r0-Y%!_?4+7& zs36<$dN;K(7C+gU&7E9xJOlk#VeS%?qtnp7kj=TQy)e&Lov!%{`L6lQa+j5h$b)j2 zCl!+>ud$I5GO=Xnq58WWXF1sM80gVb3mdoL=$qA3mva_~tL(OAsYGaTc%R<7dy z1)TUd3bx|-Nn@_zf50YQ3sZOFKABz5D1V}0u3@?o^LJ(S-RhM8I<#>d?yKc4b>**@ zsm?j0e0a`<{>G8tj)Gh%axv#EISlsO>3RcvK)Z|n3`efHnJ&Pw zE35yUZ775Khx6uMLtTsO8rm>7;~aC4SzLF#r(ZNw+N@0Zr$KfG_K`gGZGXm4*?Y?u z`kYAg$f5p3@+QS7V|Mos2IGe< zt_M0=S_7>QJgkP|vGBoAvOhMc+BzQWRCSvhytVLOo!Zyo?^Lbrt<9aS{`&(hV0obB ztA6!htN-Cvf8fE^W`Ap^y4k(Rp-5;L`8BsK-0TVVMFb0!QrM7NuXiyNkU`u~oWuvTyV)3L( zL_+aM63pER7Q^%|g-vQqM4~-rxIeCkI}|$@4O_DjjfHSxSkN5q51KU)!%z)EH3Pw+ z#8S-mJoG?IduyN>V~wG!LCb0q$yhwt8>xvz>uS1TZle)2$VwVCD^U}S4{WR7G~5`J zR^|}Vy6wm62F4Y+AKb7=o+uRu5vLzf2*2BNZm18{Zf)$Y zZ>)m74BiE^#qkZlavc4#W6g?kwGp5)!c+OsXesR4zhYh23FnCG+5GRjf3WqPc@+t?u3vh>8z9{XY*85O9Ro)_Q`2kn=fV@)` zEaH~^bY%#67Wl5EcZr8_tPR1#c*>Gr;%7W74^Jdo8Mfqa#k2BDe(|u!E%{sVsD+2E))m;Co^@KP9%Oa8BHA)NjC54wJxYPOT{Nx>uSgw4<@ z%tK?UP`#`?|Ombn&RRS{BjrCNccJ+mUzA3!52?1I|L8TBm3YZob6=1MexYo zxJ~2X{AkyB81J;<=@U5LThZ1bjsIP`PEyTwGJZ&tpQ7udnmpqv!ZH3|;^092oWNHL z{3U^}5%?(KJn#8*{W8^TC*!XQ9vRQqHJ)8`J*n|9{*K_m=L|0FAKSB^j}B}<<8Hz+Zqoi@f#Y7vrIc{yzd-uRG=9cCg6GRj0`(4o%Xw)RIDErdERPCY zuJ<9r*&g11l7zFrjHd(-uL)FgT;qA1{2I}C7$3Fanb3Gfi06#P!}vKHp5JLazaXCX zG#N}9>(9Y;dw{n zxs7<<)p!_x--c&atU!wwFFYJ6-;2Q*f zM&S1de3EeXYcJXJ7UAp{vVczB=h z(s&r}v*C$qJU_^WQ4MK4j3;e)jtX4PODXX+fS-RsZLC_T-KgPD6OQxZp*B_{)YkWb zlZ5O0zy#sQV|}2a>oZjA_PkEGZqG%*Q%!BGOls>qwS?G=TP zm$7i(GQK|xTkm)GQX4CtXXM`~aE_Z@91!0ir;BZ z;&`%OjKdkbRaQeS^IX24N;~np4Hn%_es5*HEfi0ajjV0yHVhqvZ}1#TEg`> z;P(V9dK~r%9%=u6fy+2|5zh8kko{r8*?z|R1dp^o`&~{c-OeGB*X?|XaE!n7_hrE^ zaem*&_Au&^Dw?3c;tFfl#hD; zy9F-irI>KG{}kC@N;vCfyiD*&`%Qt%c=C4)eZD*-ug}+`f=A|6Sm4tBDB)~>57|FN zINQ%Se>cGRNc;Kw0OE2#TuTAZ1Dq%=g!eJTB~Mhy%jX0IXBwfQ!)oYYGBP*}XH-_z zo2{&a59Lip4keX06dM?T6CxFWlf9(s0g;3XhC-14!D3b;)Mr(3tOEh- zbSz*UT;VC7Hy()wEm|)gWvU)L<-y@mtPUqaaNY)^5=l6X3xxU(1n^u&MH7+80p*1Q z$%ywW4?MKV>Q=*v!J&BnV6sP9r#q4xPE8-d6Cc$RipCNV>WcIQdf;3%a+8AIgTZ(h zA_l3F^+3Mnr*lMHr4eRu%t z^_4ua5cy0ZE>PH|CDD`d{ew>o^ zMd+7ByHu6XiCDfAg0@e)=1UhhsQw1jUo<|Y5$od*!g$1d@F&ZD`e@?5J&WS^cl6Uu zLj3NF|1(D}{gb43mgKq8^`D21T>87r?1=YhAnMP9KV82G%5&)-rNx<&ixEDb(qIl?N={H((psiTAZldv;n3|=y>-Or`_$-KAdM;n4 z@n1t_qOe}B{|WlIEyPr7p}bmZ>#|p9ycTBFM&pk!4YgVvKZY+uBUk*}X#KR2AjThE a;_;k+0We6{jYHle{V56<+I>fk@&5 +#include + +static napi_value KillNode(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + + int32_t pid = -1; + if (argc >= 1) { + napi_get_value_int32(env, args[0], &pid); + } + int32_t result = -1; + if (pid > 1) { + /* TERM first so express can close listening sockets cleanly; KILL if it + * survives. */ + result = kill((pid_t)pid, SIGTERM); + if (result == 0) { + usleep(200 * 1000); /* 200ms grace */ + kill((pid_t)pid, SIGKILL); + } + } + napi_value napiResult = NULL; + napi_create_int32(env, result, &napiResult); + return napiResult; +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor desc[] = { + {"killNode", NULL, KillNode, NULL, NULL, NULL, napi_default, NULL}}; + napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); + return exports; +} +EXTERN_C_END + +static napi_module demoModule = { + 1, 0, NULL, Init, "node_ctl", NULL, {0}}; + +__attribute__((constructor)) void RegisterModule(void) { + napi_module_register(&demoModule); +} diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c new file mode 100644 index 0000000..0f745bd --- /dev/null +++ b/entry/src/main/cpp/node_launcher.c @@ -0,0 +1,306 @@ +/** + * node_launcher.c — native child-process entry that becomes the Node.js + * backend on HarmonyOS. + * + * ArkTS cannot exec() a binary. It starts this library as a *native child + * process* via childProcessManager.startNativeChildProcess( + * 'libnode_launcher.so:Main', { entryParams }) — the system forks a child + * (through appspawn), loads this .so into it and calls Main() below. + * + * Main() then: + * 1. parses the entryParams string ("key=value" lines — plain text, no + * JSON parser needed); + * 2. locates the node binary (installed as libnode.so in the app's native + * lib dir, discovered via /proc/self/maps where this very library was + * loaded from, plus fallback candidates); + * 3. redirects stdout/stderr to a boot log for on-device debugging; + * 4. execv()s node with the electerm entry script. + * + * If execv fails (e.g. the lib dir turns out to be noexec) a memfd fallback + * is attempted: the binary is copied into an anonymous executable memory + * file and execveat()d — bypassing mount noexec flags entirely. + * + * Every step is logged to /node-boot.log (plus the errno of any + * failure), so `hdc file recv` of that one file answers "why is the engine + * not starting" on a real device. + */ + +#include /* native_child_process.h uses `bool` */ + +#include "AbilityKit/native_child_process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_BUF_SIZE 4096 +#define MAX_ENV_VARS 32 +#define MAX_LINE 1024 + +typedef struct { + char dataDir[MAX_LINE]; /* writable app data dir (el2 filesDir) */ + char script[MAX_LINE * 2]; /* path to resfile/electerm/index.js */ + char port[16]; + char secret[MAX_LINE]; /* SERVER_SECRET */ +} LauncherConfig; + +static int g_logFd = -1; + +static void logWrite(const char *fmt, ...) { + char buf[LOG_BUF_SIZE]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf) - 1, fmt, ap); + va_end(ap); + if (n < 0) return; + buf[n] = '\n'; + if (g_logFd >= 0) { + ssize_t ignored = write(g_logFd, buf, (size_t)n + 1); + (void)ignored; + } + /* also surface in hilog (stderr was dup2'd to the log file, so keep a + * copy on fd 1 before redirection happens via this early path) */ +} + +/* Parse "key=value\n" lines into the config struct. Unknown keys are + * also exported as environment variables for the node process. */ +static void parseEntryParams(const char *params, LauncherConfig *cfg, + char extraEnv[MAX_ENV_VARS][MAX_LINE], + int *extraEnvCount) { + /* defaults */ + snprintf(cfg->port, sizeof(cfg->port), "5577"); + cfg->dataDir[0] = '\0'; + cfg->script[0] = '\0'; + cfg->secret[0] = '\0'; + + char line[MAX_LINE]; + const char *p = params; + while (p && *p) { + const char *eol = strchr(p, '\n'); + size_t len = eol ? (size_t)(eol - p) : strlen(p); + if (len >= sizeof(line)) len = sizeof(line) - 1; + memcpy(line, p, len); + line[len] = '\0'; + p = eol ? eol + 1 : NULL; + + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + const char *key = line; + const char *value = eq + 1; + + if (strcmp(key, "dataDir") == 0) { + snprintf(cfg->dataDir, sizeof(cfg->dataDir), "%s", value); + } else if (strcmp(key, "script") == 0) { + snprintf(cfg->script, sizeof(cfg->script), "%s", value); + } else if (strcmp(key, "port") == 0) { + snprintf(cfg->port, sizeof(cfg->port), "%s", value); + } else if (strcmp(key, "secret") == 0) { + snprintf(cfg->secret, sizeof(cfg->secret), "%s", value); + } else if (*extraEnvCount < MAX_ENV_VARS) { + snprintf(extraEnv[(*extraEnvCount)++], MAX_LINE, "%s=%s", key, value); + } + } +} + +/* Find the directory this library was loaded from by scanning + * /proc/self/maps for "libnode_launcher.so" — the sibling libnode.so lives + * in the same (executable) native lib dir. */ +static int findSelfDir(char *out, size_t outSize) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return -1; + char line[MAX_LINE]; + while (fgets(line, sizeof(line), f)) { + char *hit = strstr(line, "libnode_launcher.so"); + if (hit) { + /* trim trailing newline */ + char *nl = strchr(hit, '\n'); + if (nl) *nl = '\0'; + char *slash = strrchr(hit, '/'); + if (slash) { + *slash = '\0'; + snprintf(out, outSize, "%s", hit); + fclose(f); + return 0; + } + } + } + fclose(f); + return -1; +} + +static int fileExists(const char *path) { + return access(path, F_OK) == 0; +} + +/* Build the candidate node binary paths. Returns the number of candidates + * actually appended. */ +static int buildNodeCandidates(char (*candidates)[MAX_LINE * 2], + int maxCandidates) { + int n = 0; + char selfDir[MAX_LINE]; + char bundleDir[MAX_LINE * 2]; + + if (findSelfDir(selfDir, sizeof(selfDir)) == 0) { + snprintf(candidates[n++], MAX_LINE * 2, "%s/libnode.so", selfDir); + logWrite("[launcher] self dir: %s", selfDir); + } else { + logWrite("[launcher] could not locate self dir via /proc/self/maps"); + } + + /* Fallbacks based on the standard install layout */ + const char *bundleCodeDir = getenv("ELECTERM_BUNDLE_CODE_DIR"); + if (bundleCodeDir && bundleCodeDir[0]) { + snprintf(bundleDir, sizeof(bundleDir), "%s", bundleCodeDir); + } else { + snprintf(bundleDir, sizeof(bundleDir), "/data/storage/el1/bundle"); + } + if (n < maxCandidates) + snprintf(candidates[n++], MAX_LINE * 2, "%s/entry/libs/arm64-v8a/libnode.so", bundleDir); + if (n < maxCandidates) + snprintf(candidates[n++], MAX_LINE * 2, "%s/entry/libs/arm64/libnode.so", bundleDir); + if (n < maxCandidates) + snprintf(candidates[n++], MAX_LINE * 2, "%s/libs/arm64-v8a/libnode.so", bundleDir); + return n; +} + +/* execveat on a memfd copy of the binary — the noexec-bypass fallback. */ +static int execFromMemfd(const char *binaryPath, char *const argv[], + char *const envp[]) { + int src = open(binaryPath, O_RDONLY); + if (src < 0) { + logWrite("[launcher] memfd: open(%s) failed: %s", binaryPath, + strerror(errno)); + return -1; + } + int mfd = (int)syscall(__NR_memfd_create, "node", 0); + if (mfd < 0) { + logWrite("[launcher] memfd_create failed: %s", strerror(errno)); + close(src); + return -1; + } + char buf[65536]; + ssize_t r; + while ((r = read(src, buf, sizeof(buf))) > 0) { + ssize_t off = 0; + while (off < r) { + ssize_t w = write(mfd, buf + off, (size_t)(r - off)); + if (w < 0) { + logWrite("[launcher] memfd write failed: %s", strerror(errno)); + close(src); + close(mfd); + return -1; + } + off += w; + } + } + close(src); + if (r < 0) { + logWrite("[launcher] read failed: %s", strerror(errno)); + close(mfd); + return -1; + } + fchmod(mfd, 0755); + lseek(mfd, 0, SEEK_SET); + logWrite("[launcher] execveat(memfd) ..."); + char *const empty[] = {NULL}; + /* OHOS musl does not export the execveat() wrapper — call the syscall + * directly (__NR_execveat, AT_EMPTY_PATH). */ + (void)syscall(__NR_execveat, mfd, "", argv, envp ? envp : empty, AT_EMPTY_PATH); + logWrite("[launcher] execveat failed: %s", strerror(errno)); + close(mfd); + return -1; +} + +/* The native child-process entry point. + * Signature mandated by OH_Ability_StartNativeChildProcess / + * childProcessManager.startNativeChildProcess. */ +__attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { + char extraEnv[MAX_ENV_VARS][MAX_LINE]; + int extraEnvCount = 0; + LauncherConfig cfg; + + const char *params = args.entryParams ? args.entryParams : ""; + parseEntryParams(params, &cfg, extraEnv, &extraEnvCount); + + /* 1. Open the boot log inside the writable data dir */ + if (cfg.dataDir[0]) { + char logPath[MAX_LINE * 2]; + snprintf(logPath, sizeof(logPath), "%s/node-boot.log", cfg.dataDir); + g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + } + logWrite("[launcher] Main() entered, pid=%d", (int)getpid()); + logWrite("[launcher] entryParams: %s", params); + + if (!cfg.script[0] || !fileExists(cfg.script)) { + logWrite("[launcher] FATAL: script missing: %s", cfg.script); + _exit(40); + } + + /* 2. Locate node */ + char candidates[6][MAX_LINE * 2]; + int nCand = buildNodeCandidates(candidates, 6); + const char *nodePath = NULL; + for (int i = 0; i < nCand; i++) { + if (fileExists(candidates[i])) { + nodePath = candidates[i]; + break; + } + logWrite("[launcher] candidate not found: %s", candidates[i]); + } + if (!nodePath) { + logWrite("[launcher] FATAL: no libnode.so candidate exists"); + _exit(41); + } + logWrite("[launcher] node binary: %s", nodePath); + + /* 3. Environment for the node process */ + setenv("NODE_ENV", "production", 1); + setenv("HOST", "127.0.0.1", 1); + setenv("PORT", cfg.port, 1); + setenv("ELECTERM_DATA_DIR", cfg.dataDir, 1); + if (cfg.secret[0]) { + setenv("SERVER_SECRET", cfg.secret, 1); + } + for (int i = 0; i < extraEnvCount; i++) { + putenv(extraEnv[i]); + } + + /* 4. Redirect stdout/stderr into the boot log so node console output and + * crash messages are captured on device. */ + if (g_logFd >= 0) { + dup2(g_logFd, 1); + dup2(g_logFd, 2); + } + + /* 5. Ensure the executable bit is set (it should already be, but a + * remounted/restored file could lose it) and exec. */ + chmod(nodePath, 0755); + + char nodeArg0[MAX_LINE * 2]; + snprintf(nodeArg0, sizeof(nodeArg0), "%s", nodePath); + char *const argv[] = {nodeArg0, cfg.script, NULL}; + + logWrite("[launcher] execv: %s %s", nodeArg0, cfg.script); + execv(nodeArg0, argv); + int execErr = errno; + logWrite("[launcher] execv failed: %s — trying memfd fallback", + strerror(execErr)); + + /* 6. noexec fallback */ + if (execFromMemfd(nodeArg0, argv, NULL) == 0) { + _exit(0); /* unreachable */ + } + + logWrite("[launcher] FATAL: all exec strategies failed (execv errno=%d)", + execErr); + _exit(42); +} diff --git a/entry/src/main/cpp/types/libnode_ctl/index.d.ts b/entry/src/main/cpp/types/libnode_ctl/index.d.ts new file mode 100644 index 0000000..13aee2d --- /dev/null +++ b/entry/src/main/cpp/types/libnode_ctl/index.d.ts @@ -0,0 +1 @@ +export const killNode: (pid: number) => number; diff --git a/entry/src/main/cpp/types/libnode_ctl/oh-package.json5 b/entry/src/main/cpp/types/libnode_ctl/oh-package.json5 new file mode 100644 index 0000000..71d61b5 --- /dev/null +++ b/entry/src/main/cpp/types/libnode_ctl/oh-package.json5 @@ -0,0 +1,6 @@ +{ + "name": "libnode_ctl", + "types": "./index.d.ts", + "version": "1.0.0", + "description": "Node.js child process control (kill by pid)" +} diff --git a/entry/src/main/ets/AbilityStage.ets b/entry/src/main/ets/AbilityStage.ets index 0173706..6aed7e9 100644 --- a/entry/src/main/ets/AbilityStage.ets +++ b/entry/src/main/ets/AbilityStage.ets @@ -1,45 +1,15 @@ /** * AbilityStage — app-level initialization. * - * Extends WebAbilityStage from the web_engine module, which handles - * initializing the Electron 鸿蒙 native context (libadapter.so / libelectron.so) - * before any ability is created. - * - * Writes the sandbox filesDir path to a marker file BEFORE the Electron - * runtime starts, so bootstrap.js can read it and set process.env.DATA_PATH. - * - * The sandbox filesDir is used as the DATA_PATH for reliable app data - * storage (nedb databases, config, logs). It is always writable and - * does not require runtime permission requests. - * - * For the user-visible home directory (os.homedir()), EntryAbility.ets - * requests READ_WRITE_DOCUMENTS_DIRECTORY and writes a separate marker - * (.electerm-documents-path) that bootstrap.js uses to override os.homedir() - * to point at the Documents folder. + * Minimal for the web app: all backend bootstrapping happens in + * pages/Index (native child process + HTTP polling). Kept as the module + * srcEntry for future app-level hooks. */ -import { WebAbilityStage } from 'web_engine'; -import fs from '@ohos.file.fs'; +import { AbilityStage } from '@kit.AbilityKit'; -const TAG: string = 'ElectermAbilityStage'; - -export default class ElectermAbilityStage extends WebAbilityStage { +export default class ElectermAbilityStage extends AbilityStage { onCreate(): void { - super.onCreate(); - - // super.onCreate() schedules the Electron runtime start via setTimeout(0). - // This synchronous code runs BEFORE that setTimeout fires, so the marker - // files are guaranteed to exist when bootstrap.js loads. - const filesDir: string = this.context.getApplicationContext().filesDir; - - try { - const markerPath: string = `${filesDir}/.electerm-data-path`; - const file = fs.openSync(markerPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC); - fs.writeSync(file.fd, filesDir); - fs.closeSync(file); - console.info(`[${TAG}] wrote data path marker: ${markerPath} → ${filesDir}`); - } catch (e) { - console.error(`[${TAG}] failed to write data path marker: ${JSON.stringify(e)}`); - } + console.info('[ElectermAbilityStage] onCreate'); } } diff --git a/entry/src/main/ets/BackendManager.ets b/entry/src/main/ets/BackendManager.ets new file mode 100644 index 0000000..d56d3f2 --- /dev/null +++ b/entry/src/main/ets/BackendManager.ets @@ -0,0 +1,29 @@ +/** + * BackendManager — tracks the Node.js child process for the app lifetime. + * + * pages/Index starts the backend and records the pid here; + * EntryAbility.onDestroy() calls BackendManager.killBackend() so the port + * is freed when the app is terminated. + */ + +import { killNode } from 'libnode_ctl.so'; + +export class BackendManager { + static pid: number = -1; + + static setPid(pid: number): void { + BackendManager.pid = pid; + } + + static killBackend(): void { + if (BackendManager.pid > 0) { + try { + killNode(BackendManager.pid); + console.info(`[BackendManager] killed node pid=${BackendManager.pid}`); + } catch (e) { + console.error(`[BackendManager] kill failed: ${JSON.stringify(e)}`); + } + BackendManager.pid = -1; + } + } +} diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 284dc68..7bf912b 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -1,31 +1,22 @@ /** - * EntryAbility — main ability for the electerm-harmony app. + * EntryAbility — main ability for the electerm-harmony web app. * - * Extends WebAbility from the web_engine module, which handles window - * creation, XComponent setup, and Electron runtime startup. + * Plain UIAbility (no electron/web_engine): loads pages/Index, which boots + * the on-device Node.js backend as a native child process and renders the + * UI in an ArkWeb Web component pointed at http://127.0.0.1:5577. * - * Key responsibility: request ALL declared user-grant permissions at - * startup, BEFORE the Electron runtime starts. This is done in - * onWindowStageCreate() before calling super.onWindowStageCreate(), - * because super.onWindowStageCreate() loads the WebWindow page which - * triggers the Electron runtime to load bootstrap.js. - * - * After permissions are granted, writes a second marker file - * (.electerm-documents-path) containing the Documents directory path, - * which bootstrap.js uses to override os.homedir() so that file save - * dialogs and SFTP local paths default to the user-visible Documents - * folder. + * Responsibilities: + * - request user-grant permissions at startup (Documents/Desktop/Download + * directory access for SFTP local paths, pasteboard for copy/paste); + * - on destroy, terminate the Node.js child process so port 5577 is freed. */ import window from '@ohos.window'; import Want from '@ohos.app.ability.Want'; import AbilityConstant from '@ohos.app.ability.AbilityConstant'; import { Configuration } from '@ohos.app.ability.Configuration'; -import { abilityAccessCtrl, common, Permissions, PermissionRequestResult } from '@kit.AbilityKit'; +import { abilityAccessCtrl, common, Permissions, PermissionRequestResult, UIAbility } from '@kit.AbilityKit'; import { Environment } from '@kit.CoreFileKit'; -import fs from '@ohos.file.fs'; - -import { WebAbility } from 'web_engine'; const TAG: string = 'ElectermEntryAbility'; @@ -33,8 +24,6 @@ const TAG: string = 'ElectermEntryAbility'; // Permissions without "reason"/"usedScene" (INTERNET, GET_NETWORK_INFO, // ACCESS_CERT_MANAGER, PRINT, GYROSCOPE, ACCELEROMETER) are normal or // system-grant permissions that don't need runtime request. -// Removed LOCATION, MICROPHONE, CAMERA, ACCESS_BLUETOOTH — a terminal/SSH -// app has no use for geolocation, audio/video capture, or Bluetooth. const ALL_USER_PERMISSIONS: Permissions[] = [ 'ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY', 'ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY', @@ -42,46 +31,50 @@ const ALL_USER_PERMISSIONS: Permissions[] = [ 'ohos.permission.READ_PASTEBOARD' ]; -export default class EntryAbility extends WebAbility { - onConfigurationUpdate(config: Configuration) { - super.onConfigurationUpdate(config); - } - +export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { - super.onCreate(want, launchParam); - } - - async onPrepareToTerminateAsync(): Promise { - return await super.onPrepareToTerminateAsync(); } - async onDestroy(): Promise { - await super.onDestroy(); + onConfigurationUpdate(config: Configuration) { } async onWindowStageCreate(windowStage: window.WindowStage) { - // Request ALL declared user-grant permissions BEFORE the Electron - // runtime starts. super.onWindowStageCreate() loads the WebWindow - // page which triggers the Electron runtime to load bootstrap.js. - // - // If a permission is already granted, requestPermissionsFromUser - // returns immediately without showing a dialog for that permission. + // Request permissions up front. Index requests them again via + // its own context when it needs to write user-visible files; a + // granted permission here just avoids the dialogs appearing later. await this.requestAllPermissions(); - this.writeDocumentsPathMarker(); - super.onWindowStageCreate(windowStage); + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + console.error(`[${TAG}] Failed to load content: ${JSON.stringify(err)}`); + return; + } + console.info(`[${TAG}] content loaded`); + }); } onWindowStageDestroy() { - super.onWindowStageDestroy(); } onForeground() { - super.onForeground(); } onBackground() { - super.onBackground(); + } + + async onDestroy(): Promise { + // Index registered a cleanup callback (kills the node child process) + // in its LocalStorage when it appeared. + try { + const storage = LocalStorage.getShared(); + const cleanup = storage.get('backendCleanup') as () => void; + if (cleanup) { + cleanup(); + console.info(`[${TAG}] backend cleanup done`); + } + } catch (e) { + console.warn(`[${TAG}] backend cleanup failed: ${JSON.stringify(e)}`); + } } /** @@ -105,37 +98,4 @@ export default class EntryAbility extends WebAbility { console.error(`[${TAG}] Failed to request permissions: ${JSON.stringify(e)}`); } } - - /** - * Write the documents-path marker file. - * - * After permissions are granted, Environment.getUserDocumentDir() - * returns the user-visible Documents directory path. This is written - * to a separate marker (.electerm-documents-path) which bootstrap.js - * reads to override os.homedir() — so that file save dialogs, SFTP - * local paths, and other home-directory-based operations default to - * the user-visible Documents folder. - * - * If the permission was denied, the marker is not written and - * bootstrap.js falls back to the original os.homedir() value. - */ - private writeDocumentsPathMarker(): void { - try { - const filesDir: string = this.context.getApplicationContext().filesDir; - - try { - const documentsDir: string = Environment.getUserDocumentDir(); - const markerPath: string = `${filesDir}/.electerm-documents-path`; - const file = fs.openSync(markerPath, - fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC); - fs.writeSync(file.fd, documentsDir); - fs.closeSync(file); - console.info(`[${TAG}] wrote documents path marker: ${markerPath} → ${documentsDir}`); - } catch (e) { - console.warn(`[${TAG}] getUserDocumentDir failed, skipping documents marker: ${JSON.stringify(e)}`); - } - } catch (e) { - console.error(`[${TAG}] Failed to write documents path marker: ${JSON.stringify(e)}`); - } - } } diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 463d814..1555db3 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -1,61 +1,186 @@ /** - * Index page — WebWindow host for the Electron 鸿蒙 runtime. + * Index page — ArkWeb host for the electerm web app. * - * Uses the WebWindow component from web_engine, which manages the - * XComponent surface and native context (libadapter.so). The Electron - * runtime (libelectron.so) starts automatically and loads - * resfile/resources/app/main.js. + * Startup sequence: + * 1. create the writable data dir (filesDir/electerm-data) + * 2. start the Node.js backend as a *native child process* + * (libnode_launcher.so:Main → execv libnode.so index.js — the backend + * serves the UI + SSH/SFTP/... API on http://127.0.0.1:5577) + * 3. poll the backend with plain HTTP until it answers + * 4. navigate the Web component from the local loading page to the backend + * + * While the engine is starting (or failed to start) a native overlay is + * shown; the Web component stays loaded with rawfile/loading.html behind it. */ -import { WebWindow } from 'web_engine'; -import { NativeContext } from 'web_engine/src/main/ets/interface/CommonInterface'; -import JsBindingUtils from 'web_engine/src/main/ets/utils/JsBindingUtils'; -import { ContextType } from 'web_engine/src/main/ets/common/Constants'; -import { uiObserver } from '@kit.ArkUI'; +import { webview } from '@kit.ArkWeb'; +import { common } from '@kit.AbilityKit'; +import { childProcessManager } from '@kit.AbilityKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import fs from '@ohos.file.fs'; +import http from '@ohos.net.http'; +import { BackendManager } from '../BackendManager'; + +const TAG: string = 'electerm.Index'; +const BACKEND_PORT: number = 5577; +const SERVER_URL: string = `http://127.0.0.1:${BACKEND_PORT}`; +const POLL_INTERVAL_MS: number = 500; +const BOOT_TIMEOUT_MS: number = 90_000; -let storage = LocalStorage.getShared(); -@Entry(storage) +@Entry @Component struct Index { - @LocalStorageLink('xcomponentId') xComponentId: string = ''; - @LocalStorageProp('statusBarHeight') statusBarHeight: number = 0; - @State density: number = 0; - private nativeContext: NativeContext = - JsBindingUtils.getNativeContext(ContextType.kMainProcess); + controller: webview.WebviewController = new webview.WebviewController(); + @State serverReady: boolean = false; + @State bootFailed: boolean = false; + @State statusMessage: string = 'Starting electerm …'; aboutToAppear(): void { - uiObserver.on('densityUpdate', this.getUIContext(), (info: uiObserver.DensityInfo) => { - this.density = info.density; - }); + this.startBackend(); + } + + /** Create the writable data dir and spawn the node backend. */ + async startBackend(): Promise { + try { + const context = getContext(this) as common.Context; + const filesDir: string = context.filesDir; + const dataDir: string = `${filesDir}/electerm-data`; + const bundleCodeDir: string = context.bundleCodeDir; + const scriptPath: string = `${bundleCodeDir}/entry/resource/resfile/electerm/index.js`; + + // 1. writable data dir (db, ssh keys, logs — the resfile install dir + // the backend itself runs from is read-only) + if (!this.dirExists(dataDir)) { + fs.mkdirSync(dataDir, true); + } + + // 2. start the native child process + // entryParams is a plain "key=value\n" string parsed by node_launcher.c + const entryParams: string = [ + `dataDir=${dataDir}`, + `script=${scriptPath}`, + `port=${BACKEND_PORT}` + ].join('\n'); + + this.statusMessage = 'Starting Node.js engine …'; + console.info(`[${TAG}] starting native child process, params:\n${entryParams}`); + const pid: number = await childProcessManager.startNativeChildProcess( + 'libnode_launcher.so:Main', + { entryParams: entryParams } + ); + BackendManager.setPid(pid); + console.info(`[${TAG}] node child process started, pid=${pid}`); + + // 3. wait for the HTTP server + this.statusMessage = 'Starting engine …'; + const ok: boolean = await this.waitForBackend(); + if (!ok) { + this.bootFailed = true; + this.statusMessage = + 'Engine failed to start. See electerm-data/node-boot.log'; + return; + } + + // 4. load the real UI + this.serverReady = true; + this.controller.loadUrl(SERVER_URL); + } catch (e) { + const err = e as BusinessError; + this.bootFailed = true; + this.statusMessage = `Startup error: [${err.code}] ${err.message}`; + console.error(`[${TAG}] startBackend failed: ${JSON.stringify(e)}`); + } + } + + dirExists(path: string): boolean { + try { + const stat = fs.statSync(path); + return stat.isDirectory(); + } catch { + return false; + } + } + + /** Poll http://127.0.0.1:5577 until it answers or BOOT_TIMEOUT_MS elapses. */ + async waitForBackend(): Promise { + const deadline: number = Date.now() + BOOT_TIMEOUT_MS; + while (Date.now() < deadline) { + if (await this.probe()) { + return true; + } + await this.sleep(POLL_INTERVAL_MS); + } + return false; + } + + async probe(): Promise { + const httpClient = http.createHttp(); + try { + const response = await httpClient.request(SERVER_URL, { + method: http.RequestMethod.GET, + connectTimeout: 3000, + readTimeout: 3000, + usingCache: false + }); + return response.responseCode >= 200 && response.responseCode < 500; + } catch { + return false; + } finally { + httpClient.destroy(); + } } - aboutToDisappear(): void { - uiObserver.off('densityUpdate', this.getUIContext()); + sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(), ms); + }); } onBackPress(): boolean { - // On 2-in-1/PC devices the system maps the ESC key to the back event. - // Only hand off to the last widget when one actually exists — otherwise - // the native adapter falls back to minimizing this window, which is not - // the expected behavior for a plain ESC key press. - if (this.nativeContext.GetLastActiveWidgetId()) { - this.nativeContext.OnBackToLastPage(this.xComponentId); + try { + if (this.controller.accessBackward()) { + this.controller.backward(); + return true; + } + } catch { + // controller not attached yet — fall through } - return true; + return false; } build() { - Row() { - WebWindow() + Stack() { + Web({ src: $rawfile('loading.html'), controller: this.controller }) + .width('100%') + .height('100%') + .javaScriptAccess(true) + .domStorageAccess(true) + + if (!this.serverReady) { + Column({ space: 16 }) { + Text('electerm') + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor('#cfd6e4') + if (!this.bootFailed) { + LoadingProgress() + .width(36) + .height(36) + .color('#4aa3ff') + } + Text(this.statusMessage) + .fontSize(13) + .fontColor(this.bootFailed ? '#e5484d' : '#8b93a7') + .textAlign(TextAlign.Center) + .padding({ left: 24, right: 24 }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + .backgroundColor('#15171a') + } } .width('100%') .height('100%') - .padding({ - top: this.getStatusBarHeight(), - }) - } - - getStatusBarHeight(): number { - return this.getUIContext().px2vp(this.statusBarHeight); } } diff --git a/entry/src/main/ets/pages/NodeHandleWindow.ets b/entry/src/main/ets/pages/NodeHandleWindow.ets deleted file mode 100644 index 3af72f5..0000000 --- a/entry/src/main/ets/pages/NodeHandleWindow.ets +++ /dev/null @@ -1,65 +0,0 @@ -/** - * NodeHandleWindow page — alternative WebWindow host for devices that - * support the NodeHandle rendering path. - * - * Uses the WebNodeHandleWindow component from web_engine, which manages - * the ContentSlot surface and native context (libadapter.so). The Electron - * runtime (libelectron.so) starts automatically and loads - * resfile/resources/app/main.js. - * - * This page is loaded by WebAbility when nativeContext.IsSupportNodeHandleFeature() - * returns true. If the device does not support NodeHandle, pages/Index is used instead. - */ - -import { WebNodeHandleWindow } from 'web_engine'; -import { NativeContext } from 'web_engine/src/main/ets/interface/CommonInterface'; -import JsBindingUtils from 'web_engine/src/main/ets/utils/JsBindingUtils'; -import { ContextType } from 'web_engine/src/main/ets/common/Constants'; -import { uiObserver } from '@kit.ArkUI'; - -let storage = LocalStorage.getShared(); -@Entry(storage) -@Component -struct NodeHandleWindow { - @LocalStorageLink('xcomponentId') xComponentId: string = ''; - @LocalStorageProp('statusBarHeight') statusBarHeight: number = 0; - @State density: number = 0; - private nativeContext: NativeContext = - JsBindingUtils.getNativeContext(ContextType.kMainProcess); - - aboutToAppear(): void { - uiObserver.on('densityUpdate', this.getUIContext(), (info: uiObserver.DensityInfo) => { - this.density = info.density; - }); - } - - aboutToDisappear(): void { - uiObserver.off('densityUpdate', this.getUIContext()); - } - - onBackPress(): boolean { - // On 2-in-1/PC devices the system maps the ESC key to the back event. - // Only hand off to the last widget when one actually exists — otherwise - // the native adapter falls back to minimizing this window, which is not - // the expected behavior for a plain ESC key press. - if (this.nativeContext.GetLastActiveWidgetId()) { - this.nativeContext.OnBackToLastPage(this.xComponentId); - } - return true; - } - - build() { - Row() { - WebNodeHandleWindow() - } - .width('100%') - .height('100%') - .padding({ - top: this.getStatusBarHeight(), - }) - } - - getStatusBarHeight(): number { - return this.getUIContext().px2vp(this.statusBarHeight); - } -} diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index e6ea9b7..d44c44c 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -64,11 +64,6 @@ "name": "ohos.permission.ACCELEROMETER" } ], - "definePermissions": [ - { - "name": "ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY" - } - ], "abilities": [ { "name": "EntryAbility", diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 1104194..5eddbb9 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -23,6 +23,10 @@ { "name": "reason", "value": "electerm needs access to the Documents, Download, and Desktop directories to store app data, transfer files, and persist settings reliably across restarts" + }, + { + "name": "access_pasteboard", + "value": "electerm needs pasteboard access to copy and paste commands and output in the terminal" } ] } diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json index 7c7c1bd..1898d94 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -1,6 +1,5 @@ { "src": [ - "pages/Index", - "pages/NodeHandleWindow" + "pages/Index" ] } diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 index 0464a00..eaf2fcb 100644 --- a/hvigor/hvigor-config.json5 +++ b/hvigor/hvigor-config.json5 @@ -1,13 +1,9 @@ { - "modelVersion": "5.3.15", + "modelVersion": "5.0.0", "dependencies": { - "@ohos/hvigor-ohos-plugin": "5.10.3" + "@ohos/hvigor-ohos-plugin": "file:/Applications/DevEco-Studio.app/Contents/tools/hvigor/hvigor-ohos-plugin" }, "execution": {}, - "logging": { - "level": "info" - }, - "debugging": { - "quiet": false - } + "logging": { "level": "info" }, + "debugging": { "quiet": false } } diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..2bba889 --- /dev/null +++ b/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony +ohos.sdk.dir=/Applications/DevEco-Studio.app/Contents/sdk diff --git a/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000..96541d5 --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,20 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hypium@1.0.21": "@ohos/hypium@1.0.21" + }, + "packages": { + "@ohos/hypium@1.0.21": { + "name": "@ohos/hypium", + "version": "1.0.21", + "integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.21.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 759c774..9a27bc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "electerm", + "name": "electerm-android", "version": "5.3.15", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "electerm", + "name": "electerm-android", "version": "5.3.15", "hasInstallScript": true, "license": "MIT", @@ -13,48 +13,55 @@ "@electerm/electerm-locales": "2.3.10", "@electerm/electerm-themes": "^1.0.1", "@electerm/ftp-srv": "1.0.5", - "@electerm/nedb": "2.0.0", "@electerm/ssh2": "1.22.0", "@xterm/headless": "6.1.0-beta.292", "axios": "1.18.1", "basic-ftp": "6.0.1", - "commander": "12.1.0", + "dayjs": "^1.11.21", "diffie-hellman": "^5.0.3", + "dotenv": "16.3.1", "electerm-sync": "2.0.1", - "electron-log": "4.3.5", - "express": "5.2.1", + "esbuild": "^0.28.1", + "express": "4.22.2", + "express-jwt": "^8.5.1", "express-ws": "5.0.2", "fast-deep-equal": "3.1.3", "find-free-port": "2.0.0", "font-list": "1.5.1", + "gist-wrapper": "1.0.0", + "gitee-client": "1.0.0", + "glob": "^13.0.6", "https-proxy-agent": "7.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "0.7.2", "json-deep-copy": "1.3.1", "jsonwebtoken": "^9.0.1", - "nanoid": "3.3.8", + "lodash": "4.18.1", + "morgan": "^1.10.1", + "multer": "^2.2.0", + "nanoid": "^5.1.11", "node-bash": "5.0.1", - "node-forge": "1.4.0", - "os-locale-s": "1.1.3", + "node-pty": "1.2.0-beta.15", + "os-locale-s": "^1.1.3", + "pug": "^3.0.4", + "serialport": "13.0.0", "socks": "2.8.9", "socks-proxy-agent": "8.0.1", "socksv5-server": "^1.0.2", + "sql.js": "^1.12.0", "ssh-config-loader": "1.1.2", "ssh2-scp": "3.2.1", - "tar": "7.5.21", + "tar": "^7.5.21", "trzsz2": "1.2.0", "zmodem2": "1.4.0" }, - "bin": { - "electerm": "npm/electerm" - }, "devDependencies": { - "@ant-design/icons": "6.2.5", + "@ant-design/icons": "^6.2.5", "@electerm/electerm-react": "^5.3.15", "@electerm/electerm-resource": "2.2.1", - "@fontsource/maple-mono": "^5.2.5", - "@novnc/novnc": "1.7.0", - "@types/node": "22.12.0", - "@vitejs/plugin-react": "^5.2.0", + "@fontsource/maple-mono": "^5.2.6", + "@novnc/novnc": "^1.7.0", + "@types/node": "22.9.3", + "@vitejs/plugin-react": "5.2.0", "@xterm/addon-attach": "0.13.0-beta.292", "@xterm/addon-fit": "0.12.0-beta.292", "@xterm/addon-image": "0.10.0-beta.292", @@ -64,34 +71,30 @@ "@xterm/addon-web-links": "0.13.0-beta.292", "@xterm/addon-webgl": "0.20.0-beta.291", "@xterm/xterm": "6.1.0-beta.292", - "antd": "6.5.1", + "antd": "^6.5.1", "classnames": "2.5.1", "cross-env": "7.0.3", - "dotenv": "16.4.5", "electerm-icons": "1.0.1", - "electron": "^39.2.7", + "escape-string-regexp": "^5.0.0", + "express-http-proxy": "^2.1.2", "filesize": "10.1.6", "filesize-parser": "1.5.1", - "glob": "^13.0.6", - "ironrdp-wasm": "1.1.0", - "lodash-es": "^4.17.21", - "manate": "2.0.3", - "morgan": "1.11.0", - "multer": "^2.2.0", - "pug": "3.0.4", - "react": "19.2.7", + "ironrdp-wasm": "^1.1.0", + "lodash-es": "^4.18.1", + "manate": "^2.0.3", + "react": "^19.2.6", "react-diff-viewer-continued": "^4.4.0", - "react-dom": "19.2.7", + "react-dom": "^19.2.6", "react-markdown": "9.0.1", "replace-in-file": "6.3.5", "shelljs": "0.8.5", - "spice-client": "1.2.0", + "spice-client": "^1.2.0", "standard": "^17.1.2", "stylus": "^0.64.0", - "vite": "8.1.0" + "vite": "^8.0.15" }, "engines": { - "node": ">=16.0.0" + "node": ">=24.0.0" } }, "node_modules/@adobe/css-tools": { @@ -158,14 +161,14 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz", - "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", + "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", "dev": true, "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", - "@ant-design/icons-svg": "^4.4.2", + "@ant-design/icons-svg": "^4.5.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, @@ -347,7 +350,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -357,7 +359,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -391,7 +392,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -483,7 +483,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -540,25 +539,6 @@ "node": ">=16" } }, - "node_modules/@electerm/ftp-srv/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@electerm/nedb": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@electerm/nedb/-/nedb-2.0.0.tgz", - "integrity": "sha512-3u60Dnjs4OFMsAmBhFZ4JsMrpVetY+S0LcGRSW0/15LAiI6+DqAwvxiY4HxC1S5I21nMHWMjgV228U3zb/F78w==", - "license": "MIT", - "dependencies": { - "@yetzt/binary-search-tree": "^0.2.6", - "mkdirp": "^1.0.4" - } - }, "node_modules/@electerm/ssh2": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@electerm/ssh2/-/ssh2-1.22.0.tgz", @@ -573,28 +553,6 @@ "node": ">=10.16.0" } }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -663,6 +621,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@emotion/babel-plugin/node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -802,28 +773,444 @@ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "dev": true, "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "dev": true, - "license": "MIT" + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "dev": true, - "license": "MIT" + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -915,9 +1302,9 @@ } }, "node_modules/@fontsource/maple-mono": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/maple-mono/-/maple-mono-5.3.0.tgz", - "integrity": "sha512-8N3FVDWphO/971tZIig9D558j9jf09fWYIXFjgv7DKJEvDjtgfATx1ScWHdTCsf+tufFS5M1bAa4AumVBKPhSQ==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/maple-mono/-/maple-mono-5.2.6.tgz", + "integrity": "sha512-+VAD7z8nyTkaiz2/Ww639Z5Kp/YJIL4dNMQxydqSMafNb5nKxFeoRq6W3g9WJ04IcBjdBmn0dKFNk90lyz7K+w==", "dev": true, "license": "OFL-1.1", "funding": { @@ -1951,9 +2338,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.1.tgz", - "integrity": "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.0.tgz", + "integrity": "sha512-uC3QSG7Ax3qLOE5Q2jLqJCJc4iBtJEHzNTPhqGvlRvRcU8x8CT5moIavRVe24YSQKCp2/D1GSq7y76SCSheuVA==", "dev": true, "license": "MIT", "dependencies": { @@ -2299,30 +2686,252 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, "engines": { - "node": ">=10" + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", + "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "12.0.0", + "debug": "4.4.0", + "node-addon-api": "8.3.0", + "node-gyp-build": "4.8.4" + }, + "engines": { + "node": ">=18.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "url": "https://opencollective.com/serialport/donate" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.0" + "@serialport/parser-delimiter": "12.0.0" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "license": "MIT", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", + "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", + "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", + "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", + "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", + "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", + "license": "MIT", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", + "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "13.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", + "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", + "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", + "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", + "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", + "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/@tybys/wasm-util": { @@ -2381,19 +2990,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2431,13 +3027,6 @@ "@types/unist": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2445,13 +3034,13 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { + "@types/ms": "*", "@types/node": "*" } }, @@ -2469,17 +3058,15 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", - "dev": true, + "version": "22.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", + "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.19.8" } }, "node_modules/@types/parse-json": { @@ -2496,16 +3083,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2513,17 +3090,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", @@ -2668,20 +3234,14 @@ "addons/*" ] }, - "node_modules/@yetzt/binary-search-tree": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@yetzt/binary-search-tree/-/binary-search-tree-0.2.6.tgz", - "integrity": "sha512-e/8wt8AAumI8VK5sv09b3IgWuRoblXJ5z0SQYfrL2nap89oKihvVaP1zy3FzD5NaeRi1X0gdXZA9lB3QAZILBg==", - "license": "MIT" - }, "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" @@ -2701,7 +3261,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2836,31 +3395,10 @@ "react-dom": ">=18.0.0" } }, - "node_modules/antd/node_modules/@ant-design/icons": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", - "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^8.0.1", - "@ant-design/icons-svg": "^4.5.0", - "@rc-component/util": "^1.11.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -2887,6 +3425,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -3034,7 +3578,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, "license": "MIT" }, "node_modules/asn1": { @@ -3050,7 +3593,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", - "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -3142,7 +3684,6 @@ "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.9.6" @@ -3166,16 +3707,15 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", - "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3189,7 +3729,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" @@ -3202,7 +3741,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/basic-ftp": { @@ -3230,56 +3768,60 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3295,9 +3837,9 @@ "license": "MIT" }, "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, "funding": [ { @@ -3315,9 +3857,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, @@ -3328,16 +3870,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3348,7 +3880,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/builtins": { @@ -3378,7 +3909,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, "dependencies": { "streamsearch": "^1.1.0" }, @@ -3395,35 +3925,6 @@ "node": ">= 0.8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3568,7 +4069,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dev": true, "license": "MIT", "dependencies": { "is-regex": "^1.0.3" @@ -3600,6 +4100,24 @@ "trim-buffer": "^5.0.0" } }, + "node_modules/child-shell/node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -3631,19 +4149,6 @@ "node": ">=12" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3698,12 +4203,12 @@ } }, "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/compute-scroll-into-view": { @@ -3724,7 +4229,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, "engines": [ "node >= 6.0" ], @@ -3740,7 +4244,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.6.0", @@ -3748,16 +4251,15 @@ } }, "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "safe-buffer": "5.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 0.6" } }, "node_modules/content-type": { @@ -3786,13 +4288,10 @@ } }, "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "7.1.0", @@ -3929,7 +4428,6 @@ "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3963,35 +4461,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3999,16 +4468,6 @@ "dev": true, "license": "MIT" }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4073,6 +4532,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4083,14 +4552,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -4143,20 +4604,18 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", - "dev": true, "license": "MIT" }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dunder-proto": { @@ -4211,35 +4670,10 @@ "jsonwebtoken": "^9.0.3" } }, - "node_modules/electron": { - "version": "39.2.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", - "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-log": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.3.5.tgz", - "integrity": "sha512-J5Ew3axdk7W4jzzxKLSAi1sqbcAoo9CzHuBVsG0tT47j256xKulNrWFf3lZmHJ1KDXOQUcuwOngQF0jjmpEdpw==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.395", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", - "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", "dev": true, "license": "ISC" }, @@ -4259,26 +4693,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -4484,13 +4898,53 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, "license": "MIT", - "optional": true + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } }, "node_modules/escalade": { "version": "3.2.0", @@ -4509,13 +4963,13 @@ "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5105,6 +5559,19 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5222,48 +5689,96 @@ "license": "MIT" }, "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/express-http-proxy": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-2.1.2.tgz", + "integrity": "sha512-FXcAcs7Nf/hF73Mzh0WDWPwaOlsEUL/fCHW3L4wU6DH79dypsaxmbnAildCLniFs7HQuuvoiR6bjNVUvGuTb5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.0.1", + "es6-promise": "^4.1.1", + "raw-body": "^2.3.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/express-http-proxy/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/express-ws": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", @@ -5279,6 +5794,21 @@ "express": "^4.0.0 || ^5.0.0-alpha.1" } }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5286,27 +5816,6 @@ "dev": true, "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5337,16 +5846,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5395,27 +5894,39 @@ "dev": true, "license": "MIT" }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-free-port": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-free-port/-/find-free-port-2.0.0.tgz", @@ -5462,9 +5973,9 @@ } }, "node_modules/flatted": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", - "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -5543,27 +6054,6 @@ "node": ">= 6" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5574,27 +6064,12 @@ } }, "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" + "node": ">= 0.6" } }, "node_modules/fs.realpath": { @@ -5742,22 +6217,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -5776,11 +6235,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gist-wrapper": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gist-wrapper/-/gist-wrapper-1.0.0.tgz", + "integrity": "sha512-vKkE6mkO8TeeS6rcnHeniPIflBCqKqgM2cPqu/lQxJS3pXdmpGyc7KibehpU1M4O+92y3k84UnfwOoQOQaRPCQ==", + "license": "MIT", + "peerDependencies": { + "axios": "*" + } + }, + "node_modules/gitee-client": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitee-client/-/gitee-client-1.0.0.tgz", + "integrity": "sha512-r2mmnUnMCOGp5e38fD2c07995gHbhUNKjv32SzwNppFqLCP7cGHkNqKaVMiLuMr9uzytowXPEFgDyQHMtIgygA==", + "license": "MIT", + "peerDependencies": { + "axios": "*" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -5807,39 +6283,6 @@ "node": ">=10.13.0" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -5885,32 +6328,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6118,13 +6535,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -6145,20 +6555,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", @@ -6173,9 +6569,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6421,7 +6817,6 @@ "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -6499,7 +6894,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dev": true, "license": "MIT", "dependencies": { "acorn": "^7.1.1", @@ -6663,14 +7057,12 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6863,7 +7255,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", - "dev": true, "license": "MIT" }, "node_modules/js-tokens": { @@ -6950,14 +7341,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, "node_modules/json2mq": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", @@ -6981,16 +7364,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -7029,7 +7402,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dev": true, "license": "MIT", "dependencies": { "is-promise": "^2.0.0", @@ -7107,9 +7479,9 @@ } }, "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -7123,23 +7495,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -7158,9 +7530,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -7179,9 +7551,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -7200,9 +7572,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -7221,9 +7593,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -7242,9 +7614,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -7263,9 +7635,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -7284,9 +7656,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -7305,9 +7677,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -7326,9 +7698,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -7347,9 +7719,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -7417,6 +7789,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -7497,16 +7875,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7524,20 +7892,6 @@ "dev": true, "license": "MIT" }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7709,12 +8063,12 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/memoize-one": { @@ -7725,17 +8079,23 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -8212,46 +8572,43 @@ "miller-rabin": "bin/miller-rabin" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" + "mime-db": "1.52.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -8294,23 +8651,10 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/morgan": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", - "dev": true, "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -8331,7 +8675,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -8341,7 +8684,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/ms": { @@ -8354,7 +8696,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", - "dev": true, "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -8367,60 +8708,13 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" + "url": "https://opencollective.com/express" } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -8429,10 +8723,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -8443,14 +8737,20 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-bash": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/node-bash/-/node-bash-5.0.1.tgz", @@ -8479,13 +8779,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-pty": { + "version": "1.2.0-beta.15", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.15.tgz", + "integrity": "sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" } }, "node_modules/node-releases": { @@ -8498,24 +8810,10 @@ "node": ">=18" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8649,7 +8947,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8659,6 +8956,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8703,14 +9001,13 @@ } }, "node_modules/own-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", - "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", + "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -8721,16 +9018,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -8939,14 +9226,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -8963,21 +9248,16 @@ "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -8989,13 +9269,6 @@ "node": ">=8" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9117,9 +9390,9 @@ } }, "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -9137,7 +9410,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9174,21 +9447,10 @@ "node": ">= 0.8.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, "license": "MIT", "dependencies": { "asap": "~2.0.3" @@ -9250,7 +9512,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", - "dev": true, "license": "MIT", "dependencies": { "pug-code-gen": "^3.0.4", @@ -9267,7 +9528,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dev": true, "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -9279,7 +9539,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", - "dev": true, "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -9296,14 +9555,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", - "dev": true, "license": "MIT" }, "node_modules/pug-filters": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dev": true, "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -9317,7 +9574,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dev": true, "license": "MIT", "dependencies": { "character-parser": "^2.2.0", @@ -9329,7 +9585,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dev": true, "license": "MIT", "dependencies": { "pug-error": "^2.0.0", @@ -9340,7 +9595,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -9351,7 +9605,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dev": true, "license": "MIT", "dependencies": { "pug-error": "^2.0.0", @@ -9362,14 +9615,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", - "dev": true, "license": "MIT" }, "node_modules/pug-strip-comments": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dev": true, "license": "MIT", "dependencies": { "pug-error": "^2.0.0" @@ -9379,20 +9630,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", - "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9440,19 +9679,6 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9463,31 +9689,39 @@ } }, "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", + "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/react": { @@ -9537,9 +9771,9 @@ } }, "node_modules/react-is": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "dev": true, "license": "MIT" }, @@ -9584,7 +9818,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -9801,7 +10034,6 @@ "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9819,13 +10051,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9836,19 +10061,6 @@ "node": ">=4" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9930,25 +10142,6 @@ "node": "*" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -9990,28 +10183,6 @@ "dev": true, "license": "MIT" }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10154,88 +10325,103 @@ "semver": "bin/semver.js" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8.0" } }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "optional": true, "dependencies": { - "type-fest": "^0.13.1" + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serialport": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", + "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "13.0.0", + "@serialport/parser-byte-length": "13.0.0", + "@serialport/parser-cctalk": "13.0.0", + "@serialport/parser-delimiter": "13.0.0", + "@serialport/parser-inter-byte-timeout": "13.0.0", + "@serialport/parser-packet-length": "13.0.0", + "@serialport/parser-readline": "13.0.0", + "@serialport/parser-ready": "13.0.0", + "@serialport/parser-regex": "13.0.0", + "@serialport/parser-slip-encoder": "13.0.0", + "@serialport/parser-spacepacket": "13.0.0", + "@serialport/stream": "13.0.0", + "debug": "4.4.0" }, "engines": { - "node": ">=10" + "node": ">=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/serialport/donate" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, + "node_modules/serialport/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=10" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8.0" } }, "node_modules/set-function-length": { @@ -10580,13 +10766,11 @@ "dev": true, "license": "LGPL-3.0-or-later" }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" }, "node_modules/ssh-config-loader": { "version": "1.1.2", @@ -10698,7 +10882,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -10707,7 +10890,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11044,19 +11226,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11074,7 +11243,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11084,9 +11252,9 @@ } }, "node_modules/tar": { - "version": "7.5.21", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz", - "integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -11155,7 +11323,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", - "dev": true, "license": "MIT" }, "node_modules/trim-buffer": { @@ -11275,34 +11442,16 @@ } }, "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/typed-array-buffer": { @@ -11387,7 +11536,6 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, "license": "MIT" }, "node_modules/unbox-primitive": { @@ -11410,10 +11558,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, "node_modules/unified": { @@ -11509,16 +11656,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -11573,9 +11710,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -11626,16 +11771,16 @@ } }, "node_modules/vite": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", - "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "~1.1.2", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { @@ -11707,7 +11852,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11822,7 +11966,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.9.6", @@ -11885,12 +12028,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.12.tgz", + "integrity": "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -11974,17 +12118,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d89653a..47808c3 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,35 @@ { - "name": "electerm", + "name": "electerm-harmony", "version": "5.3.15", - "description": "electerm — a free and open-source ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS, built on the electerm codebase.", - "main": "app.js", - "bin": "npm/electerm", + "description": "Free and open-sourced ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS (ArkWeb + on-device Node.js)", + "main": "src/app/app.js", + "type": "module", "scripts": { - "app": "node build/bin/app", - "build": "npm run vite-build", - "start": "node build/bin/start.js", + "dev": "NODE_ENV=development node ./src/app/app.js", + "prod": "NODE_ENV=production node ./src/app/app.js", + "build": "npm run compile", + "start": "NODE_ENV=development node ./build/vite/dev-server.js", "clean": "node build/bin/clean", "compile": "node build/bin/build", - "vite-build": "node build/bin/vite-build.js", + "logo": "python3 build/bin/gen-logo.py", + "vite-build": "cross-env NODE_ENV=production vite build --config ./build/vite/conf.js", "install": "node build/bin/install", - "prepare-file": "node build/bin/prepare", - "build:harmony": "node build/harmony/build.js", "lint": "./node_modules/.bin/standard --verbose", "fix": "./node_modules/.bin/standard --fix", "lock": "npm i --package-lock-only", - "r": "./build/bin/release", - "b": "npm run clean && npm run compile && npm run prepare-file" + "build:web": "node build/web/build.mjs" }, "license": "MIT", "languageRepo": "https://github.com/electerm/electerm-locales", "privacyNoticeLink": "https://github.com/electerm/electerm/wiki/privacy-notice", "knownIssuesLink": "https://github.com/electerm/electerm/wiki/Know-issues", - "sponsorLink": "https://electerm.org/sponsor-electerm", + "sponsorLink": "https://electerm.org/sponsor-electerm.html", "repository": { "type": "git", "url": "git+https://github.com/electerm/electerm-harmony.git" }, "author": { - "name": "赵旭东", + "name": "ZHAO Xudong", "email": "zxdong@gmail.com", "url": "https://github.com/zxdong262" }, @@ -40,17 +39,17 @@ "homepage": "https://electerm.org", "releases": "https://github.com/electerm/electerm-harmony/releases", "engines": { - "node": ">=16.0.0" + "node": ">=24.0.0" }, "preferGlobal": true, "devDependencies": { - "@ant-design/icons": "6.2.5", + "@ant-design/icons": "^6.2.5", "@electerm/electerm-react": "^5.3.15", "@electerm/electerm-resource": "2.2.1", - "@fontsource/maple-mono": "^5.2.5", - "@novnc/novnc": "1.7.0", - "@types/node": "22.12.0", - "@vitejs/plugin-react": "^5.2.0", + "@fontsource/maple-mono": "^5.2.6", + "@novnc/novnc": "^1.7.0", + "@types/node": "22.9.3", + "@vitejs/plugin-react": "5.2.0", "@xterm/addon-attach": "0.13.0-beta.292", "@xterm/addon-fit": "0.12.0-beta.292", "@xterm/addon-image": "0.10.0-beta.292", @@ -60,64 +59,70 @@ "@xterm/addon-web-links": "0.13.0-beta.292", "@xterm/addon-webgl": "0.20.0-beta.291", "@xterm/xterm": "6.1.0-beta.292", - "antd": "6.5.1", + "antd": "^6.5.1", "classnames": "2.5.1", "cross-env": "7.0.3", - "dotenv": "16.4.5", "electerm-icons": "1.0.1", - "electron": "^39.2.7", + "escape-string-regexp": "^5.0.0", + "express-http-proxy": "^2.1.2", "filesize": "10.1.6", "filesize-parser": "1.5.1", - "glob": "^13.0.6", - "ironrdp-wasm": "1.1.0", - "lodash-es": "^4.17.21", - "manate": "2.0.3", - "morgan": "1.11.0", - "multer": "^2.2.0", - "pug": "3.0.4", - "react": "19.2.7", + "ironrdp-wasm": "^1.1.0", + "lodash-es": "^4.18.1", + "manate": "^2.0.3", + "react": "^19.2.6", "react-diff-viewer-continued": "^4.4.0", - "react-dom": "19.2.7", + "react-dom": "^19.2.6", "react-markdown": "9.0.1", "replace-in-file": "6.3.5", "shelljs": "0.8.5", - "spice-client": "1.2.0", + "spice-client": "^1.2.0", "standard": "^17.1.2", "stylus": "^0.64.0", - "vite": "8.1.0" + "vite": "^8.0.15" }, "dependencies": { "@electerm/electerm-locales": "2.3.10", "@electerm/electerm-themes": "^1.0.1", "@electerm/ftp-srv": "1.0.5", - "@electerm/nedb": "2.0.0", "@electerm/ssh2": "1.22.0", "@xterm/headless": "6.1.0-beta.292", "axios": "1.18.1", "basic-ftp": "6.0.1", - "commander": "12.1.0", + "dayjs": "^1.11.21", "diffie-hellman": "^5.0.3", + "dotenv": "16.3.1", "electerm-sync": "2.0.1", - "electron-log": "4.3.5", - "express": "5.2.1", + "esbuild": "^0.28.1", + "express": "4.22.2", + "express-jwt": "^8.5.1", "express-ws": "5.0.2", "fast-deep-equal": "3.1.3", "find-free-port": "2.0.0", "font-list": "1.5.1", + "gist-wrapper": "1.0.0", + "gitee-client": "1.0.0", + "glob": "^13.0.6", "https-proxy-agent": "7.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "0.7.2", "json-deep-copy": "1.3.1", "jsonwebtoken": "^9.0.1", - "nanoid": "3.3.8", + "lodash": "4.18.1", + "morgan": "^1.10.1", + "multer": "^2.2.0", + "nanoid": "^5.1.11", "node-bash": "5.0.1", - "node-forge": "1.4.0", - "os-locale-s": "1.1.3", + "node-pty": "1.2.0-beta.15", + "os-locale-s": "^1.1.3", + "pug": "^3.0.4", + "serialport": "13.0.0", "socks": "2.8.9", "socks-proxy-agent": "8.0.1", "socksv5-server": "^1.0.2", + "sql.js": "^1.12.0", "ssh-config-loader": "1.1.2", "ssh2-scp": "3.2.1", - "tar": "7.5.21", + "tar": "^7.5.21", "trzsz2": "1.2.0", "zmodem2": "1.4.0" }, @@ -127,12 +132,6 @@ "LICENSE" ], "standard": { - "sourceType": "module", - "ignore": [ - "work", - "temp", - "dist" - ], "globals": [ "log", "MouseEvent", @@ -140,8 +139,12 @@ "FileReader", "CustomEvent", "onmessage", - "requestAnimationFrame", "self" - ] + ], + "ignore": [ + "/public/", + "src/client/entry-web/rle.js" + ], + "sourceType": "module" } -} +} \ No newline at end of file diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh new file mode 100755 index 0000000..612fb27 --- /dev/null +++ b/scripts/build-web-app.sh @@ -0,0 +1,404 @@ +#!/usr/bin/env bash +# build-web-app.sh — Build and sign the HarmonyOS APP package (web variant: +# ArkWeb + on-device Node.js, no electron runtime). +# +# Adapted from build-app.sh with the electron runtime steps removed: +# - only the `entry` module is built (no web_engine HAR) +# - prerequisites are entry/libs/arm64-v8a/libnode.so (from prepare-node.sh) +# and entry/src/main/resources/resfile/electerm (from prepare-web.sh) +# +# Builds an UNSIGNED .app with hvigorw assembleApp, then signs it directly +# with hap-sign-tool.jar using plaintext passwords. +# +# Prerequisites: +# - HarmonyOS Command Line Tools installed (ohpm, hvigorw in PATH) +# - Signing materials in signing/ directory +# - prepare-node.sh and prepare-web.sh already run +# +# Usage: +# ./scripts/build-web-app.sh [--debug|--release] +# +# Environment variables (all optional, see defaults below): +# OHOS_SDK_HOME — path to HarmonyOS SDK +# COMMANDLINE_TOOLS — path to Command Line Tools +# SIGNING_DIR — directory with .p12, .cer, .p7b (default: signing/) +# KEYSTORE_FILE — keystore filename (default: electerm.p12) +# CERT_FILE — certificate filename (default: electerm_publish.cer) +# PROFILE_FILE — profile filename (default: electermRelease.p7b) +# KEYSTORE_PASSWORD — keystore password (plaintext) +# KEY_PASSWORD — key password (plaintext) +# KEY_ALIAS — key alias (default: electerm_key) +set -euo pipefail + +# --- Parse args ------------------------------------------------------------- + +BUILD_MODE="release" +if [[ "${1:-}" == "--debug" ]]; then + BUILD_MODE="debug" +elif [[ "${1:-}" == "--release" ]]; then + BUILD_MODE="release" +fi + +# --- Config ----------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +SIGNING_DIR="${SIGNING_DIR:-${PROJECT_ROOT}/signing}" +KEYSTORE_FILE="${KEYSTORE_FILE:-electerm.p12}" +CERT_FILE="${CERT_FILE:-electerm_publish.cer}" +PROFILE_FILE="${PROFILE_FILE:-electermRelease.p7b}" +KEY_ALIAS="${KEY_ALIAS:-electerm_key}" + +KEYSTORE_PATH="${SIGNING_DIR}/${KEYSTORE_FILE}" +CERT_PATH="${SIGNING_DIR}/${CERT_FILE}" +PROFILE_PATH="${SIGNING_DIR}/${PROFILE_FILE}" + +OHPM="${OHPM:-ohpm}" +HVIGORW="${HVIGORW:-hvigorw}" + +# --- Read version from package.json ----------------------------------------- + +echo "==> Reading version from package.json ..." + +APP_VERSION=$(python3 -c "import json; print(json.load(open('${PROJECT_ROOT}/package.json'))['version'])") +echo " ✓ version: ${APP_VERSION}" + +# Compute versionCode from semver: major * 10000000 + minor * 100000 + patch +VERSION_CODE=$(python3 -c " +import re +v = '${APP_VERSION}' +m = re.match(r'(\d+)\.(\d+)\.(\d+)', v) +if m: + major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3)) + print(major * 10000000 + minor * 100000 + patch) +else: + print(1) +") + +if [ "${VERSION_CODE}" -gt 2147483647 ] || [ "${VERSION_CODE}" -lt 1 ]; then + echo " ✗ versionCode ${VERSION_CODE} is out of range (1–2147483647)" + exit 1 +fi +echo " ✓ versionCode: ${VERSION_CODE}" + +# --- Verify build prerequisites --------------------------------------------- + +echo "==> Verifying build prerequisites ..." + +LIBS_DIR="${PROJECT_ROOT}/entry/libs/arm64-v8a" +RESFILE_APP_DIR="${PROJECT_ROOT}/entry/src/main/resources/resfile/electerm" + +if [ ! -f "${LIBS_DIR}/libnode.so" ]; then + echo " ✗ Missing: ${LIBS_DIR}/libnode.so" + echo " Run ./scripts/prepare-node.sh first." + exit 1 +fi +echo " ✓ Found: libnode.so ($(du -h "${LIBS_DIR}/libnode.so" | cut -f1))" + +for f in index.js app.bundle.mjs views/index.pug; do + if [ ! -f "${RESFILE_APP_DIR}/${f}" ]; then + echo " ✗ Missing: ${RESFILE_APP_DIR}/${f}" + echo " Run ./scripts/prepare-web.sh first." + exit 1 + fi + echo " ✓ Found: ${f}" +done + +for f in "${KEYSTORE_PATH}" "${CERT_PATH}" "${PROFILE_PATH}"; do + if [ ! -s "${f}" ]; then + echo " ✗ Missing signing material: ${f}" + exit 1 + fi +done +echo " ✓ Signing materials present" + +# --- Fix permissions for SDK compatibility ---------------------------------- + +echo "==> Cleaning unsupported permissions ..." + +UNSUPPORTED_PERMS="SET_ABILITY_INSTANCE_INFO GET_FILE_ICON PRIVACY_WINDOW LOCK_WINDOW_CURSOR ACCESS_BIOMETRIC SYSTEM_FLOAT_WINDOW FILE_ACCESS_PERSIST PREPARE_APP_TERMINATE CUSTOM_SCREEN_CAPTURE" + +ENTRY_MODULE_JSON="${PROJECT_ROOT}/entry/src/main/module.json5" + +remove_module_perms() { + local file="${1}" + local label="${2}" + shift 2 + + if [ ! -f "${file}" ]; then + echo " (${label} module.json5 not found, skipping)" + return + fi + + python3 - "${file}" "${label}" "$@" <<'PYEOF' +import re, sys + +file_path = sys.argv[1] +label = sys.argv[2] +perms = sys.argv[3:] + +with open(file_path, 'r') as f: + content = f.read() + +for perm in perms: + if f'"ohos.permission.{perm}"' not in content: + continue + pattern = ( + r'\{[^{}]*"ohos\.permission\.' + re.escape(perm) + r'"' + r'[^{}]*(?:\{[^{}]*\}[^{}]*)*\}[\s,]*' + ) + new_content = re.sub(pattern, '', content) + if new_content != content: + print(f' {label}: removed ohos.permission.{perm}') + content = new_content + else: + print(f' {label}: WARNING — could not remove ohos.permission.{perm}') + +with open(file_path, 'w') as f: + f.write(content) +PYEOF +} + +remove_module_perms "${ENTRY_MODULE_JSON}" "entry" ${UNSUPPORTED_PERMS} + +# --- Generate build-profile.json5 (entry module only, unsigned) -------------- + +echo "==> Configuring build-profile.json5 ..." + +BUILD_PROFILE="${PROJECT_ROOT}/build-profile.json5" + +SDK_PKG_JSON="${OHOS_SDK_HOME}/default/sdk-pkg.json" +if [ -f "${SDK_PKG_JSON}" ]; then + SDK_API_VERSION=$(python3 -c "import json; d=json.load(open('${SDK_PKG_JSON}')); print(d['data']['apiVersion'])" 2>/dev/null || echo "") + SDK_DISPLAY_NAME=$(python3 -c "import json; d=json.load(open('${SDK_PKG_JSON}')); print(d['data']['displayName'])" 2>/dev/null || echo "") + SDK_VERSION=$(echo "${SDK_DISPLAY_NAME}" | sed -n 's/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/p') + if [ -n "${SDK_API_VERSION}" ] && [ -n "${SDK_VERSION}" ]; then + COMPILE_SDK_VERSION="${SDK_VERSION}(${SDK_API_VERSION})" + echo " Detected SDK: ${SDK_DISPLAY_NAME} (API ${SDK_API_VERSION})" + else + COMPILE_SDK_VERSION="5.0.1(13)" + echo " Warning: Could not parse SDK version, using default 5.0.1(13)" + fi +else + COMPILE_SDK_VERSION="5.0.1(13)" + echo " Warning: sdk-pkg.json not found, using default 5.0.1(13)" +fi + +cat > "${PROJECT_ROOT}/local.properties" < "${BUILD_PROFILE}" < Updating app version to ${APP_VERSION} ..." + +APP_JSON5="${PROJECT_ROOT}/AppScope/app.json5" +sed -i.bak "s/\"versionName\": \"[^\"]*\"/\"versionName\": \"${APP_VERSION}\"/" "${APP_JSON5}" +sed -i.bak "s/\"versionCode\": [0-9]*/\"versionCode\": ${VERSION_CODE}/" "${APP_JSON5}" +rm -f "${APP_JSON5}.bak" + +ROOT_PKG="${PROJECT_ROOT}/oh-package.json5" +sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"${APP_VERSION}\"/" "${ROOT_PKG}" +rm -f "${ROOT_PKG}.bak" + +ENTRY_PKG="${PROJECT_ROOT}/entry/oh-package.json5" +sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"${APP_VERSION}\"/" "${ENTRY_PKG}" +rm -f "${ENTRY_PKG}.bak" + +echo " ✓ app.json5: versionName=${APP_VERSION}, versionCode=${VERSION_CODE}" + +# --- Generate hvigor-config.json5 ------------------------------------------- + +echo "==> Configuring hvigor-config.json5 ..." + +HVIGOR_CONFIG="${PROJECT_ROOT}/hvigor/hvigor-config.json5" + +BUNDLED_HVIGOR_DIR="${COMMANDLINE_TOOLS}/hvigor/hvigor" +BUNDLED_PLUGIN_DIR="${COMMANDLINE_TOOLS}/hvigor/hvigor-ohos-plugin" + +if [ -d "${BUNDLED_PLUGIN_DIR}" ]; then + cat > "${HVIGOR_CONFIG}" < "${NPMRC_FILE}" <<'NPMRC' +@ohos:registry=https://repo.harmonyos.com/npm/ +registry=https://registry.npmjs.org/ +NPMRC +echo " ✓ Created ${NPMRC_FILE}" + +# --- Install ohpm dependencies ---------------------------------------------- + +echo "==> Installing ohpm dependencies ..." +cd "${PROJECT_ROOT}" +"${OHPM}" install + +# --- Build the unsigned APP ------------------------------------------------- + +echo "==> Building unsigned APP (${BUILD_MODE}) ..." + +if [ "${BUILD_MODE}" = "debug" ]; then + "${HVIGORW}" assembleApp -p product=default \ + -p buildMode=debug -p enableSignTask=false --no-daemon +else + "${HVIGORW}" assembleApp -p product=default \ + -p buildMode=release -p enableSignTask=false --no-daemon +fi + +# --- Locate the unsigned APP ------------------------------------------------ + +APP_OUTPUT_DIR="${PROJECT_ROOT}/build/outputs/default" +UNSIGNED_APP=$(find "${APP_OUTPUT_DIR}" -name "*.app" -type f 2>/dev/null | head -1) + +if [ -z "${UNSIGNED_APP}" ]; then + echo " ✗ No .app file found in ${APP_OUTPUT_DIR}" + echo " Searching entire build tree ..." + UNSIGNED_APP=$(find "${PROJECT_ROOT}/build" -name "*.app" -type f 2>/dev/null | head -1) + if [ -z "${UNSIGNED_APP}" ]; then + echo " ✗ No .app file found anywhere in build/" + exit 1 + fi +fi + +echo " ✓ Unsigned APP: ${UNSIGNED_APP} ($(du -h "${UNSIGNED_APP}" | cut -f1))" + +# --- Sign the APP with hap-sign-tool.jar ------------------------------------ + +echo "==> Signing APP with hap-sign-tool.jar ..." + +SIGN_TOOL_JAR="${OHOS_SDK_HOME}/default/openharmony/toolchains/lib/hap-sign-tool.jar" +if [ ! -f "${SIGN_TOOL_JAR}" ]; then + SIGN_TOOL_JAR=$(find "${OHOS_SDK_HOME}" -name "hap-sign-tool.jar" -type f 2>/dev/null | head -1) +fi +if [ ! -f "${SIGN_TOOL_JAR}" ]; then + echo " ✗ hap-sign-tool.jar not found in SDK" + exit 1 +fi + +SIGNED_APP="${UNSIGNED_APP%.app}-signed.app" + +java -jar "${SIGN_TOOL_JAR}" sign-app \ + -mode localSign \ + -keyAlias "${KEY_ALIAS}" \ + -keyPwd "${KEY_PASSWORD}" \ + -appCertFile "${CERT_PATH}" \ + -profileFile "${PROFILE_PATH}" \ + -inFile "${UNSIGNED_APP}" \ + -signAlg SHA256withECDSA \ + -keystoreFile "${KEYSTORE_PATH}" \ + -keystorePwd "${KEYSTORE_PASSWORD}" \ + -outFile "${SIGNED_APP}" + +if [ ! -f "${SIGNED_APP}" ]; then + echo " ✗ Signing failed — no signed APP produced" + exit 1 +fi + +mv -f "${SIGNED_APP}" "${UNSIGNED_APP}" +APP_FILE="${UNSIGNED_APP}" + +echo " ✓ Signed APP: ${APP_FILE} ($(du -h "${APP_FILE}" | cut -f1))" + +# --- Verify APP contents ------------------------------------------------------ + +echo "==> Verifying APP contents ..." + +VERIFY_TMPDIR=$(mktemp -d) +trap 'rm -rf "${VERIFY_TMPDIR}"' EXIT + +unzip -q "${APP_FILE}" -d "${VERIFY_TMPDIR}" +HAP_IN_APP=$(find "${VERIFY_TMPDIR}" -name "*.hap" -type f | head -1) +if [ -z "${HAP_IN_APP}" ]; then + echo " ✗ No .hap inside the .app" + exit 1 +fi + +HAP_DIR="${VERIFY_TMPDIR}/hap" +unzip -q "${HAP_IN_APP}" -d "${HAP_DIR}" + +ERRORS="" +check_file() { + if [ ! -f "$1" ]; then + ERRORS="${ERRORS}\n ✗ MISSING: $2" + else + echo " ✓ $2 ($(du -h "$1" | cut -f1))" + fi +} + +check_file "${HAP_DIR}/libs/arm64-v8a/libnode.so" "libs/arm64-v8a/libnode.so" +check_file "${HAP_DIR}/libs/arm64-v8a/libnode_launcher.so" "libs/arm64-v8a/libnode_launcher.so" +check_file "${HAP_DIR}/libs/arm64-v8a/libnode_ctl.so" "libs/arm64-v8a/libnode_ctl.so" +check_file "${HAP_DIR}/resources/resfile/electerm/index.js" "resfile/electerm/index.js" +check_file "${HAP_DIR}/resources/resfile/electerm/app.bundle.mjs" "resfile/electerm/app.bundle.mjs" +check_file "${HAP_DIR}/resources/resfile/electerm/views/index.pug" "resfile/electerm/views/index.pug" + +JS_COUNT=$(find "${HAP_DIR}/resources/resfile/electerm/dist/assets/js" -name "*.js" 2>/dev/null | wc -l | tr -d ' ') +echo " ✓ resfile/electerm/dist/assets/js: ${JS_COUNT} files" +if [ "${JS_COUNT}" = "0" ]; then + ERRORS="${ERRORS}\n ✗ No frontend JS in resfile" +fi + +if [ -n "${ERRORS}" ]; then + echo -e "::error::APP content verification failed:${ERRORS}" + exit 1 +fi +echo "✓ All critical files verified in APP" + +echo "==> Build complete: ${APP_FILE}" diff --git a/scripts/prepare-electron-runtime.sh b/scripts/prepare-electron-runtime.sh deleted file mode 100755 index 02ca69d..0000000 --- a/scripts/prepare-electron-runtime.sh +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env bash -# prepare-electron-runtime.sh — Extract the pre-built Electron 鸿蒙 runtime -# from a tarball and install it into the project. -# -# The Electron 鸿蒙 runtime is provided as a pre-built tarball by the -# openharmony-sig/electron project. It contains: -# -# - web_engine/ — A complete HarmonyOS HAR module (ArkTS source + resfile -# resources + libadapter.so type definitions). This module -# provides WebAbility, WebAbilityStage, WebWindow, and -# JsBindingUtils — the ArkTS API for the Electron runtime. -# - electron/libs/arm64-v8a/ — Native .so libraries: -# libelectron.so (Chromium + Node.js + V8), -# libadapter.so (HarmonyOS ↔ Electron bridge), -# libffmpeg.so, libc++_shared.so, libvk_swiftshader.so, -# vscode-sqlite3.node -# -# After extraction: -# - web_engine/ is placed at the project root (as a sibling of entry/) -# - .so files are placed in entry/libs/arm64-v8a/ -# -# Usage: -# ./scripts/prepare-electron-runtime.sh -# -# Environment variables (ONE of these must be set): -# ELECTRON_RUNTIME_URL — URL to the tarball (e.g. .tar.gz or .zip) -# Used in CI. The tarball must extract to a directory -# containing web_engine/ and electron/libs/. -# ELECTRON_RUNTIME_DIR — Path to an already-extracted tarball directory -# (for local development) -# ELECTRON_RUNTIME_FILE — Path to a local tarball file -# (for local development) -# -# Example tarball: -# electron40_hap_electron_v40.0.0_20260629.tar.gz -# → extracts to electron144_ohos_hap/ -# ├── electron/libs/arm64-v8a/*.so -# └── web_engine/ (complete HAR module) - -set -euo pipefail - -# --- Config ----------------------------------------------------------------- - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -LIBS_DIR="${PROJECT_ROOT}/entry/libs/arm64-v8a" -WEB_ENGINE_DIR="${PROJECT_ROOT}/web_engine" -DOWNLOAD_DIR="${PROJECT_ROOT}/.cache" - -# --- Functions -------------------------------------------------------------- - -die() { - echo " ERROR: $*" >&2 - exit 1 -} - -info() { - echo " $*" -} - -ok() { - echo " [OK] $*" -} - -# Find the top-level extracted directory (e.g. electron144_ohos_hap/) -find_extracted_root() { - local dir="$1" - # Check if web_engine/ is directly in the directory - if [ -d "${dir}/web_engine" ]; then - echo "${dir}" - return 0 - fi - # Search one level deep - local found - found=$(find "${dir}" -maxdepth 2 -name "web_engine" -type d 2>/dev/null | head -1) - if [ -n "${found}" ]; then - dirname "${found}" - else - echo "" - fi -} - -# Install runtime files from an extracted directory. -install_from_dir() { - local extracted_root="$1" - info "Installing runtime from: ${extracted_root}" - - # Verify structure - if [ ! -d "${extracted_root}/web_engine" ]; then - die "web_engine/ directory not found in: ${extracted_root}" - fi - - local so_dir="${extracted_root}/electron/libs/arm64-v8a" - if [ ! -d "${so_dir}" ]; then - # Try alternative: some tarballs may have libs at a different path - so_dir=$(find "${extracted_root}" -path "*/arm64-v8a/libelectron.so" -exec dirname {} \; 2>/dev/null | head -1) - if [ -z "${so_dir}" ]; then - die "Could not find arm64-v8a/ directory with libelectron.so" - fi - fi - - # --- Copy web_engine/ module to project root --- - info "Installing web_engine module ..." - rm -rf "${WEB_ENGINE_DIR}" - cp -r "${extracted_root}/web_engine" "${WEB_ENGINE_DIR}" - ok "web_engine/ installed ($(du -sh "${WEB_ENGINE_DIR}" | cut -f1))" - - # --- Copy .so libraries to entry/libs/arm64-v8a/ --- - info "Installing native libraries ..." - mkdir -p "${LIBS_DIR}" - rm -f "${LIBS_DIR}"/*.so "${LIBS_DIR}"/*.node - cp -f "${so_dir}"/*.so "${LIBS_DIR}/" 2>/dev/null || true - cp -f "${so_dir}"/*.node "${LIBS_DIR}/" 2>/dev/null || true - ok "Libraries installed to ${LIBS_DIR}/" - for f in "${LIBS_DIR}"/*; do - ok " $(basename "${f}") ($(du -h "${f}" | cut -f1))" - done -} - -# Download and extract archive, then install. -download_and_install() { - local url="$1" - info "Downloading Electron runtime from: ${url}" - - mkdir -p "${DOWNLOAD_DIR}" - - local archive="${DOWNLOAD_DIR}/electron-runtime" - local extract_dir="${DOWNLOAD_DIR}/electron-runtime-extracted" - rm -rf "${extract_dir}" - mkdir -p "${extract_dir}" - - case "${url}" in - *.tar.gz|*.tgz) - archive="${archive}.tar.gz" - curl -L --fail --retry 3 --retry-delay 5 -o "${archive}" "${url}" - tar -xzf "${archive}" -C "${extract_dir}" - ;; - *.zip) - archive="${archive}.zip" - curl -L --fail --retry 3 --retry-delay 5 -o "${archive}" "${url}" - if command -v unzip &>/dev/null; then - unzip -q -o "${archive}" -d "${extract_dir}" - else - die "unzip command not found — please install unzip" - fi - ;; - *) - die "Unsupported archive format. URL must end in .tar.gz or .zip" - ;; - esac - - if [ ! -s "${archive}" ]; then - die "Download failed — archive is empty or missing" - fi - - ok "Downloaded and extracted runtime archive" - - local extracted_root - extracted_root=$(find_extracted_root "${extract_dir}") - if [ -z "${extracted_root}" ]; then - die "Could not find web_engine/ in extracted archive" - fi - info "Extracted root: ${extracted_root}" - - install_from_dir "${extracted_root}" - - # Clean up - rm -f "${archive}" - rm -rf "${extract_dir}" -} - -# Extract a local tarball file and install. -extract_file_and_install() { - local filepath="$1" - info "Extracting local tarball: ${filepath}" - - if [ ! -f "${filepath}" ]; then - die "File not found: ${filepath}" - fi - - local extract_dir="${DOWNLOAD_DIR}/electron-runtime-extracted" - rm -rf "${extract_dir}" - mkdir -p "${extract_dir}" - - case "${filepath}" in - *.tar.gz|*.tgz) - tar -xzf "${filepath}" -C "${extract_dir}" - ;; - *.zip) - if command -v unzip &>/dev/null; then - unzip -q -o "${filepath}" -d "${extract_dir}" - else - die "unzip command not found — please install unzip" - fi - ;; - *) - die "Unsupported archive format. File must be .tar.gz or .zip" - ;; - esac - - ok "Extracted tarball" - - local extracted_root - extracted_root=$(find_extracted_root "${extract_dir}") - if [ -z "${extracted_root}" ]; then - die "Could not find web_engine/ in extracted archive" - fi - info "Extracted root: ${extracted_root}" - - install_from_dir "${extracted_root}" - - # Clean up - rm -rf "${extract_dir}" -} - -# --- Main ------------------------------------------------------------------- - -echo "==> Preparing Electron runtime" - -# Check that at least one source is provided -if [ -z "${ELECTRON_RUNTIME_URL:-}" ] && [ -z "${ELECTRON_RUNTIME_DIR:-}" ] && [ -z "${ELECTRON_RUNTIME_FILE:-}" ]; then - echo "" - echo " ERROR: None of ELECTRON_RUNTIME_URL, ELECTRON_RUNTIME_DIR, or" - echo " ELECTRON_RUNTIME_FILE is set." - echo "" - echo " The pre-built Electron 鸿蒙 runtime tarball must be obtained from:" - echo "" - echo " 1. openharmony-sig/electron project (Huawei Cloud CodeHub):" - echo " https://gitcode.com/openharmony-sig/electron" - echo " Download the latest release tarball (e.g." - echo " electron40_hap_electron_v40.0.0_20260629.tar.gz)" - echo "" - echo " Then use ONE of:" - echo " export ELECTRON_RUNTIME_FILE=/path/to/electron40_hap_*.tar.gz" - echo " export ELECTRON_RUNTIME_DIR=/path/to/extracted/electron144_ohos_hap" - echo " export ELECTRON_RUNTIME_URL=https://your-host/electron40_hap_*.tar.gz (for CI)" - echo "" - exit 1 -fi - -if [ -n "${ELECTRON_RUNTIME_FILE:-}" ]; then - # Mode 1: Use a local tarball file - extract_file_and_install "${ELECTRON_RUNTIME_FILE}" -elif [ -n "${ELECTRON_RUNTIME_DIR:-}" ]; then - # Mode 2: Use an already-extracted directory - if [ ! -d "${ELECTRON_RUNTIME_DIR}" ]; then - die "ELECTRON_RUNTIME_DIR does not exist: ${ELECTRON_RUNTIME_DIR}" - fi - info "Using local directory: ${ELECTRON_RUNTIME_DIR}" - install_from_dir "${ELECTRON_RUNTIME_DIR}" -elif [ -n "${ELECTRON_RUNTIME_URL:-}" ]; then - # Mode 3: Download from a URL - download_and_install "${ELECTRON_RUNTIME_URL}" -fi - -# --- Verify --- -echo "" -echo "==> Verifying runtime files" - -required_libs=("libelectron.so" "libadapter.so" "libffmpeg.so") -for lib in "${required_libs[@]}"; do - if [ ! -f "${LIBS_DIR}/${lib}" ]; then - die "Missing required library: ${LIBS_DIR}/${lib}" - fi - ok "Found ${lib}" -done - -if [ ! -f "${WEB_ENGINE_DIR}/Index.ets" ]; then - die "Missing web_engine/Index.ets" -fi -ok "Found web_engine/Index.ets" - -if [ ! -f "${WEB_ENGINE_DIR}/oh-package.json5" ]; then - die "Missing web_engine/oh-package.json5" -fi -ok "Found web_engine/oh-package.json5" - -# Verify resfile resources exist -RESFILE_DIR="${WEB_ENGINE_DIR}/src/main/resources/resfile" -for res in icudtl.dat resources.pak chrome_100_percent.pak v8_context_snapshot.bin; do - if [ ! -f "${RESFILE_DIR}/${res}" ]; then - info "Note: ${res} not found (may be optional)" - else - ok "Found ${res}" - fi -done - -if [ -d "${RESFILE_DIR}/locales" ]; then - ok "Found locales/ ($(ls "${RESFILE_DIR}/locales/" | wc -l) files)" -fi - -echo "" -echo "==> Electron runtime preparation complete!" -echo " web_engine module: ${WEB_ENGINE_DIR}/" -echo " Native libraries: ${LIBS_DIR}/" -echo "" -echo " Next steps:" -echo " 1. ./scripts/prepare-web.sh (build web app → web_engine resfile)" -echo " 2. ./scripts/build-app.sh (build & sign the HAP)" diff --git a/scripts/prepare-node.sh b/scripts/prepare-node.sh new file mode 100755 index 0000000..cde3876 --- /dev/null +++ b/scripts/prepare-node.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# prepare-node.sh — Download the OpenHarmony Node.js runtime (hqzing/ohos-node) +# and install it into the entry module as a native "library". +# +# The node binary is a musl PIE executable for aarch64-ohos. It is installed +# as entry/libs/arm64-v8a/libnode.so because: +# - files in the HAP libs directory are installed to the app's (executable) +# native lib dir, so a child process can execv() it without any runtime +# extraction step — rawfile/resfile content would need copying to filesDir +# first, and filesDir may be mounted noexec; +# - hvigor packages entry/libs//*.so into the HAP automatically. +# +# The binary is stripped (when an OHOS llvm-strip is available) to drop the +# ~60MB of debug info that hqzing builds ship with. +# +# Usage: +# ./scripts/prepare-node.sh +# +# Environment variables: +# NODE_VERSION — which hqzing/ohos-node release to use (default 24.19.0) +# OHOS_SDK_HOME — SDK root (for llvm-strip; stripping is skipped if unset) +# NODE_DIST_MIRROR — override the download mirror (default: GitHub releases) +set -euo pipefail + +# --- Config ----------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +NODE_VERSION="${NODE_VERSION:-24.19.0}" +NODE_DIST_MIRROR="${NODE_DIST_MIRROR:-https://github.com/hqzing/ohos-node/releases/download}" +ARCHIVE_NAME="node-v${NODE_VERSION}-openharmony-arm64.tar.xz" +DOWNLOAD_URL="${NODE_DIST_MIRROR}/v${NODE_VERSION}/${ARCHIVE_NAME}" + +CACHE_DIR="${PROJECT_ROOT}/.cache/node-runtime" +ARCHIVE_PATH="${CACHE_DIR}/${ARCHIVE_NAME}" +LIBS_DIR="${PROJECT_ROOT}/entry/libs/arm64-v8a" +OUT_BIN="${LIBS_DIR}/libnode.so" + +# --- Main ------------------------------------------------------------------- + +echo "==> Preparing OpenHarmony Node.js runtime (hqzing/ohos-node v${NODE_VERSION})" + +mkdir -p "${CACHE_DIR}" "${LIBS_DIR}" + +MARKER_FILE="${CACHE_DIR}/installed-v${NODE_VERSION}.marker" + +if [ -f "${OUT_BIN}" ] && [ -f "${MARKER_FILE}" ]; then + echo " ✓ libnode.so already prepared (v${NODE_VERSION}), skipping." + exit 0 +fi + +# 1. Download (cached) +if [ ! -s "${ARCHIVE_PATH}" ]; then + echo " Downloading ${DOWNLOAD_URL} ..." + curl -fL --retry 5 --retry-all-errors --retry-delay 5 \ + -o "${ARCHIVE_PATH}.tmp" "${DOWNLOAD_URL}" + mv "${ARCHIVE_PATH}.tmp" "${ARCHIVE_PATH}" +else + echo " ✓ Using cached archive: ${ARCHIVE_PATH}" +fi + +# 2. Extract just the node binary (skip npm/corepack/man — not needed on device) +echo " Extracting node binary ..." +rm -rf "${CACHE_DIR}/extract" +mkdir -p "${CACHE_DIR}/extract" +tar -xJf "${ARCHIVE_PATH}" -C "${CACHE_DIR}/extract" \ + --strip-components=1 "node-v${NODE_VERSION}-openharmony-arm64/bin/node" + +NODE_EXTRACTED="${CACHE_DIR}/extract/bin/node" +if [ ! -f "${NODE_EXTRACTED}" ]; then + echo " ✗ node binary not found in archive" + exit 1 +fi + +file "${NODE_EXTRACTED}" || true + +# 3. Strip debug info with the OHOS toolchain's llvm-strip (optional) +STRIP_BIN="" +for candidate in \ + "${OHOS_SDK_HOME:-}/default/openharmony/native/llvm/bin/llvm-strip" \ + "${OHOS_SDK_HOME:-}/native/llvm/bin/llvm-strip"; do + if [ -x "${candidate}" ]; then + STRIP_BIN="${candidate}" + break + fi +done +# Fall back to PATH llvm-strip (same target-independence: strip only removes +# sections, it does not need to understand the target ABI). +if [ -z "${STRIP_BIN}" ]; then + STRIP_BIN="$(command -v llvm-strip || true)" +fi + +cp "${NODE_EXTRACTED}" "${OUT_BIN}.tmp" +if [ -n "${STRIP_BIN}" ]; then + echo " Stripping with ${STRIP_BIN} ..." + "${STRIP_BIN}" "${OUT_BIN}.tmp" || echo " ⚠ strip failed, keeping unstripped binary" +else + echo " ⚠ no llvm-strip found, keeping unstripped binary" +fi +mv "${OUT_BIN}.tmp" "${OUT_BIN}" + +echo "${NODE_VERSION}" > "${MARKER_FILE}" +echo "${DOWNLOAD_URL}" > "${CACHE_DIR}/node-source-url.txt" +# NOTE: only the .so lives in entry/libs — hvigor strips every file there +# and rejects non-object files. + +echo " ✓ Installed: ${OUT_BIN} ($(du -h "${OUT_BIN}" | cut -f1))" +echo "==> Node.js runtime preparation complete." diff --git a/scripts/prepare-web.sh b/scripts/prepare-web.sh index ccd24af..7c44e7b 100755 --- a/scripts/prepare-web.sh +++ b/scripts/prepare-web.sh @@ -1,26 +1,22 @@ #!/usr/bin/env bash -# prepare-web.sh — Install, build, and bundle the electerm app -# from the project root into the HarmonyOS app's resfile resources. +# prepare-web.sh — Install deps and build the electerm web app (frontend + +# pure-node backend) into the entry module's resfile directory. # -# This script: -# 1. Installs npm dependencies in the project root (dev deps for build tools) -# 2. Runs build/harmony/build.js which: -# - Copies @electerm/electerm-react/client → src/client/ (gitignored) -# - Runs `npm run b` (complete electerm build: clean + compile + prepare-file) -# - Applies HarmonyOS delta (main → bootstrap.js, remove native modules) -# - Copies work/app/ → web_engine/src/main/resources/resfile/resources/app/ +# Runs build/web/build.mjs which: +# 1. vite-builds the frontend -> resfile/electerm/dist/assets +# 2. copies static assets + views/index.pug +# 3. esbuild-bundles the backend -> resfile/electerm/app.bundle.mjs +# 4. writes resfile/electerm/index.js (runtime env setup) + package.json # -# Key points: -# - Reuses electerm's full build pipeline (npm run b), only adds harmony delta -# - Native modules (node-pty, serialport, cpu-features) are removed post-build -# - The app entry is bootstrap.js (sets DATA_PATH before loading app.js) +# The resfile directory is packaged into the HAP and read directly by the +# on-device node process — no runtime extraction. # # Usage: # ./scripts/prepare-web.sh # # Environment variables: -# OHOS_SERVER_SECRET — sets SERVER_SECRET in .env (optional) - +# SERVER_SECRET / OHOS_SERVER_SECRET — JWT secret baked into the build +# (required in CI, optional locally) set -euo pipefail # --- Config ----------------------------------------------------------------- @@ -28,90 +24,53 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -# Web app source (project root) -WEB_SRC_DIR="${PROJECT_ROOT}" -# Output: web_engine/src/main/resources/resfile/resources/app/ -# (Electron app directory in the web_engine HAR module's resfile) -RESFILE_APP_DIR="${PROJECT_ROOT}/web_engine/src/main/resources/resfile/resources/app" +RESFILE_APP_DIR="${PROJECT_ROOT}/entry/src/main/resources/resfile/electerm" # --- Main ------------------------------------------------------------------- -echo "==> Preparing electerm app (from project root: ${WEB_SRC_DIR})" +echo "==> Preparing electerm web app (from project root: ${PROJECT_ROOT})" -if [ ! -f "${WEB_SRC_DIR}/package.json" ]; then - echo " ✗ package.json not found at ${WEB_SRC_DIR}/package.json" - exit 1 -fi +cd "${PROJECT_ROOT}" -cd "${WEB_SRC_DIR}" - -# Print version for traceability APP_VERSION=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "unknown") echo " Version: ${APP_VERSION}" -# Install dependencies (needed for vite, pug, shelljs, and static asset packages) -# --ignore-scripts prevents native module compilation (electron-rebuild etc.) +# Install dependencies (frontend build tools + backend runtime deps). +# --ignore-scripts avoids native module compilation for the host platform +# (node-pty, serialport — not needed, they are esbuild externals). echo " Installing dependencies ..." -npm install --legacy-peer-deps --ignore-scripts +npm ci --legacy-peer-deps --ignore-scripts || { + echo " npm ci failed, falling back to npm install ..." + npm install --legacy-peer-deps --ignore-scripts +} -# Create .env from .sample.env if it exists (needed by build/vite/common.js for dotenv) -if [ -f ".sample.env" ]; then - echo " Creating .env ..." - cp .sample.env .env -fi - -# Set SERVER_SECRET from CI env var (optional) -# Use printf + grep -v + append to avoid sed delimiter issues -# with base64 secrets that may contain / or & characters. -if [ -n "${OHOS_SERVER_SECRET:-}" ]; then - echo " Setting SERVER_SECRET from OHOS_SERVER_SECRET ..." - grep -v '^SERVER_SECRET=' .env > .env.tmp 2>/dev/null || true - printf 'SERVER_SECRET=%s\n' "${OHOS_SERVER_SECRET}" >> .env.tmp - mv .env.tmp .env -fi - -# Run the HarmonyOS build script (vite + copy source + install deps + copy to resfile) -echo " Building HarmonyOS electerm app (direct source mode) ..." -npm run build:harmony +# Build frontend + backend into entry resfile +echo " Building electerm web app ..." +npm run build:web # --- Verify output --- if [ ! -d "${RESFILE_APP_DIR}" ]; then echo " ✗ Build output not found at ${RESFILE_APP_DIR}" - echo " Run node build/harmony/build.js manually to check for errors." - exit 1 -fi - -# Verify bootstrap.js (HarmonyOS Electron main process entry) -if [ ! -f "${RESFILE_APP_DIR}/bootstrap.js" ]; then - echo " ✗ Missing: ${RESFILE_APP_DIR}/bootstrap.js" - exit 1 -fi -echo " ✓ Found: bootstrap.js" - -# Verify app.js (loaded by bootstrap.js after paths are ready) -if [ ! -f "${RESFILE_APP_DIR}/app.js" ]; then - echo " ✗ Missing: ${RESFILE_APP_DIR}/app.js" + echo " Run node build/web/build.mjs manually to check for errors." exit 1 fi -echo " ✓ Found: app.js" -# Verify package.json -if [ ! -f "${RESFILE_APP_DIR}/package.json" ]; then - echo " ✗ Missing: ${RESFILE_APP_DIR}/package.json" +for f in "index.js" "app.bundle.mjs" "package.json" "views/index.pug"; do + if [ ! -f "${RESFILE_APP_DIR}/${f}" ]; then + echo " ✗ Missing: ${RESFILE_APP_DIR}/${f}" + exit 1 + fi + echo " ✓ Found: ${f}" +done + +JS_COUNT=$(find "${RESFILE_APP_DIR}/dist/assets/js" -name "*.js" 2>/dev/null | wc -l | tr -d ' ') +CSS_COUNT=$(find "${RESFILE_APP_DIR}/dist/assets/css" -name "*.css" 2>/dev/null | wc -l | tr -d ' ') +echo " ✓ dist/assets/js: ${JS_COUNT} files, dist/assets/css: ${CSS_COUNT} files" +if [ "${JS_COUNT}" = "0" ] || [ "${CSS_COUNT}" = "0" ]; then + echo " ✗ Frontend assets missing" exit 1 fi -echo " ✓ Found: package.json" - -# Verify node_modules -if [ ! -d "${RESFILE_APP_DIR}/node_modules" ]; then - echo " ✗ Missing: node_modules/" - exit 1 -fi -echo " ✓ Found: node_modules/" - -# Remove any .env file — not needed in the Electron app -rm -f "${RESFILE_APP_DIR}/.env" echo " ✓ App size: $(du -sh "${RESFILE_APP_DIR}" | cut -f1)" echo "==> Web app preparation complete." diff --git a/src/app/app.js b/src/app/app.js index e2a1bf6..b6fcd78 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -1,13 +1,26 @@ /** * app entry */ -const log = require('./common/log') -const { createApp } = require('./lib/create-app') -const globalState = require('./lib/glob-state') -globalState.set('initTime', Date.now()) +import log from './common/log.js' +import { createApp } from './server/server.js' -log.debug('electerm start') +process.on('uncaughtException', (err) => { + log.error('uncaughtException', err) +}) +process.on('unhandledRejection', (err) => { + log.error('unhandledRejection', err) +}) -const app = createApp() -globalState.set('app', app) +async function main () { + log.info('app start') + const app = await createApp() + + const { HOST, PORT } = process.env + + app.listen(PORT, HOST, () => { + log.info(`server runs on http://${HOST}:${PORT}`) + }) +} + +main() diff --git a/src/app/bootstrap.js b/src/app/bootstrap.js deleted file mode 100644 index 0132142..0000000 --- a/src/app/bootstrap.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * bootstrap.js — HarmonyOS entry point. - * - * AbilityStage.ets writes the sandbox filesDir path to a marker file - * (.electerm-data-path) before the Electron runtime starts. This file - * reads that marker and sets process.env.DATA_PATH so all downstream - * modules use the sandbox directory for data storage (nedb, config, logs). - * - * EntryAbility.ets requests READ_WRITE_DOCUMENTS_DIRECTORY at runtime, - * then writes the Documents directory path to a second marker file - * (.electerm-documents-path). This file reads that marker and overrides - * os.homedir() to return the Documents folder — so that file save - * dialogs, SFTP local paths, and other home-directory-based operations - * default to the user-visible Documents folder. - * - * DATA_PATH resolution order: - * 1. Marker file (.electerm-data-path, written by AbilityStage.ets → sandbox filesDir) - * 2. Derived sandbox filesDir (from __dirname) - * 3. /data/local/tmp or os.tmpdir() — absolute last resort - * - * HOMEDIR_PATH resolution order: - * 1. Marker file (.electerm-documents-path, written by EntryAbility.ets → Documents dir) - * 2. Original os.homedir() (system default) - */ -const fs = require('fs') -const path = require('path') -const os = require('os') - -function deriveSandboxFilesDir () { - // __dirname is like: /data/storage/el1/bundle/entry/resources/resfile/resources/app - // sandbox filesDir is like: /data/storage/el2/base/haps/entry/files - const m = __dirname.match(/^(.+?)\/el1\/bundle\/([^/]+)/) - if (m) { - return `${m[1]}/el2/base/haps/${m[2]}/files` - } - return null -} - -/** - * Resolve DATA_PATH — the sandbox filesDir used for app data storage. - * This is always the sandbox directory, NOT the user-visible Documents - * folder. The sandbox is always writable and doesn't require runtime - * permission requests. - */ -function getDataPath () { - const derivedDir = deriveSandboxFilesDir() - - if (derivedDir) { - // 1. Try reading the marker file written by AbilityStage.ets - const markerPath = path.join(derivedDir, '.electerm-data-path') - try { - const data = fs.readFileSync(markerPath, 'utf8').trim() - if (data) { - return data - } - } catch (e) { /* ignore */ } - - // 2. Use the derived sandbox filesDir directly - try { - fs.mkdirSync(derivedDir, { recursive: true }) - return derivedDir - } catch (e) { /* ignore */ } - } - - // 3. Final fallback — try /data/local/tmp, then os.tmpdir() - const fallbacks = ['/data/local/tmp', os.tmpdir()] - for (const dir of fallbacks) { - try { - fs.mkdirSync(dir, { recursive: true }) - return dir - } catch (e) { /* ignore */ } - } - - return os.tmpdir() -} - -/** - * Resolve HOMEDIR_PATH — the user-visible Documents directory. - * This is used to override os.homedir() so that file save dialogs, - * SFTP local paths, and other home-directory-based operations default - * to the Documents folder visible to users. - * - * If the Documents path marker is not available (permission denied), - * falls back to the original os.homedir() value. - */ -function getHomedirPath () { - const derivedDir = deriveSandboxFilesDir() - - if (derivedDir) { - const markerPath = path.join(derivedDir, '.electerm-documents-path') - try { - const data = fs.readFileSync(markerPath, 'utf8').trim() - if (data) { - return data - } - } catch (e) { /* ignore */ } - } - - // Fallback: try os.homedir() + '/Documents' - const docsPath = path.join(os.homedir(), 'Documents') - try { - fs.mkdirSync(docsPath, { recursive: true }) - const testFile = path.join(docsPath, '.write-test') - fs.writeFileSync(testFile, 'ok') - fs.unlinkSync(testFile) - return docsPath - } catch (e) { /* ignore */ } - - // Final fallback: original os.homedir() - return os.homedir() -} - -process.env.DATA_PATH = getDataPath() - -// ── Override os.homedir() ────────────────────────────────────────── -// On HarmonyOS the default os.homedir() returns an inaccessible path -// (e.g. /storage/Users/currentUser). We override it to return the -// user-visible Documents directory, so that file save dialogs, SFTP -// local paths, and other home-directory-based operations work -// correctly for the user. -// -// DATA_PATH (sandbox filesDir) is used for internal app data storage -// and is NOT exposed as the home directory. -const _originalHomedir = os.homedir.bind(os) -const _homedirPath = getHomedirPath() -os.homedir = function homedir () { - return _homedirPath || _originalHomedir() -} - -require('./app.js') diff --git a/src/app/common/app-props.js b/src/app/common/app-props.js deleted file mode 100644 index 2ae1ca1..0000000 --- a/src/app/common/app-props.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * app path — HarmonyOS only. - * - * bootstrap.js sets process.env.DATA_PATH (the app's sandbox data - * directory) and overrides os.homedir() before loading app.js, - * so this module simply uses it as the base for all derived paths. - */ -const { resolve } = require('path') -const fs = require('fs') -const constants = require('./runtime-constants') - -function getAppDataPath () { - return process.env.DATA_PATH || resolve(__dirname, '../../data') -} - -const appDataPath = getAppDataPath() -const sshKeysPath = resolve(appDataPath, '.ssh') -// Create immediately so SSH key reads/writes never fail on a missing dir. -try { fs.mkdirSync(sshKeysPath, { recursive: true, mode: 0o700 }) } catch {} - -module.exports = { - appPath: appDataPath, - isPortable: false, - exePath: '', - sshKeysPath, - homeOrTmp: constants.homeDir, - ...constants -} diff --git a/src/app/common/bookmark-zod-schemas.js b/src/app/common/bookmark-zod-schemas.js index 5a4d1f1..c52b859 100644 --- a/src/app/common/bookmark-zod-schemas.js +++ b/src/app/common/bookmark-zod-schemas.js @@ -1,4 +1,4 @@ -const { z } = require('../lib/zod') +import { z } from '../lib/zod.js' const runScriptSchema = z.object({ delay: z.number().optional().describe('Delay in ms before executing this command'), @@ -117,7 +117,7 @@ const localBookmarkSchema = { // execLinuxArgs: z.array(z.string()).optional().describe('Linux exec arguments') } -module.exports = { +export { runScriptSchema, quickCommandSchema, sshTunnelSchema, diff --git a/src/app/common/build-run-scripts.js b/src/app/common/build-run-scripts.js index 2510ef0..8f0b813 100644 --- a/src/app/common/build-run-scripts.js +++ b/src/app/common/build-run-scripts.js @@ -1,4 +1,4 @@ -exports.buildRunScripts = function (inst) { +export const buildRunScripts = function (inst) { return [{ delay: inst.loginScriptDelay || 0, script: inst.loginScript diff --git a/src/app/common/build-ssh-tunnel.js b/src/app/common/build-ssh-tunnel.js index 4b1e587..7514aae 100644 --- a/src/app/common/build-ssh-tunnel.js +++ b/src/app/common/build-ssh-tunnel.js @@ -1,4 +1,4 @@ -exports.buildSshTunnels = function (inst) { +export const buildSshTunnels = function (inst) { return [{ sshTunnel: inst.sshTunnel, sshTunnelRemotePort: inst.sshTunnelRemotePort, diff --git a/src/app/common/config-default.js b/src/app/common/config-default.js index 077709f..e11247b 100644 --- a/src/app/common/config-default.js +++ b/src/app/common/config-default.js @@ -1,6 +1,6 @@ -const defaultSettings = require('./default-setting') +import defaultSettings from './default-setting.js' -module.exports = exports.default = { +export default { keepaliveInterval: 10000, rightClickSelectsWord: false, pasteWhenContextMenu: false, @@ -17,7 +17,18 @@ module.exports = exports.default = { autoSyncInterval: 0, autoSyncDirection: 'upload' }, + terminalTypes: [ + 'xterm-256color', + 'xterm-new', + 'xterm-color', + 'xterm-vt220', + 'xterm', + 'linux', + 'vt100', + 'ansi', + 'rxvt' + ], + host: '127.0.0.1', keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode', - - host: '127.0.0.1' + enableSixel: true } diff --git a/src/app/common/constants.js b/src/app/common/constants.js index dec3442..68434a9 100644 --- a/src/app/common/constants.js +++ b/src/app/common/constants.js @@ -2,9 +2,8 @@ * contants shared in app/client */ -exports.userConfigId = 'userConfig' -exports.userNoEncryptConfigId = 'userConfigNoEncrypt' -exports.instSftpKeys = [ +export const userConfigId = 'userConfig' +export const instSftpKeys = [ 'connect', 'list', 'download', diff --git a/src/app/common/get-folder-size-and-file-count.js b/src/app/common/count-folder-data.js similarity index 89% rename from src/app/common/get-folder-size-and-file-count.js rename to src/app/common/count-folder-data.js index db84926..7051350 100644 --- a/src/app/common/get-folder-size-and-file-count.js +++ b/src/app/common/count-folder-data.js @@ -1,4 +1,4 @@ -exports.getSizeCount = function (str) { +export const getSizeCount = function (str) { const [s1, s2] = str.split('\n').map(d => d.trim()) const arr = s1.split(/\s+/) const d1 = arr[0] @@ -16,7 +16,7 @@ exports.getSizeCount = function (str) { } } -exports.getSizeCountWin = function (str) { +export const getSizeCountWin = function (str) { const arr = str.trim().split('\n') let count = 0 let size = 0 diff --git a/src/app/common/create-session-log-file-path.js b/src/app/common/create-session-log-file-path.js index b283941..b9ad63c 100644 --- a/src/app/common/create-session-log-file-path.js +++ b/src/app/common/create-session-log-file-path.js @@ -2,6 +2,6 @@ * functions to create ssh log of session */ -exports.createLogFileName = (id) => { +export const createLogFileName = (id) => { return `${id}.log` } diff --git a/src/app/common/default-setting.js b/src/app/common/default-setting.js index 3b6d934..f784304 100644 --- a/src/app/common/default-setting.js +++ b/src/app/common/default-setting.js @@ -2,13 +2,13 @@ * default setting */ -module.exports = exports.default = { +export default { hotkey: 'Control+2', sshReadyTimeout: 50000, scrollback: 3000, onStartSessions: [], fontSize: 16, - fontFamily: 'Maple Mono, mono, courier-new, courier, monospace', + fontFamily: 'Fira Code, mono, courier-new, courier, monospace', execWindows: 'System32/WindowsPowerShell/v1.0/powershell.exe', execMac: 'zsh', execLinux: 'bash', @@ -16,7 +16,7 @@ module.exports = exports.default = { execMacArgs: [], execLinuxArgs: [], enableGlobalProxy: false, - disableConnectionHistory: false, + disableSshHistory: false, disableTransferHistory: false, terminalBackgroundImagePath: '', terminalBackgroundFilterOpacity: 1, @@ -39,8 +39,7 @@ module.exports = exports.default = { initDefaultTabOnStart: true, screenReaderMode: false, autoRefreshWhenSwitchToSftp: false, - addTimeStampToTermLog: false, - keepaliveInterval: 10000, + keepaliveInterval: 0, backspaceMode: '^?', shiftEnterMode: '\\n', showHiddenFilesOnSftpStart: true, @@ -65,13 +64,11 @@ module.exports = exports.default = { roleAI: '终端专家,提供不同系统下命令,简要解释用法,用markdown格式', apiPathAI: '/chat/completions', authHeaderNameAI: 'Authorization: Bearer', - proxyAI: '', sessionLogPath: '', sshSftpSplitView: false, showCmdSuggestions: false, startDirectoryLocal: '', - allowMultiInstance: false, - disableDeveloperTool: false, + autoReconnectTerminal: false, dragDropBehavior: 'ask', switchTabOnHover: false, disableShortcutBar: false, diff --git a/src/app/common/default-user-name.js b/src/app/common/default-user-name.js deleted file mode 100644 index 9182796..0000000 --- a/src/app/common/default-user-name.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = exports.defaultUserName = 'default_user' diff --git a/src/app/common/fs-functions.js b/src/app/common/fs-functions.js new file mode 100644 index 0000000..91b4d2d --- /dev/null +++ b/src/app/common/fs-functions.js @@ -0,0 +1,34 @@ +export default [ + 'readdirOnly', + 'readdirAndFiles', + 'run', + 'runWinCmd', + 'access', + 'statAsync', + 'lstatAsync', + 'cp', + 'mv', + 'mkdir', + 'touch', + 'chmod', + 'rename', + 'unlink', + 'rmrf', + 'readdirAsync', + 'readFile', + 'readFileAsBase64', + 'writeFile', + 'openFile', + 'zipFolder', + 'unzipFile', + 'readCustom', + 'exists', + 'readdir', + 'mkdir', + 'realpath', + 'statCustom', + 'openCustom', + 'closeCustom', + 'writeCustom', + 'getFolderSize' +] diff --git a/src/app/common/get-json.js b/src/app/common/get-json.js new file mode 100644 index 0000000..5978c68 --- /dev/null +++ b/src/app/common/get-json.js @@ -0,0 +1,4 @@ +import { readFileSync } from 'fs' +export default (pth) => { + return JSON.parse(readFileSync(pth, 'utf8')) +} diff --git a/src/app/common/is-ip.js b/src/app/common/is-ip.js new file mode 100644 index 0000000..afebe3f --- /dev/null +++ b/src/app/common/is-ip.js @@ -0,0 +1,16 @@ +export function isValidIP (input) { + // Check IPv4 format + const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ + if (ipv4Pattern.test(input)) { + return true + } + + // Check IPv6 format + const ipv6Pattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i + if (ipv6Pattern.test(input)) { + return true + } + + // If input doesn't match IPv4 or IPv6 patterns, it's not a valid IP + return false +} diff --git a/src/app/common/log.js b/src/app/common/log.js index 450b17f..a37bc5e 100644 --- a/src/app/common/log.js +++ b/src/app/common/log.js @@ -1,11 +1,84 @@ -const log = require('electron-log') -const { isDev } = require('./runtime-constants') +import { config } from 'dotenv' +import fs from 'fs' +import path from 'path' -log.transports.console.format = '{h}:{i}:{s} {level} › {text}' +config() -if (!isDev) { - log.transports.console.level = 'warn' - log.transports.file.level = 'warn' +// Lightweight, dependency-free logger. +// - Logs to the console (level-aware) and, when possible, to a rolling file +// under the node project's `data/log` directory so logs can be pulled for +// debugging on Android. +// - Replaces `electron-log` entirely so the backend has no native/desktop-only +// dependency and starts reliably on the mobile Node runtime. + +const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } + +function levelFromEnv () { + const raw = (process.env.LOG_LEVEL || '').toLowerCase() + return Object.prototype.hasOwnProperty.call(LEVELS, raw) ? LEVELS[raw] : LEVELS.info +} + +const threshold = levelFromEnv() + +let logFile = null +try { + // Honour DB_PATH (set by the Android entry point to a stable, app-private + // directory) so logs live next to the database/uploads. Fall back to + // /data/log when DB_PATH is not set (desktop / local runs). + const base = process.env.DB_PATH + ? path.resolve(process.env.DB_PATH, 'log') + : path.resolve(process.cwd(), 'data', 'log') + fs.mkdirSync(base, { recursive: true }) + logFile = path.join(base, 'electerm.log') +} catch (e) { + // File logging is best-effort; never let it break startup. + logFile = null +} + +function ts () { + const d = new Date() + const p = (n) => String(n).padStart(2, '0') + return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} + +function formatArg (a) { + if (a instanceof Error) return a.stack || a.message + if (typeof a === 'string') return a + if (a === undefined) return 'undefined' + if (a === null) return 'null' + try { + return JSON.stringify(a) + } catch (e) { + return String(a) + } +} + +function emit (level, args) { + const line = `[${ts()}] ${level} › ${args.map(formatArg).join(' ')}` + if (LEVELS[level] <= threshold) { + const fn = + level === 'error' ? console.error + : level === 'warn' ? console.warn + : level === 'debug' ? console.debug + : console.log + fn(line) + } + if (logFile) { + try { + fs.appendFileSync(logFile, line + '\n') + } catch (e) { + // ignore write failures + } + } +} + +const logger = { + error: (...args) => emit('error', args), + warn: (...args) => emit('warn', args), + info: (...args) => emit('info', args), + debug: (...args) => emit('debug', args), + // kept for minimal API compatibility with callers that touch transports + transports: { console: { format: '' } } } -module.exports = exports.default = log +export default logger diff --git a/src/app/common/parse-quick-connect.js b/src/app/common/parse-quick-connect.js deleted file mode 100644 index 7a1f8f3..0000000 --- a/src/app/common/parse-quick-connect.js +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Quick Connect String Parser - * Parses connection strings according to temp/quick-connect.wiki.md specification - * - * Supported Protocols: ssh, telnet, vnc, rdp, spice, serial, ftp, http, https, electerm - * - * Basic Format: - * protocol://[username:password@]host[:port]?anyQueryParam=anyValue&opts={"key":"value"} - * - * electerm:// Format (default type is ssh): - * electerm://[username:password@]host[:port]?type=ssh&anyQueryParam=anyValue - * electerm://host?type=telnet - * electerm://user@host:22?type=vnc - * - * Shortcut Format (SSH default): - * user@host - * user@host:22 - * 192.168.1.100 - * 192.168.1.100:22 - */ - -const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm'] - -/** - * Deny list for opts keys - these are parsed from the URL itself - * and should not be overridable via the opts JSON parameter for safety - */ -const OPTS_DENY_LIST = ['type', 'host'] - -/** - * Default ports for each protocol - */ -const DEFAULT_PORTS = { - ssh: 22, - telnet: 23, - vnc: 5900, - rdp: 3389, - spice: 5900, - serial: undefined, // Serial doesn't have a default port - ftp: 21, - http: 80, - https: 443, - electerm: 22 // electerm defaults to SSH port -} - -/** - * Default values for each protocol type - * Based on src/client/components/bookmark-form/config - */ -const TYPE_DEFAULT_VALUES = { - ssh: { - port: 22, - enableSsh: true, - enableSftp: true, - useSshAgent: true, - authType: 'password', - term: 'xterm-256color', - encode: 'utf-8', - envLang: 'en_US.UTF-8' - }, - telnet: { - port: 23 - }, - vnc: { - port: 5900, - viewOnly: false, - clipViewport: false, - scaleViewport: true, - qualityLevel: 3, - compressionLevel: 1, - shared: true - }, - rdp: { - port: 3389 - }, - spice: { - port: 5900, - viewOnly: false, - scaleViewport: true - }, - serial: { - baudRate: 9600, - dataBits: 8, - lock: true, - stopBits: 1, - parity: 'none', - rtscts: false, - xon: false, - xoff: false, - xany: false, - term: 'xterm-256color', - displayRaw: false - }, - ftp: { - port: 21, - encode: 'utf-8', - secure: false - }, - web: {}, - local: {} -} - -/** - * Parse a quick connect string into connection options - * @param {string} str - The connection string - * @returns {object|null} - Parsed options or null if invalid - */ -function parseQuickConnect (str) { - if (!str || typeof str !== 'string') { - return null - } - - const trimmed = str.trim() - if (!trimmed) { - return null - } - - try { - // Strip trailing slashes (supports pasted URLs like host/ or ssh://host/) - const input = trimmed.replace(/\/+$/, '') - - // Detect protocol - const protocolMatch = input.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i) - - let protocol = '' - let connectionString = '' - let originalProtocol = 'ssh' - - if (protocolMatch) { - originalProtocol = protocolMatch[1].toLowerCase() - protocol = originalProtocol - // Normalize http/https to web - if (protocol === 'http' || protocol === 'https') { - protocol = 'web' - } - connectionString = input.slice(protocolMatch[0].length) - } else { - // Shortcut format - default to SSH - // Match user@host or user@host:port or just host or host:port - // Use last colon to determine port for host:port format - if (/^[\w.-]+(?::[^@]+)?@[\w.-]+/.test(input)) { - // user@host, user:password@host, or user@host:port - protocol = 'ssh' - connectionString = input - } else if (/^[\w.-]+:.*:[\d]+$/.test(input)) { - // host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344) - // Check if the last colon is followed by digits (port number) - protocol = 'ssh' - connectionString = input - } else if (/^[\w.-]+:[\d]+$/.test(input)) { - // host:port (no username, simple format like host:22) - protocol = 'ssh' - connectionString = input - } else if (/^[\w.-]+$/.test(input)) { - // just host - protocol = 'ssh' - connectionString = input - } else { - return null - } - } - - if (!SUPPORTED_PROTOCOLS.includes(protocol) && protocol !== 'web') { - return null - } - - // Extract opts from the connection string before parsing - let optsStr = '' - const optsMatch = connectionString.match(/[?&]opts=('|")(.+?)('|")$/) - if (!optsMatch) { - // Try without quotes - const optsMatchNoQuote = connectionString.match(/[?&]opts=(\{.+?\})$/) - if (optsMatchNoQuote) { - optsStr = optsMatchNoQuote[1] - connectionString = connectionString.slice(0, optsMatchNoQuote.index) - } - } else { - optsStr = optsMatch[2] - connectionString = connectionString.slice(0, optsMatch.index) - } - - // Extract query string for web type and electerm type - let queryStr = '' - const queryMatch = connectionString.match(/\?(.+)$/) - if (queryMatch) { - queryStr = queryMatch[1] - connectionString = connectionString.slice(0, queryMatch.index) - } - - // Parse username:password@host:port - // First, check if there's an @ for auth - let username = '' - let password = '' - let hostOrPath = '' - let port = '' - - const atIndex = connectionString.indexOf('@') - if (atIndex !== -1) { - // Has auth - const authPart = connectionString.slice(0, atIndex) - const hostPart = connectionString.slice(atIndex + 1) - const colonIndex = authPart.indexOf(':') - if (colonIndex !== -1) { - username = authPart.slice(0, colonIndex) - password = authPart.slice(colonIndex + 1) - } else { - username = authPart - } - // Parse host:port from hostPart - const hostColonIndex = hostPart.lastIndexOf(':') - if (hostColonIndex !== -1) { - hostOrPath = hostPart.slice(0, hostColonIndex) - port = hostPart.slice(hostColonIndex + 1) - } else { - hostOrPath = hostPart - } - } else { - // No @ sign - check for special case: protocol://password:host (e.g., spice://password:host) - // This only applies to spice protocol - if (protocol === 'spice') { - // Count colons in the connection string - const colonCount = (connectionString.match(/:/g) || []).length - - if (colonCount >= 2) { - // Multiple colons - could be password:host:port or host:port with IP - // Use lastIndexOf for port, then check if first part is password or IP - const lastColonIndex = connectionString.lastIndexOf(':') - const portCandidate = connectionString.slice(lastColonIndex + 1) - - if (/^\d+$/.test(portCandidate)) { - // Last part is a port number - const hostPortPart = connectionString.slice(0, lastColonIndex) - const secondLastColonIndex = hostPortPart.lastIndexOf(':') - - if (secondLastColonIndex !== -1) { - // There's another colon - first part could be password - const potentialPassword = hostPortPart.slice(0, secondLastColonIndex) - const hostPart = hostPortPart.slice(secondLastColonIndex + 1) - - // Check if potentialPassword is NOT an IP/hostname - // An IP/hostname should contain dots, a password typically doesn't - // Also check it's not a simple number (port) - const isIPorHostname = (potentialPassword.includes('.') || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(potentialPassword)) - - if (isIPorHostname) { - // It's IP, no password - hostOrPath = hostPortPart - port = portCandidate - } else { - // It's password - password = potentialPassword - hostOrPath = hostPart - port = portCandidate - } - } else { - // Only one colon before the port - it's host:port - hostOrPath = hostPortPart - port = portCandidate - } - } else { - // Last part is not a port - hostOrPath = connectionString - } - } else if (colonCount === 1) { - // Single colon - could be host:port or just a word with colon - const colonIndex = connectionString.indexOf(':') - const firstPart = connectionString.slice(0, colonIndex) - const secondPart = connectionString.slice(colonIndex + 1) - - // Check if first part is an IP - const isIP = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(firstPart) - - if (isIP) { - // IP with port - hostOrPath = firstPart - port = secondPart - } else if (/^\d+$/.test(secondPart)) { - // Just a word with port number - // This is likely password (for spice) or host without port - // For spice, treat first part as host (not password) since there's only one colon - hostOrPath = firstPart - port = secondPart - } else { - // host or hostname - hostOrPath = connectionString - } - } else { - // No colon - just host - hostOrPath = connectionString - } - } else { - // Normal case - just host:port - const hostColonIndex = connectionString.lastIndexOf(':') - if (hostColonIndex !== -1) { - // Make sure it's a port number (all digits) - const potentialPort = connectionString.slice(hostColonIndex + 1) - if (/^\d+$/.test(potentialPort)) { - hostOrPath = connectionString.slice(0, hostColonIndex) - port = potentialPort - } else { - hostOrPath = connectionString - } - } else { - hostOrPath = connectionString - } - } - } - - if (!hostOrPath) { - return null - } - - // Build base options - // For electerm protocol, we need to handle the type from query params - let finalProtocol = protocol - let webProtocol = originalProtocol // Store original for web type - - // Handle electerm:// protocol - extract type from query params, default to ssh - if (originalProtocol === 'electerm') { - // Parse query params to get type - const params = new URLSearchParams(queryStr) - finalProtocol = params.get('type') || params.get('tp') || 'ssh' - - // Validate the type is supported - if (!SUPPORTED_PROTOCOLS.includes(finalProtocol) && finalProtocol !== 'web') { - return null - } - - // Normalize http/https to web - if (finalProtocol === 'http' || finalProtocol === 'https') { - webProtocol = finalProtocol // Store the http/https before normalizing - finalProtocol = 'web' - // Remove type/tp from query string for web URL construction - params.delete('type') - params.delete('tp') - queryStr = params.toString() - } - } else { - webProtocol = originalProtocol - } - - const opts = { - type: finalProtocol - } - - // Handle different protocol types - if (finalProtocol === 'serial') { - // Serial: path is the port - opts.path = hostOrPath - if (port) { - opts.baudRate = parseInt(port, 10) - } - // Parse query params for serial (like baudRate) - if (queryStr) { - const params = new URLSearchParams(queryStr) - if (params.has('baudRate')) { - opts.baudRate = parseInt(params.get('baudRate'), 10) - } - } - } else if (finalProtocol === 'web') { - // Web: construct URL from protocol + host + port + query - let url = `${webProtocol}://${hostOrPath}` - if (port) { - // Add non-standard port to URL - const defaultPort = originalProtocol === 'https' ? 443 : 80 - if (parseInt(port, 10) !== defaultPort) { - url += `:${port}` - } - } - // Add query string if present - if (queryStr) { - const separator = url.includes('?') ? '&' : '?' - url += `${separator}${queryStr}` - } - opts.url = url - } else { - // SSH, Telnet, VNC, RDP, Spice, FTP - opts.host = hostOrPath - if (port) { - opts.port = parseInt(port, 10) - } - if (username !== undefined && username !== '') { - // FTP form uses 'user' instead of 'username' - if (finalProtocol === 'ftp') { - opts.user = username - } else { - opts.username = username - } - } - if (password !== undefined && password !== '') { - opts.password = password - } - // Parse query params for other protocols (like title) - if (queryStr) { - const params = new URLSearchParams(queryStr) - if (params.has('title')) { - opts.title = params.get('title') - } - } - } - - // Parse opts JSON to extend params - if (optsStr) { - try { - const extraOpts = JSON.parse(optsStr) - OPTS_DENY_LIST.forEach(key => delete extraOpts[key]) - Object.assign(opts, extraOpts) - } catch (err) { - console.error('Failed to parse opts:', err) - } - } - - // Apply default values for the protocol type - const typeDefaults = TYPE_DEFAULT_VALUES[finalProtocol] - if (typeDefaults) { - Object.keys(typeDefaults).forEach(key => { - // Only apply default if not already set - if (opts[key] === undefined) { - opts[key] = typeDefaults[key] - } - }) - } - - return opts - } catch (error) { - console.error('Error parsing quick connect string:', error) - return null - } -} - -/** - * Get default port for a protocol - * @param {string} protocol - The protocol name - * @returns {number|undefined} - Default port or undefined - */ -function getDefaultPort (protocol) { - return DEFAULT_PORTS[protocol] -} - -/** - * Get list of supported protocols - * @returns {string[]} - List of supported protocols - */ -function getSupportedProtocols () { - return [...SUPPORTED_PROTOCOLS] -} - -module.exports = { - parseQuickConnect, - getDefaultPort, - getSupportedProtocols, - SUPPORTED_PROTOCOLS, - DEFAULT_PORTS, - OPTS_DENY_LIST -} diff --git a/src/app/common/pass-enc.js b/src/app/common/pass-enc.js index 69a097a..734f6f9 100644 --- a/src/app/common/pass-enc.js +++ b/src/app/common/pass-enc.js @@ -1,4 +1,4 @@ -exports.enc = (str) => { +export const enc = (str) => { if (typeof str !== 'string') { return str } @@ -7,7 +7,7 @@ exports.enc = (str) => { }).join('') } -exports.dec = (str) => { +export const dec = (str) => { if (typeof str !== 'string') { return str } diff --git a/src/app/common/runtime-constants.js b/src/app/common/runtime-constants.js index 01bf478..e3838a0 100644 --- a/src/app/common/runtime-constants.js +++ b/src/app/common/runtime-constants.js @@ -1,77 +1,38 @@ -/** - * run time contants - */ +import os from 'os' +import { resolve } from 'path' +import getJson from './get-json.js' -const os = require('os') -const fs = require('fs') -const { resolve } = require('path') +export const cwd = process.cwd() const platform = os.platform() const arch = os.arch() -const isWin = platform === 'win32' -const isMac = platform === 'darwin' -const isLinux = platform === 'linux' -const isArm = arch.includes('arm') - const { NODE_ENV, NODE_TEST } = process.env -const isDev = NODE_ENV === 'development' -const iconPath = resolve( - __dirname, - ( - isDev - ? '../../../node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png' - : '../assets/images/electerm-round-128x128.png' - ) +export const home = os.homedir() +export const sshKeysPath = resolve( + home, + '.ssh' ) -const trayIconPath = resolve( - __dirname, - ( - isDev - ? '../../../node_modules/@electerm/electerm-resource/tray-icons/electerm-tray.png' - : '../assets/images/electerm-tray.png' - ) +export const isWin = platform === 'win32' +export const isMac = platform === 'darwin' +export const isLinux = platform === 'linux' +export const isArm = arch.includes('arm') +export const isDev = NODE_ENV === 'development' +export const iconPath = resolve( + cwd, + isDev + ? 'node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png' + : 'dist/assets/images/electerm-round-128x128.png' ) -const extIconPath = isDev +export const extIconPath = isDev ? '/node_modules/electerm-icons/icons/' - : 'icons/' - -const defaultUserName = require('./default-user-name') - -/** - * bootstrap.js overrides os.homedir() to return the app's sandbox data - * directory (DATA_PATH), so getHomeDir() simply delegates to it. - * os.tmpdir() may still point outside the sandbox, so getTempDir() - * derives a writable tmp/ subdirectory under DATA_PATH. - */ -function getHomeDir () { - return os.homedir() -} - -function getTempDir () { - if (process.env.DATA_PATH) { - const dir = resolve(process.env.DATA_PATH, 'tmp') - // Create immediately so downstream writes never fail on a missing dir. - try { fs.mkdirSync(dir, { recursive: true }) } catch {} - return dir - } - return os.tmpdir() -} - -module.exports = { - isTest: !!NODE_TEST, - isDev, - isWin, - isMac, - isArm, - isLinux, - iconPath, - trayIconPath, - extIconPath, - defaultUserName, - minWindowWidth: 590, - minWindowHeight: 400, - defaultLang: 'zh_cn', - homeDir: getHomeDir(), - tempDir: getTempDir(), - packInfo: require(isDev ? '../../../package.json' : '../package.json') -} + : '/icons/' +export const defaultUserName = 'default_user' +export const minWindowWidth = 590 +export const minWindowHeight = 400 +export const defaultLang = 'en_us' +export const tempDir = os.tmpdir() +export const homeOrTmp = os.homedir() || os.tmpdir() +export const packInfo = getJson( + resolve(cwd, 'package.json') +) +export const isTest = !!NODE_TEST diff --git a/src/app/common/sanitize-filename.js b/src/app/common/sanitize-filename.js index dfe0141..bcdebf9 100644 --- a/src/app/common/sanitize-filename.js +++ b/src/app/common/sanitize-filename.js @@ -34,7 +34,7 @@ const MAX_FILENAME_LENGTH = 255 const REPLACEMENT_CHAR = '_' -module.exports = function sanitizeFilename (name) { +export default function sanitizeFilename (name) { if (!name || typeof name !== 'string') { return 'unnamed' } diff --git a/src/app/common/time.js b/src/app/common/time.js index f5c4d31..943e1bc 100644 --- a/src/app/common/time.js +++ b/src/app/common/time.js @@ -2,18 +2,11 @@ * time formatter */ -const formatTime = (time = new Date()) => { - const date = time instanceof Date ? time : new Date(time) +import dayjs from 'dayjs' - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - const milliseconds = String(date.getMilliseconds()).padStart(3, '0') - - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}` +export default ( + time = new Date(), + format = 'YYYY-MM-DD HH:mm:ss.SSS' +) => { + return dayjs(time).format(format) } - -module.exports = formatTime diff --git a/src/app/common/uid.js b/src/app/common/uid.js index b0463d3..32a9a0c 100644 --- a/src/app/common/uid.js +++ b/src/app/common/uid.js @@ -1,4 +1,4 @@ -const { nanoid } = require('nanoid') -module.exports = () => { +import { nanoid } from 'nanoid' +export default function uid () { return nanoid(7) } diff --git a/src/app/common/version-compare.js b/src/app/common/version-compare.js index 64bc3d2..1c88b88 100644 --- a/src/app/common/version-compare.js +++ b/src/app/common/version-compare.js @@ -8,7 +8,7 @@ // return 1 when a > b // return -1 when a < b // return 0 when a === b -module.exports = exports.default = function (a, b) { +export default function (a, b) { const ar = a.split('.').map(n => Number(n.replace('v', ''))) const br = b.split('.').map(n => Number(n.replace('v', ''))) let res = 0 diff --git a/src/app/lib/ai.js b/src/app/lib/ai.js index 07c67f1..0e53066 100644 --- a/src/app/lib/ai.js +++ b/src/app/lib/ai.js @@ -1,34 +1,18 @@ -const axios = require('axios') -const { StringDecoder } = require('string_decoder') -const log = require('../common/log') -const defaultSettings = require('../common/config-default') -const { createProxyAgent } = require('./proxy-agent') +/** + * AI integration with DeepSeek API + */ +import axios from 'axios' +import { + StringDecoder +} from 'string_decoder' +import log from '../common/log.js' +import defaultSettings from '../common/config-default.js' +import { createProxyAgent } from './proxy-agent.js' // Store for ongoing streaming sessions const streamingSessions = new Map() -// Stop an ongoing streaming session -exports.stopStream = (sessionId) => { - const session = streamingSessions.get(sessionId) - if (!session) { - return { error: 'Session not found' } - } - - // Destroy the stream to stop receiving data - if (session.stream && !session.stream.destroyed) { - session.stream.destroy() - } - - // Mark as completed (not an error, just stopped by user) - session.completed = true - session.stopped = true - - // Clean up - streamingSessions.delete(sessionId) - - return { stopped: true } -} - +// Initialize OpenAI with DeepSeek configuration const createAIClient = (baseURL, apiKey, proxy, authHeaderName) => { const headerStr = authHeaderName || 'Authorization: Bearer' const parts = headerStr.split(': ') @@ -56,7 +40,7 @@ const createAIClient = (baseURL, apiKey, proxy, authHeaderName) => { return axios.create(config) } -exports.AIchatWithTools = async (messages, model, baseURL, path, apiKey, proxy, tools, authHeaderName) => { +export const AIchatWithTools = async (messages, model, baseURL, path, apiKey, proxy, tools, authHeaderName) => { try { const client = createAIClient(baseURL, apiKey, proxy, authHeaderName) const requestData = { @@ -64,7 +48,7 @@ exports.AIchatWithTools = async (messages, model, baseURL, path, apiKey, proxy, messages, stream: false } - if (tools && tools.length) { + if (tools?.length) { requestData.tools = tools } const response = await client.post(path, requestData) @@ -78,7 +62,7 @@ exports.AIchatWithTools = async (messages, model, baseURL, path, apiKey, proxy, } } -exports.AIchat = async ( +export const AIchat = async ( prompt, model = defaultSettings.modelAI, role = defaultSettings.roleAI, @@ -161,7 +145,7 @@ exports.AIchat = async ( } // Function to get the current state of a streaming session -exports.getStreamContent = (sessionId) => { +export const getStreamContent = async (sessionId) => { const session = streamingSessions.get(sessionId) if (!session) { return { @@ -233,3 +217,25 @@ function processStream (sessionId, sessionData) { sessionData.completed = true }) } + +// Stop an ongoing streaming session +export const stopStream = (sessionId) => { + const session = streamingSessions.get(sessionId) + if (!session) { + return { error: 'Session not found' } + } + + // Destroy the stream to stop receiving data + if (session.stream && !session.stream.destroyed) { + session.stream.destroy() + } + + // Mark as completed (not an error, just stopped by user) + session.completed = true + session.stopped = true + + // Clean up + streamingSessions.delete(sessionId) + + return { stopped: true } +} diff --git a/src/app/lib/auth.js b/src/app/lib/auth.js deleted file mode 100644 index 7f03db6..0000000 --- a/src/app/lib/auth.js +++ /dev/null @@ -1,64 +0,0 @@ -const { userConfigId } = require('../common/constants') -const { dbAction } = require('./db') -const getPort = require('./get-port') - -function hashPassword (password) { - const crypto = require('crypto') - const salt = crypto.randomBytes(16).toString('hex') - const hashedPassword = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex') - return { salt, hashedPassword } -} - -function comparePasswords (password, salt, hashedPassword) { - const crypto = require('crypto') - const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex') - return hash === hashedPassword -} - -exports.setPassword = async function setPassword (password) { - const q = { - _id: userConfigId - } - const userConfig = await dbAction('data', 'findOne', q) || {} - if (password === '') { - await dbAction('data', 'update', q, { - ...q, - ...userConfig, - salt: '', - hashedPassword: '' - }, { - upsert: true - }) - return true - } - const { salt, hashedPassword } = hashPassword(password) - await dbAction('data', 'update', q, { - ...q, - ...userConfig, - salt, - hashedPassword - }, { - upsert: true - }) - return true -} - -exports.checkPassword = async function checkPassword (password) { - const axios = require('axios') - axios.defaults.proxy = false - if (!password) { - return false - } - const q = { - _id: userConfigId - } - const { salt, hashedPassword } = await dbAction('data', 'findOne', q) || {} - const r = comparePasswords(password, salt, hashedPassword) - if (r) { - const port = await getPort() - await axios.post(`http://127.0.0.1:${port}/auth`, { - token: hashedPassword - }) - } - return r -} diff --git a/src/app/lib/build-proxy.js b/src/app/lib/build-proxy.js index 6715b37..883ad96 100644 --- a/src/app/lib/build-proxy.js +++ b/src/app/lib/build-proxy.js @@ -1,4 +1,4 @@ -exports.buildProxyString = function (obj) { +export function buildProxyString (obj) { if (!obj.proxyIp) { return '' } diff --git a/src/app/lib/command-line.js b/src/app/lib/command-line.js deleted file mode 100644 index 5c8ddfe..0000000 --- a/src/app/lib/command-line.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * command line support - */ - -const { packInfo, isTest } = require('../common/app-props') -const { version } = packInfo - -let helpInfo -let options -let program - -function parseCommandLine (argv, options) { - const { Command } = require('commander') - const prog = new Command() - - prog - .version(version) - .name('electerm') - .usage('[options] sshServer') - .description(` -### Connect ssh server from command line examples: -- electerm user@xx.com -- electerm user@xx.com:22 -- electerm --password password --set-env "SECRET=xxx USER=hhhh" user@xx.com:22 -- electerm -l user -P 22 -i /path/to/private-key -pw password xx.com -T -t "XX Server" - -### Other params examples: -- server port: -electerm -sp 30976 -- load and run batch operation from json file: -electerm -bo "/home/root/works.json" - -### other connection types -- telnet: -electerm -tp "telnet" -opts '{"host":"192.168.1.1","port":21","username":"root","password":"123456"}' -- rdp: electerm -tp "rdp" -opts '{"host":"192.168.1.1","port":3389","username":"root","password":"123456"}' -- vnc: electerm -tp "vnc" -opts '{"host":"192.168.1.1","port":3389","username":"root","password":"123456"}' -- serial: electerm -tp "serial" -opts '{"port":"COM1","baudRate":115200,"dataBits":8,"stopBits":1,"parity":"none"}' -- local: electerm -tp "local" -opts '{"title": "local terminal"}' - -### Environment variables: -- DATA_PATH: -DATA_PATH=/custom/path/to/electerm-data electerm - -- NO_PROXY_SERVER: -NO_PROXY_SERVER=1 electerm - -- PROXY_BYPASS_LIST: -PROXY_BYPASS_LIST="127.0.0.1, 127.0.0.1" electerm - -- PROXY_PAC_URL: -PROXY_PAC_URL="http://proxy.example.com/pac" electerm - -- PROXY_SERVER: -PROXY_SERVER="socks5://127.0.0.1:1080" electerm -`) - .option('-t, --title [Tab Name]', 'Specify the title of the new tab') - .option('-l, --user ', 'specify a login name') - .option('-P, --port ', 'specify ssh port') - .option('-bo, --batch-op ', 'load and run batch operation from json file') - .option('-sp, --server-port ', 'specify server port, default is') - .option('-i, --private-key-path ', 'specify an SSH private key path') - .option('-ps, --passphrase ', 'specify an SSH private key passphrase') - .option('-pw, --password ', 'specify ssh server password') - .option('-se, --set-env ', 'specify envs') - .option('-so, --sftp-only', 'only show sftp panel') - .option('-d, --init-folder ', 'init folder got init terminal') - .option('-tp, --tp ', 'specify connection type') - .option('-opts, --opts ', 'specify connection options, json string') - .allowUnknownOption() - .exitOverride() - - try { - prog.parse(argv, options) - } catch (err) { - if (err.message.includes('outputHelp')) { - process.exit(0) - } - } - return prog -} - -if (!isTest) { - program = parseCommandLine() - options = program.opts() - helpInfo = program.helpInformation() -} - -exports.initCommandLine = function () { - if (isTest) { - return false - } - return { - options, - argv: program.args, - helpInfo - } -} diff --git a/src/app/lib/conf.js b/src/app/lib/conf.js new file mode 100644 index 0000000..8ec2106 --- /dev/null +++ b/src/app/lib/conf.js @@ -0,0 +1,27 @@ +import { + cwd +} from '../common/runtime-constants.js' +import log from '../common/log.js' +import { + resolve +} from 'path' + +const glob = {} + +export async function getConf () { + if (glob.conf) { + return glob.conf + } + const conf = await import( + resolve(cwd, 'config.js') + ).catch(err => { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + return + } + log.error('read config.js failed', err) + }) + if (conf) { + glob.conf = conf + } + return glob.conf || {} +} diff --git a/src/app/lib/create-app.js b/src/app/lib/create-app.js deleted file mode 100644 index 4613e8f..0000000 --- a/src/app/lib/create-app.js +++ /dev/null @@ -1,164 +0,0 @@ -const { - app -} = require('electron') -const { createWindow } = require('./create-window') -const { - packInfo -} = require('../common/runtime-constants') -const { initCommandLine } = require('./command-line') -const globalState = require('./glob-state') -const { getUserConfigNoEnc, getDbConfig } = require('./get-config') -const { - setupDeepLinkHandlers -} = require('./deep-link') -const { handleSingleInstance } = require('./single-instance') -const log = require('../common/log') - -let conf = {} - -// GPU error suggestion message -const GPU_ERROR_SUGGESTION = ` -================================================================================ -⚠️ GPU Process Error Detected -================================================================================ -If you encounter GPU process crashes (exit_code=-2147483645 or similar), -try running electerm with one of these flags: - - 1. --no-sandbox (Recommended - run without sandbox) - 2. --disable-gpu (Disable GPU rendering) - 3. --disable-gpu-sandbox (Disable GPU sandbox) - 4. --disable-hardware-acceleration - -Or set environment variable: - DISABLE_GPU=1 (Disable GPU) - DISABLE_GPU_SANDBOX=1 (Disable GPU + sandbox, use SwiftShader) - ENABLE_GPU=1 (Linux only: force-enable hardware GPU) - -Example: - electerm --no-sandbox - or - DISABLE_GPU=1 electerm -================================================================================ -` - -// Handle GPU process crashes -app.on('gpu-process-crashed', (event, killed) => { - log.error(`GPU process crashed, killed: ${killed}`) - console.error(GPU_ERROR_SUGGESTION) -}) - -// Handle render process gone events -app.on('render-process-gone', (event, webContents, details) => { - if (details.reason === 'crashed' || details.reason === 'abnormal-exit') { - log.error(`Render process gone: ${details.reason}`, details) - console.error(GPU_ERROR_SUGGESTION) - } -}) - -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - log.error('uncaughtException:', error?.message || error, error?.stack || '') - const errorMsg = error?.message || '' - // Check if it's GPU related - if ( - errorMsg.includes('GPU') || - errorMsg.includes('gpu') || - errorMsg.includes('graphics') || - errorMsg.includes('Vulkan') || - errorMsg.includes('DXGI') - ) { - console.error(GPU_ERROR_SUGGESTION) - } -}) - -// Handle unhandled promise rejections -process.on('unhandledRejection', (reason, promise) => { - log.error('unhandledRejection:', reason?.message || reason, reason?.stack || '') -}) - -exports.createApp = async function () { - app.setName(packInfo.name) - // Disable GPU for stability — the HarmonyOS Electron runtime does not - // support hardware-accelerated rendering reliably. - app.commandLine.appendSwitch('disable-gpu') - app.commandLine.appendSwitch('disable-gpu-compositing') - app.commandLine.appendSwitch('disable-gpu-rasterization') - app.commandLine.appendSwitch('use-gl', 'swiftshader') - app.disableHardwareAcceleration() - if (process.env.DISABLE_GPU_SANDBOX) { - app.disableHardwareAcceleration() - app.commandLine.appendSwitch('disable-gpu') - app.commandLine.appendSwitch('disable-gpu-compositing') - app.commandLine.appendSwitch('disable-gpu-rasterization') - app.commandLine.appendSwitch('disable-gpu-sandbox') - app.commandLine.appendSwitch('disable-software-rasterizer') - app.commandLine.appendSwitch('use-gl', 'swiftshader') - } - // Handle proxy-related command-line arguments - if (process.env.NO_PROXY_SERVER) { - app.commandLine.appendSwitch('no-proxy-server') - } - if (process.env.PROXY_BYPASS_LIST) { - app.commandLine.appendSwitch('proxy-bypass-list', process.env.PROXY_BYPASS_LIST) - } - if (process.env.PROXY_PAC_URL) { - app.commandLine.appendSwitch('proxy-pac-url', process.env.PROXY_PAC_URL) - } - if (process.env.PROXY_SERVER) { - app.commandLine.appendSwitch('proxy-server', process.env.PROXY_SERVER) - } - - const progs = initCommandLine() - const opts = progs?.options - globalState.set('serverPort', opts?.serverPort) - - const { allowMultiInstance = false } = await getUserConfigNoEnc() - - // Setup deep link handlers (open-url for macOS, etc.) - setupDeepLinkHandlers() - // Only request single instance lock if multi-instance is not allowed - if (!allowMultiInstance) { - // Use socket-based single instance lock for compatibility with Electron 22 - // where additionalData doesn't work in the second-instance event - const isPrimaryInstance = await handleSingleInstance(progs) - - if (!isPrimaryInstance) { - app.quit() - return app - } - - // Also use Electron's built-in lock as a fallback - app.requestSingleInstanceLock() - } - - app.on('second-instance', (event, commandLine) => { - const newWindowFlag = commandLine.includes('--new-window') - if (newWindowFlag) { - createWindow(conf) - return - } - const win = globalState.get('win') - if (win) { - if (win.isMinimized()) { - win.restore() - } - win.focus() - } - }) - app.whenReady().then(async () => { - try { - conf = await getDbConfig() - await createWindow(conf) - } catch (e) { - log.error('Failed to create window:', e?.message || e, e?.stack || '') - } - }) - app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (globalState.get('win') === null) { - app.once('ready', () => createWindow(conf)) - } - }) - return app -} diff --git a/src/app/lib/create-window.js b/src/app/lib/create-window.js deleted file mode 100644 index 02230c1..0000000 --- a/src/app/lib/create-window.js +++ /dev/null @@ -1,161 +0,0 @@ -const { - BrowserWindow, screen, shell -} = require('electron') -const { resolve } = require('path') -const { - isDev, packInfo, iconPath, isMac, - minWindowWidth, minWindowHeight -} = require('../common/runtime-constants') -const { - getWindowSize, - setWindowPos -} = require('./window-control') -const { ensureWindowVisible } = require('./window-restore') -const { onClose } = require('./on-close') -const { initIpc, initAppServer } = require('./ipc') -const { disableShortCuts } = require('./key-bind') -const _ = require('./lodash.js') -const getPort = require('./get-port') -const globalState = require('./glob-state') -const webviewHandler = require('./webview-handler') -const log = require('../common/log') - -exports.createWindow = async function (userConfig) { - log.info('createWindow: starting...') - globalState.set('closeAction', 'closeApp') - globalState.set('requireAuth', !!userConfig.hashedPassword) - const { width, height, x, y } = await getWindowSize() - // HarmonyOS: `transparent: true` and `titleBarStyle: 'hidden'` are NOT - // supported — they cause a double title bar (the OS title bar plus the - // app's custom one). We therefore always use the system title bar, - // mirroring the override in get-config.js. - // `frame` IS supported, so once transparent/titleBarStyle are supported, - // remove this line to respect the user's useSystemTitleBar setting. - // useSystemTitleBar = true - const win = new BrowserWindow({ - width, - height, - x, - y, - fullscreenable: true, - minWidth: minWindowWidth, - minHeight: minWindowHeight, - title: packInfo.name, - frame: true, - backgroundColor: '#333333', - autoHideMenuBar: true, - webPreferences: { - contextIsolation: true, - nodeIntegration: false, - enableRemoteModule: false, - preload: resolve(__dirname, '../preload/preload.js'), - webviewTag: true, - devTools: !userConfig.disableDeveloperTool, - spellcheck: false - }, - icon: iconPath - }) - // Safety net: verify the window is actually visible on a connected - // display and move it to the primary display if not. - ensureWindowVisible(win, screen) - - // macOS: show the traffic-light buttons - if (isMac) { - win.setWindowButtonVisibility(true) - } - - win.webContents.session.setSpellCheckerDictionaryDownloadURL('https://00.00/') - - webviewHandler.init(win) - - globalState.set('win', win) - log.info('createWindow: BrowserWindow created, starting initAppServer...') - - // Intercept navigation to external URLs. Without this, clicking a - // link () inside the app would navigate the - // Electron window itself to that URL, loading the external page - // in-app instead of opening the system browser. - win.webContents.on('will-navigate', (event, url) => { - // Allow navigation to the app's own local server - if (url.startsWith('http://127.0.0.1:') || url.startsWith('data:')) { - return - } - event.preventDefault() - log.info('will-navigate: redirecting to system browser:', url) - shell.openExternal(url) - }) - - // Intercept window.open() calls — redirect to system browser - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://127.0.0.1:') || url.startsWith('data:')) { - return { action: 'allow' } - } - log.info('setWindowOpenHandler: redirecting to system browser:', url) - shell.openExternal(url) - return { action: 'deny' } - }) - - try { - await initAppServer() - log.info('createWindow: initAppServer done') - } catch (e) { - log.error('createWindow: initAppServer failed:', e?.message || e, e?.stack || '') - // Show error page in the window instead of leaving black screen - const htmlContent = `

Server failed to start

${e?.message || e}
` - const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}` - win.loadURL(dataUrl) - return - } - - initIpc() - log.info('createWindow: initIpc done') - const port = isDev - ? process.env.devPort || 5570 - : await getPort() - const opts = `http://127.0.0.1:${port}/index.html?v=${packInfo.version}` - log.info('createWindow: loading URL:', opts) - // If loading the URL fails (e.g. proxy/firewall interference), show error page - win.webContents.once('did-fail-load', (event, errorCode, errorDescription) => { - log.error('createWindow: did-fail-load:', errorCode, errorDescription) - const htmlContent = require('./error-page')(port) - const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}` - win.loadURL(dataUrl) - }) - win.loadURL(opts) - win.webContents.once('dom-ready', () => { - log.info('createWindow: dom-ready') - if (isDev && !userConfig.disableDeveloperTool) { - win.webContents.openDevTools() - } - win.on('unmaximize', () => { - const { width, height } = win.getBounds() - if (width < minWindowWidth || height < minWindowHeight) { - win.setBounds({ - x: 0, - y: 0, - width: minWindowWidth, - height: minWindowHeight - }) - win.center() - } - }) - win.on('resize', _.debounce(() => { - if (!win.isMaximized()) { - globalState.set('oldRectangle', win.getBounds()) - } - }, 200)) - win.on('move', _.debounce(() => { - const { x, y } = win.getBounds() - setWindowPos({ x, y }) - }, 100)) - - win.on('focus', () => { - win.webContents.send('focused', null) - }) - win.on('blur', () => { - win.webContents.send('blur', null) - }) - disableShortCuts(win) - }) - win.on('close', onClose) -} diff --git a/src/app/lib/custom-require.js b/src/app/lib/custom-require.js index 9c65c1e..c9fd268 100644 --- a/src/app/lib/custom-require.js +++ b/src/app/lib/custom-require.js @@ -1,35 +1,63 @@ -const path = require('path') -const { downloadPackage } = require('./npm') +import { resolve, join } from 'path' +import { readFileSync, existsSync } from 'fs' +import { downloadPackage } from './npm.js' +import { cwd } from '../common/runtime-constants.js' -exports.customRequire = async (moduleName, options = {}) => { +function getDataFolderPath () { + const dbFolder = process.env.DB_PATH || resolve(cwd, 'data') + return resolve(dbFolder, 'custom-modules') +} + +function resolveModulePath (modulePath) { + const packageJsonPath = join(modulePath, 'package.json') + if (existsSync(packageJsonPath)) { + const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) + if (pkg.main) { + return resolve(modulePath, pkg.main) + } + } + if (existsSync(join(modulePath, 'index.js'))) { + return join(modulePath, 'index.js') + } + return modulePath +} + +export const customRequire = async (moduleName, options = {}) => { const customModulesFolderPath = options.customModulesFolderPath || process.env.CUSTOM_MODULES_FOLDER_PATH || - path.resolve(require('../common/app-props').appPath, 'electerm', 'custom-modules') + getDataFolderPath() const isCustomModule = options.isCustomModule || false const downloadModule = options.downloadModule !== false - const modulePath = path.join(customModulesFolderPath, 'node_modules', moduleName) + const modulePath = resolve(customModulesFolderPath, 'node_modules', moduleName) if (isCustomModule) { try { - return require(modulePath) + const resolvedPath = resolveModulePath(modulePath) + const mod = await import(resolvedPath) + return mod.default || mod } catch (err) { if (!downloadModule) { throw err } await downloadPackage(moduleName, customModulesFolderPath) - return require(modulePath) + const resolvedPath = resolveModulePath(modulePath) + const mod = await import(resolvedPath) + return mod.default || mod } } try { - return require(moduleName) + const mod = await import(moduleName) + return mod.default || mod } catch (err) { if (!downloadModule) { throw err } await downloadPackage(moduleName, customModulesFolderPath) - return require(modulePath) + const resolvedPath = resolveModulePath(modulePath) + const mod = await import(resolvedPath) + return mod.default || mod } } diff --git a/src/app/lib/db.js b/src/app/lib/db.js index 90b538d..391c194 100644 --- a/src/app/lib/db.js +++ b/src/app/lib/db.js @@ -1,13 +1,18 @@ /** * db loader - * Uses nedb (pure JS, no native dependencies). */ -const { appPath, defaultUserName } = require('../common/app-props') -const { safeEncrypt, safeDecrypt } = require('./safe-storage') +let dbModule = null -const encOpts = { enc: safeEncrypt, dec: safeDecrypt } +async function getDbModule () { + if (!dbModule) { + // await performMigration() + dbModule = await import('./sqlite.js') + } + return dbModule +} -const { createDb } = require('./nedb') -const db = createDb(appPath, defaultUserName, encOpts) -module.exports = db +export async function dbAction (...args) { + const db = await getDbModule() + return db.dbAction ? db.dbAction(...args) : db.default.dbAction(...args) +} diff --git a/src/app/lib/deep-link.js b/src/app/lib/deep-link.js deleted file mode 100644 index 03026e8..0000000 --- a/src/app/lib/deep-link.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Deep link support for electerm - * Handles protocol URLs like ssh://, telnet://, rdp://, vnc://, etc. - */ - -const { app } = require('electron') -const log = require('../common/log') -const globalState = require('./glob-state') -const { parseQuickConnect } = require('../common/parse-quick-connect') - -/** - * Protocols registered as OS-level deep link handlers. - * http/https are intentionally excluded: registering them would make electerm - * the handler for every clicked web link, hijacking the user's default browser. - * They remain parseable via quick-connect (normalized to type "web"). - */ -const DEEP_LINK_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'ftp', 'electerm'] - -/** - * Register electerm as a handler for supported protocols - * Note: This makes electerm available as a handler but doesn't force it as default. - * Users can still choose their preferred app in system settings. - * - * @param {boolean} force - If true, register even if not packaged (for testing) - * @returns {object} - Status of registration for each protocol - */ -function registerDeepLink (force = false) { - const protocols = DEEP_LINK_PROTOCOLS - const results = {} - - // Only register in packaged app or when explicitly requested - const shouldRegister = app.isPackaged || - force || - process.env.ELECTERM_REGISTER_PROTOCOLS === '1' - - if (!shouldRegister) { - log.info('Skipping protocol registration in development mode') - log.info('Set ELECTERM_REGISTER_PROTOCOLS=1 or pass force=true to enable') - return { registered: false, reason: 'development-mode' } - } - - protocols.forEach(protocol => { - // Check if already registered - const isDefault = app.isDefaultProtocolClient(protocol) - - if (isDefault) { - log.info(`Already registered as handler for ${protocol}:// protocol`) - results[protocol] = { success: true, alreadyDefault: true } - } else { - const registered = app.setAsDefaultProtocolClient(protocol) - if (registered) { - log.info(`Registered as handler for ${protocol}:// protocol`) - results[protocol] = { success: true, alreadyDefault: false } - } else { - log.warn(`Failed to register ${protocol}:// protocol handler`) - results[protocol] = { success: false, error: 'registration-failed' } - } - } - }) - - return { registered: true, protocols: results } -} - -/** - * Check which protocols are currently registered - * @returns {object} - Status of each protocol - */ -function checkProtocolRegistration () { - const protocols = DEEP_LINK_PROTOCOLS - const status = {} - - protocols.forEach(protocol => { - status[protocol] = app.isDefaultProtocolClient(protocol) - }) - - return status -} - -/** - * Unregister electerm as handler for protocols - * @param {Array} protocols - Optional array of specific protocols to unregister - * @returns {object} - Status of unregistration - */ -function unregisterDeepLink (protocols = DEEP_LINK_PROTOCOLS) { - const results = {} - - protocols.forEach(protocol => { - const removed = app.removeAsDefaultProtocolClient(protocol) - results[protocol] = removed - if (removed) { - log.info(`Unregistered as handler for ${protocol}:// protocol`) - } else { - log.warn(`Failed to unregister ${protocol}:// protocol handler`) - } - }) - - return results -} - -/** - * Handle deep link URL by opening a new tab - * @param {string} url - The protocol URL - */ -function handleDeepLink (url) { - const parsed = parseQuickConnect(url) - - if (!parsed) { - log.warn('Could not parse deep link URL:', url) - return - } - - const win = globalState.get('win') - - if (win) { - // If window exists, send message to open new tab - if (win.isMinimized()) { - win.restore() - } - win.focus() - win.webContents.send('open-tab', parsed) - } else { - // Store the URL to open when window is ready - globalState.set('pendingDeepLink', parsed) - } -} - -/** - * Check if there's a pending deep link to open - * @returns {object|null} - Pending deep link in the same format as initCommandLine or null - */ -function getPendingDeepLink () { - const pending = globalState.get('pendingDeepLink') - if (pending) { - globalState.set('pendingDeepLink', null) - return pending - } - return null -} - -/** - * Setup deep link handlers for the app - */ -function setupDeepLinkHandlers () { - // Note: second-instance and process.argv protocol URL handling is done by - // single-instance.js (socket-based IPC → add-tab-from-command-line) and - // command-line.js (initCommandLine → addTabFromCommandLine) respectively. - // Handling them here too would cause duplicate tabs to open. -} - -module.exports = { - registerDeepLink, - unregisterDeepLink, - checkProtocolRegistration, - handleDeepLink, - getPendingDeepLink, - setupDeepLinkHandlers -} diff --git a/src/app/lib/enc.js b/src/app/lib/enc.js index b889766..dc8cf87 100644 --- a/src/app/lib/enc.js +++ b/src/app/lib/enc.js @@ -1,13 +1,15 @@ /** * data encrypt/decrypt * - * New format (GCM): 'gcm::::' - * Legacy format: '' (pure hex, no colons — aes-192-cbc) + * New format (GCM): 'gcm::::' + * Legacy format: '' (pure hex, no colons — aes-192-cbc) * * decrypt/decryptAsync detect the format automatically via the 'gcm:' prefix, * so old data encrypted with the static IV/salt continues to work without migration. */ +import crypto from 'crypto' + const algorithmDefault = 'aes-256-gcm' // Legacy constants — kept only for decrypting old data (aes-192-cbc) @@ -19,8 +21,9 @@ const IV_LENGTH = 12 // 12 bytes is recommended for GCM const SALT_LENGTH = 16 const KEY_LENGTH = 32 // aes-256 requires a 32-byte key +const funcs = {} + function scryptAsync (...args) { - const crypto = require('crypto') return new Promise((resolve, reject) => crypto.scrypt(...args, (err, result) => { if (err) { @@ -31,12 +34,11 @@ function scryptAsync (...args) { ) } -exports.encrypt = function ( +funcs.encrypt = function ( str = '', password, algorithm = algorithmDefault ) { - const crypto = require('crypto') const iv = crypto.randomBytes(IV_LENGTH) const salt = crypto.randomBytes(SALT_LENGTH) const key = crypto.scryptSync(password, salt, KEY_LENGTH) @@ -47,12 +49,11 @@ exports.encrypt = function ( return 'gcm:' + iv.toString('hex') + ':' + salt.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted } -exports.decrypt = function ( +funcs.decrypt = function ( encrypted = '', password, algorithm = algorithmDefault ) { - const crypto = require('crypto') if (encrypted.startsWith('gcm:')) { // New format: gcm:iv_hex:salt_hex:authtag_hex:ciphertext_hex const parts = encrypted.split(':') @@ -75,12 +76,11 @@ exports.decrypt = function ( return decrypted } -exports.encryptAsync = async function ( +export const encryptAsync = async function ( str = '', password, algorithm = algorithmDefault ) { - const crypto = require('crypto') const iv = crypto.randomBytes(IV_LENGTH) const salt = crypto.randomBytes(SALT_LENGTH) const key = await scryptAsync(password, salt, KEY_LENGTH) @@ -91,12 +91,11 @@ exports.encryptAsync = async function ( return 'gcm:' + iv.toString('hex') + ':' + salt.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted } -exports.decryptAsync = async function ( +export const decryptAsync = async function ( encrypted = '', password, algorithm = algorithmDefault ) { - const crypto = require('crypto') if (encrypted.startsWith('gcm:')) { // New format: gcm:iv_hex:salt_hex:authtag_hex:ciphertext_hex const parts = encrypted.split(':') diff --git a/src/app/lib/error-page.js b/src/app/lib/error-page.js deleted file mode 100644 index 4d6e3cc..0000000 --- a/src/app/lib/error-page.js +++ /dev/null @@ -1,70 +0,0 @@ -// Function to generate the error HTML string -function generateErrorHtml (port) { - return ` - - - - - - Connection Error - - - -
-

Connection Issue Detected

-

Unable to connect to the local server at http://127.0.0.1:${port}. This is often caused by applications (such as proxy software, VPNs, or network tools) intercepting or blocking localhost (127.0.0.1) traffic.

-

Suggested fixes:

-
    -
  • Check if proxy software (e.g., Proxifier) is running. Ensure it excludes localhost (127.0.0.1) or this app's executable from proxying.
  • -
  • Verify that VPNs or other network tools are not redirecting localhost traffic.
  • -
  • Check firewall rules or antivirus software that might block local ports.
  • -
-

Restart the app after making changes. If the problem persists, contact author: zxdong@gmail.com.

-
- -
-

检测到连接问题

-

无法连接到本地服务器 http://127.0.0.1:${port}。这通常是由于应用程序(如代理软件、VPN 或网络工具)拦截或阻止了本地 (127.0.0.1) 流量。

-

建议的解决方法:

-
    -
  • 检查是否正在运行代理软件(如 Proxifier)。确保其设置排除本地连接 (127.0.0.1) 或此应用程序的可执行文件。
  • -
  • 确认 VPN 或其他网络工具未重定向本地流量。
  • -
  • 检查防火墙规则或防病毒软件是否阻止了本地端口。
  • -
-

更改设置后重启应用程序。如果问题仍然存在,请联系作者:zxdong@gmail.com。

-

- -

-
- - - ` -} - -module.exports = generateErrorHtml diff --git a/src/app/lib/extensions.js b/src/app/lib/extensions.js new file mode 100644 index 0000000..9abc3dd --- /dev/null +++ b/src/app/lib/extensions.js @@ -0,0 +1,17 @@ +import { + jwtAuth, + errHandler +} from './jwt.js' +import { + getConf +} from './conf.js' +export async function applyExtensions (app) { + const conf = await getConf() + if (conf && conf.extensions && conf.extensions.length) { + for (const ext of conf.extensions) { + if (ext && ext.appExtend) { + ext.appExtend(app, jwtAuth, errHandler) + } + } + } +} diff --git a/src/app/lib/fancy-console.js b/src/app/lib/fancy-console.js new file mode 100644 index 0000000..77fa574 --- /dev/null +++ b/src/app/lib/fancy-console.js @@ -0,0 +1,181 @@ +/** + * Fancy console logging utilities with colors and decorations + */ + +// ANSI color codes +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + + // Text colors + black: '\x1b[30m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', + + // Background colors + bgBlack: '\x1b[40m', + bgRed: '\x1b[41m', + bgGreen: '\x1b[42m', + bgYellow: '\x1b[43m', + bgBlue: '\x1b[44m', + bgMagenta: '\x1b[45m', + bgCyan: '\x1b[46m', + bgWhite: '\x1b[47m' +} + +// Emoji collections +const emoji = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️', + rocket: '🚀', + lightning: '⚡', + gear: '⚙️', + package: '📦', + sparkles: '✨', + fire: '🔥', + folder: '📁', + file: '📄', + arrow: '➤', + bullet: '•', + star: '⭐', + hourglass: '⏳', + checkmark: '✔️', + cross: '✖️', + diamond: '💎', + heart: '❤️', + thumbsUp: '👍' +} + +/** + * Create a fancy box with title and content + * @param {string} title - Box title + * @param {string[]} lines - Content lines + * @param {object} options - Styling options + */ +export function fancyBox (title, lines = [], options = {}) { + const { + width = 80, + color = colors.cyan, + titleColor = colors.yellow, + borderChar = '═', + lineChar = '─' + } = options + + const titleLine = `${titleColor}${title}${colors.reset}` + const topBorder = borderChar.repeat(width) + const bottomBorder = borderChar.repeat(width) + + console.log('\n' + color + topBorder + colors.reset) + console.log(titleLine) + console.log(color + topBorder + colors.reset) + + lines.forEach(line => { + console.log(line) + }) + + if (lines.length > 0) { + console.log(color + lineChar.repeat(width) + colors.reset) + } + console.log(color + bottomBorder + colors.reset + '\n') +} + +/** + * Log success message with decoration + */ +export function success (message, details = []) { + fancyBox(`${emoji.success} SUCCESS`, [ + `${colors.green}${message}${colors.reset}`, + ...details + ], { color: colors.green, titleColor: colors.bright + colors.green }) +} + +/** + * Log error message with decoration + */ +export function error (message, details = []) { + fancyBox(`${emoji.error} ERROR`, [ + `${colors.red}${message}${colors.reset}`, + ...details + ], { color: colors.red, titleColor: colors.bright + colors.red }) +} + +/** + * Log warning message with decoration + */ +export function warning (message, details = []) { + fancyBox(`${emoji.warning} WARNING`, [ + `${colors.yellow}${message}${colors.reset}`, + ...details + ], { color: colors.yellow, titleColor: colors.bright + colors.yellow }) +} + +/** + * Log info message with decoration + */ +export function info (message, details = []) { + fancyBox(`${emoji.info} INFO`, [ + `${colors.cyan}${message}${colors.reset}`, + ...details + ], { color: colors.cyan, titleColor: colors.bright + colors.cyan }) +} + +/** + * Log migration notice with special styling + */ +export function migrationNotice (version, oldDb, newDb, command) { + const lines = [ + `${colors.cyan}${emoji.gear} Since ${version}, electerm-web uses ${newDb} for better performance and stability.${colors.reset}`, + `${colors.yellow}${emoji.package} Old ${oldDb} database detected!${colors.reset}`, + `${colors.green}${emoji.sparkles} Please migrate your data to ${newDb} for enhanced performance and stability.${colors.reset}`, + '', + `${colors.magenta}${emoji.arrow} MIGRATION COMMAND:${colors.reset}`, + `${colors.white} ${command}${colors.reset}`, + `${colors.cyan}${emoji.file} Then import data.json in the new version via data sync panel${colors.reset}` + ] + + fancyBox(`${emoji.lightning} ELECTERM-WEB MIGRATION NOTICE ${emoji.lightning}`, lines, { + color: colors.magenta, + titleColor: colors.bright + colors.yellow + }) +} + +/** + * Log startup message with ASCII art + */ +export function startup (appName, version, port) { + const art = [ + ' ___________ __ ', + ' / ____/ / /__ _____ ____ / /_ ___ ______ ___ ', + ' / __/ / / / _ \\/ ___/ / __ `/ __/ _ \\/ ___/ __ `__ \\ ', + ' / /___/ / / __/ / / /_/ / /_/ __/ / / / / / / / ', + '/_____/_/_/\\___/_/ \\__,_/\\__/\\___/_/ /_/ /_/ /_/ ' + ] + + console.log('\n' + colors.cyan + '═'.repeat(60) + colors.reset) + art.forEach(line => { + console.log(colors.bright + colors.blue + line + colors.reset) + }) + console.log(colors.cyan + '═'.repeat(60) + colors.reset) + console.log(`${colors.yellow}${emoji.rocket} ${appName} v${version}${colors.reset}`) + console.log(`${colors.green}${emoji.gear} Running on port ${port}${colors.reset}`) + console.log(colors.cyan + '═'.repeat(60) + colors.reset + '\n') +} + +/** + * Simple colored log + */ +export function colorLog (message, color = colors.white) { + console.log(`${color}${message}${colors.reset}`) +} + +// Export colors and emoji for direct use +export { colors, emoji } diff --git a/src/app/lib/file-server.js b/src/app/lib/file-server.js deleted file mode 100644 index 3e5f471..0000000 --- a/src/app/lib/file-server.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = (app) => { - const express = require('express') - const path = require('path') - - return new Promise((resolve) => { - const assetsPath = path.resolve(__dirname, '../assets') - const conf = { - maxAge: 1000 * 60 * 60 * 24 * 365 - } - - // Handle _temp_*.css files - return empty CSS to prevent MIME type errors - app.use((req, res, next) => { - if (req.url.startsWith('/css/_temp_') && req.url.endsWith('.css')) { - res.setHeader('Content-Type', 'text/css') - res.send('') - return - } - next() - }) - - app.use( - express.static(assetsPath, conf) - ) - }) -} diff --git a/src/app/lib/font-list.js b/src/app/lib/font-list.js index d9d6e3f..2f7c938 100644 --- a/src/app/lib/font-list.js +++ b/src/app/lib/font-list.js @@ -1,17 +1,35 @@ /** * load font list after start */ +import log from '../common/log.js' -const log = require('../common/log') +// `font-list` (a native-ish module) may be absent on some platforms (e.g. the +// Android runtime). Load it lazily and tolerate its absence so the server can +// still start. +let fontsPromise = null +function loadGetFonts () { + if (!fontsPromise) { + fontsPromise = import('font-list') + .then(m => m.getFonts) + .catch(err => { + log.warn('font-list is not available:', err.message) + return null + }) + } + return fontsPromise +} -exports.loadFontList = () => { - return require('font-list').getFonts() - .then(fonts => { - return fonts.map(f => f.replace(/"/g, '')) - }) - .catch(err => { - log.error('load font list error') - log.error(err) - return [] - }) +export const loadFontList = async () => { + const getFonts = await loadGetFonts() + if (!getFonts) { + return [] + } + try { + const fonts = await getFonts() + return fonts.map(f => f.replace(/"/g, '')) + } catch (err) { + log.error('load font list error') + log.error(err) + return [] + } } diff --git a/src/app/lib/fs.js b/src/app/lib/fs.js index 9dc728e..a1f5357 100644 --- a/src/app/lib/fs.js +++ b/src/app/lib/fs.js @@ -1,45 +1,18 @@ -const fss = require('fs/promises') -const fs = require('fs') -const log = require('../common/log') -const path = require('path') -const { tempDir } = require('../common/runtime-constants') -const uid = require('../common/uid') -const { promisify } = require('util') -const { exec, spawn } = require('child_process') +import fs, { promises as fss } from 'fs' +import log from '../common/log.js' +import { isWin, isMac, tempDir } from '../common/runtime-constants.js' +import path from 'path' +import uid from '../common/uid.js' +import { promisify } from 'util' +import * as tar from 'tar' +import { getSizeCount, getSizeCountWin } from '../common/count-folder-data.js' +import { exec, spawn } from 'child_process' const execAsync = promisify(exec) -const { getSizeCount } = require('../common/get-folder-size-and-file-count.js') -// Encoding function -function encodeUint8Array (uint8Arr) { - return Buffer.from(uint8Arr).toString('base64') -} +const ROOT_PATH = '/' -// Decoding function -function decodeBase64String (base64String) { - return new Uint8Array(Buffer.from(base64String, 'base64')) -} - -/** - * run cmd - * @param {string} cmd - */ -const run = (cmd) => { - const { Bash } = require('node-bash') - const ps = new Bash({ - executableOptions: { - '--login': true - } - }) - return ps.invokeCommand(cmd) - .then(s => s.stdout.toString()) -} - -/** - * run windows cmd - * @param {string} cmd - */ -const runWinCmd = (cmd) => { - return execAsync(`powershell.exe -Command "${cmd}"`) +function encodeUtf8Base64 (value) { + return Buffer.from(String(value), 'utf8').toString('base64') } function spawnDetachedCommand (command, args, options = {}) { @@ -83,6 +56,62 @@ function spawnDetachedCommand (command, args, options = {}) { }) } +// Encoding function +function encodeUint8Array (uint8Arr) { + return Buffer.from(uint8Arr).toString('base64') +} + +// Decoding function +function decodeBase64String (base64String) { + return new Uint8Array(Buffer.from(base64String, 'base64')) +} + +const isWinDrive = function (path) { + return /^\w+:$/.test(path) +} + +// `node-bash` (a native-ish module) is not available on every platform +// (e.g. the Android runtime). Load it lazily and tolerate its absence so the +// server can still start; callers that need a local shell get a clear error. +let bashPromise = null +function loadBash () { + if (!bashPromise) { + bashPromise = import('node-bash') + .then(m => m.Bash) + .catch(err => { + log.warn('node-bash is not available, local shell features will be limited:', err.message) + return null + }) + } + return bashPromise +} + +/** + * run cmd + * @param {string} cmd + */ +const run = async (cmd) => { + const Bash = await loadBash() + if (!Bash) { + throw new Error('Local shell (node-bash) is not available on this platform') + } + const ps = new Bash({ + executableOptions: { + '--login': true + } + }) + return ps.invokeCommand(cmd) + .then(s => s.stdout.toString()) +} + +/** + * run windows cmd + * @param {string} cmd + */ +const runWinCmd = (cmd) => { + return execAsync(`powershell.exe -Command "${cmd}"`) +} + /** * Escape a string for safe use inside POSIX single quotes. * Within single quotes the only special character is the single quote itself; @@ -93,7 +122,25 @@ function escapePosixShellArg (value) { return String(value).replace(/'/g, "'\\''") } +/** + * Escape a string for safe use inside PowerShell single-quoted strings. + * Single quotes are escaped by doubling them: ' -> '' + */ +function escapePowerShellArg (value) { + return String(value).replace(/'/g, "''") +} + +function getFolderSizeWin (folderPath) { + const safePath = escapePowerShellArg(folderPath) + return runWinCmd( + `Get-ChildItem -Path '${safePath}' -Recurse | Where-Object { ! $_.PSIsContainer } | Measure-Object -Property Length -Sum` + ).then(res => getSizeCountWin(res.stdout)) +} + function getFolderSize (folderPath) { + if (isWin) { + return getFolderSizeWin(folderPath) + } const safePath = escapePosixShellArg(folderPath) return run(`du -sh '${safePath}' && find '${safePath}' -type f | wc -l`) .then(getSizeCount) @@ -167,7 +214,22 @@ const touch = (localFilePath) => { * @param {string} localFolderPath absolute path */ const openFile = (localFilePath) => { - return spawnDetachedCommand('xdg-open', [localFilePath]) + if (isWin) { + const script = '$path = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:ELECTERM_OPEN_FILE_PATH_B64)); Invoke-Item -LiteralPath $path' + return spawnDetachedCommand('powershell.exe', [ + '-NoLogo', + '-NonInteractive', + '-Command', + script + ], { + windowsHide: true, + env: { + ...process.env, + ELECTERM_OPEN_FILE_PATH_B64: encodeUtf8Base64(localFilePath) + } + }) + } + return spawnDetachedCommand(isMac ? 'open' : 'xdg-open', [localFilePath]) } /** @@ -179,7 +241,6 @@ const zipFolder = (localFolerPath) => { const p = path.resolve(tempDir, `electerm-temp-${n}.tar`) const cwd = path.dirname(localFolerPath) const file = path.basename(localFolerPath) - const tar = require('tar') return tar.c({ gzip: false, file: p, @@ -188,17 +249,77 @@ const zipFolder = (localFolerPath) => { .then(() => p) } +const handleWindowsDrive = async (localFilePath, targetFolderPath) => { + const tempExtractDir = path.join(tempDir, `electerm-unzip-${uid()}`) + await fss.mkdir(tempExtractDir, { recursive: true }) + + try { + await tar.x({ file: localFilePath, C: tempExtractDir }) + const items = await fss.readdir(tempExtractDir) + + await Promise.all(items.map(async (item) => { + const from = path.join(tempExtractDir, item) + const to = path.join(targetFolderPath, item) + await mv(from, to) + })) + } finally { + await rmrf(tempExtractDir).catch(log.error) + } +} + /** * unzip file * @param {string} localFilePath absolute path of a zip file * @param {string} targetFolderPath absolute path of unzip target folder */ const unzipFile = async (localFilePath, targetFolderPath) => { - const tar = require('tar') - await tar.x({ file: localFilePath, C: targetFolderPath }) + if (isWin && isWinDrive(targetFolderPath)) { + await handleWindowsDrive(localFilePath, targetFolderPath) + } else { + await tar.x({ file: localFilePath, C: targetFolderPath }) + } return 1 } +async function listWindowsRootPath () { + const drives = await new Promise((resolve, reject) => { + const command = 'powershell.exe -Command "Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Root"' + + exec(command, { encoding: 'utf8' }, (error, stdout, stderr) => { + if (error) { + reject(error) + return + } + if (stderr) { + reject(new Error(stderr)) + return + } + const drives = stdout.split('\r\n') + .map(line => line.trim()) + // Accept any valid Windows path that ends with backslash + .filter(line => /^[^<>:"/\\|?*]+:\\$/.test(line)) + .map(drive => drive.slice(0, -1)) // Remove trailing backslash + resolve(drives) + }) + }) + const distros = await listWslDistros() + return [...drives, ...distros] +} + +async function listWslDistros () { + try { + const { stdout } = await execAsync('wsl.exe -l -q', { encoding: 'buffer' }) + const output = Buffer.from(stdout).toString('utf16le').replace(/^\uFEFF/, '') + const distros = output.split(/\r?\n/) + .map(line => line.trim()) + .filter(Boolean) + .map(name => '\\\\wsl.localhost\\' + name) + return distros + } catch { + return [] + } +} + const readCustom = (p1, len, ...args) => { return new Promise((resolve, reject) => { fs.read(p1, new Uint8Array(len), ...args, (err, n, buffer) => { @@ -251,29 +372,59 @@ const statCustom = async (...args) => { return st } -const fsExport = Object.assign( +const readdirOnly = async (path) => { + const r = await fss.readdir(path, { withFileTypes: true }) + return r.filter(dirent => dirent.isDirectory()) + .map(d => { + return { + name: d.name, + isDirectory: true + } + }) +} + +const readdirAndFiles = async (path) => { + const r = await fss.readdir(path, { withFileTypes: true }) + return r.map(d => { + return { + name: d.name, + isDirectory: d.isDirectory() + } + }) +} + +export const fsExport = Object.assign( {}, fss, { - getFolderSize, run, + getFolderSize, runWinCmd, rmrf, touch, cp, mv, openFile, - zipFolder, - unzipFile, readCustom, - writeCustom, + statCustom, openCustom, closeCustom, - statCustom + writeCustom, + zipFolder, + unzipFile, + readdirOnly, + readdirAndFiles }, { readdirAsync: (_path) => { - return fss.readdir(_path) + if (_path === ROOT_PATH && isWin) { + return listWindowsRootPath() + } + let path = _path + if (isWin && isWinDrive(path)) { + path = path + '\\' + } + return fss.readdir(path) }, statAsync: (...args) => { return fss.stat(...args) @@ -313,7 +464,3 @@ const fsExport = Object.assign( } } ) - -module.exports = { - fsExport -} diff --git a/src/app/lib/get-config.js b/src/app/lib/get-config.js deleted file mode 100644 index 670629a..0000000 --- a/src/app/lib/get-config.js +++ /dev/null @@ -1,49 +0,0 @@ -const { dbAction } = require('./db') -const defaultSetting = require('../common/config-default') -const getPort = require('./get-port') -const { userConfigId, userNoEncryptConfigId } = require('../common/constants') -const generate = require('../common/uid') -const globalState = require('./glob-state') - -exports.getConfig = async (inited) => { - const userConfig = await dbAction('data', 'findOne', { - _id: userConfigId - }) || {} - const requireAuth = userConfig.hashedPassword - delete userConfig._id - delete userConfig.host - delete userConfig.terminalTypes - delete userConfig.tokenElecterm - delete userConfig.hashedPassword - delete userConfig.salt - const port = inited - ? globalState.get('config').port - : await getPort() - const config = { - ...defaultSetting, - ...userConfig, - requireAuth, - port, - tokenElecterm: inited ? globalState.get('config').tokenElecterm : generate() - } - // HarmonyOS: always use system title bar to avoid double title bar - config.useSystemTitleBar = true - return { - userConfig, - config - } -} - -exports.getDbConfig = async () => { - const userConfig = await dbAction('data', 'findOne', { - _id: userConfigId - }) || {} - return userConfig -} - -exports.getUserConfigNoEnc = async () => { - const userConfig = await dbAction('data', 'findOne', { - _id: userNoEncryptConfigId - }) || {} - return userConfig -} diff --git a/src/app/lib/get-constants.js b/src/app/lib/get-constants.js new file mode 100644 index 0000000..dc23d1a --- /dev/null +++ b/src/app/lib/get-constants.js @@ -0,0 +1,84 @@ +/** + * ipc main + */ + +import * as constants from '../common/runtime-constants.js' +import { transferKeys } from '../server/transfer.js' +import fs from 'fs' +import os from 'os' +import _ from 'lodash' +import { sep } from 'path' +import { getConfig } from './init.js' +import copy from 'json-deep-copy' +const allowList = new Set([ + 'SHELL', 'TERM', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'COLORTERM', + 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_TERMINAL', 'LC_TERMINAL_VERSION', + 'HOME', 'USER', 'LOGNAME', 'USERNAME', + 'PATH', 'PATHEXT', + 'TMPDIR', 'TMP', 'TEMP', + 'DISPLAY', 'WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'XDG_RUNTIME_DIR', + 'XDG_DATA_DIRS', 'XDG_CONFIG_DIRS', 'XDG_CURRENT_DESKTOP', 'XDG_SEAT', 'XDG_VTNR', + 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'SSH_CLIENT', 'SSH_CONNECTION', 'SSH_TTY', + 'NODE_PATH', 'NODE_ENV', 'NVM_DIR', 'NVM_BIN', + 'NPM_CONFIG_PREFIX', 'NPM_CONFIG_CACHE', + 'GIT_EDITOR', 'GIT_PAGER', 'GIT_TERMINAL_PROMPT', + 'EDITOR', 'VISUAL', 'PAGER', + 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', + 'APPDATA', 'LOCALAPPDATA', 'ProgramFiles', 'ProgramFiles(x86)', 'CommonProgramFiles', + 'ComSpec', 'SystemRoot', 'SystemDrive', 'USERPROFILE', 'USERDOMAIN', + 'COMPUTERNAME', 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE', 'OS', + 'Apple_PubSub_Socket_Render', + 'DBUS_SESSION_BUS_ADDRESS', 'DESKTOP_SESSION', 'GNOME_DESKTOP_SESSION_ID', 'KDE_FULL_SESSION', + 'CI', 'DOCKER_HOST', 'CONTAINER' +]) + +export function getEnv (key) { + if (key) { + if (!allowList.has(key)) { + return '' + } + return process.env[key] + } + return Object.fromEntries( + Object.entries(process.env).filter(([k]) => allowList.has(k)) + ) +} + +export async function getConstants (req, res) { + const config = await getConfig(true) + const data = { + osInfoData: (() => { + return Object.keys(os).map((k, i) => { + const vf = os[k] + if (!_.isFunction(vf)) { + return null + } + let v + try { + v = vf() + } catch (e) { + return null + } + if (!v) { + return null + } + v = JSON.stringify(v, null, 2) + return { k, v } + }).filter(d => d) + })(), + config, + sep, + fsConstants: fs.constants, + ...constants, + env: (() => { + return Object.fromEntries( + Object.entries(process.env).filter(([k]) => allowList.has(k)) + ) + })(), + versions: copy(process.versions), + transferKeys + } + res.send( + data + ) +} diff --git a/src/app/lib/get-port.js b/src/app/lib/get-port.js deleted file mode 100644 index 1877c12..0000000 --- a/src/app/lib/get-port.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * get first free open port - */ - -const log = require('../common/log') -const globalState = require('./glob-state') -let port = null - -function getPort (fromPort = 30975) { - const serverPort = globalState.get('serverPort') - if (serverPort) { - port = parseInt(serverPort, 10) - return Promise.resolve( - port - ) - } - return new Promise((resolve, reject) => { - require('find-free-port')(fromPort, '127.0.0.1', function (err, freePort) { - if (err) { - reject(err) - } else { - port = freePort - resolve(freePort) - } - }) - }) -} - -module.exports = () => { - if (port) { - return port - } - return getPort() - .catch(e => { - log.error('failed to get free port') - return 0 - }) -} diff --git a/src/app/lib/glob-state.js b/src/app/lib/global-state.js similarity index 84% rename from src/app/lib/glob-state.js rename to src/app/lib/global-state.js index 53c954a..a23bffe 100644 --- a/src/app/lib/glob-state.js +++ b/src/app/lib/global-state.js @@ -16,12 +16,11 @@ class GlobalState { app: null, rawArgs: null, loadTime: null, - initTime: null, + initTime: Date.now(), watchFilePath: '', oldRectangle: null, serverPort: null, - isSecondInstance: false, - pendingDeepLink: null + isSecondInstance: false } } @@ -38,4 +37,4 @@ class GlobalState { } } -module.exports = new GlobalState() +export default new GlobalState() diff --git a/src/app/lib/init-app.js b/src/app/lib/init-app.js deleted file mode 100644 index a10e48f..0000000 --- a/src/app/lib/init-app.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * after data loaded, init menu and other things - */ - -const { - Menu, - Notification -} = require('electron') -const globalState = require('./glob-state') -const { - packInfo -} = require('../common/runtime-constants') - -function capitalizeFirstLetter (string) { - return string.charAt(0).toUpperCase() + string.slice(1) -} - -function initApp (langMap, config) { - globalState.set('langMap', langMap) - globalState.set('getLang', (lang = config.language || 'en_us') => { - return langMap[lang].lang - }) - globalState.set('translate', txt => { - const config = globalState.get('config') - if (config.language === 'en_us') { - return capitalizeFirstLetter( - globalState.get('getLang')()[txt] || txt - ) - } - return globalState.get('getLang')()[txt] || txt - }) - // Remove the desktop-style menu bar — all menu functionality - // (settings, about, etc.) is available in the web UI. - Menu.setApplicationMenu(null) - const e = globalState.get('translate') - // handle autohide flag - if (process.argv.includes('--autohide')) { - globalState.set('timer', setTimeout(() => globalState.get('win').minimize(), 500)) - if (Notification.isSupported()) { - const notice = new Notification({ - title: `${packInfo.name} ${e('isRunning')}, ${e('press')} ${config.hotkey} ${e('toShow')}` - }) - notice.show() - } - } -} - -module.exports = initApp diff --git a/src/app/lib/init-server.js b/src/app/lib/init-server.js deleted file mode 100644 index 58b1cd8..0000000 --- a/src/app/lib/init-server.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * server init script - * - * Starts the Express server in-process (no child process). - * Returns a promise that resolves when the server reports ready. - */ - -const createChildServer = require('../server/child-process') -const globalState = require('./glob-state') -const log = require('../common/log') - -const SERVER_TIMEOUT = 15000 // 15 seconds - -module.exports = async (config, env, sysLocale) => { - return new Promise((resolve, reject) => { - let resolved = false - let timer = null - - const child = createChildServer(config, env, sysLocale) - - timer = setTimeout(() => { - if (!resolved) { - resolved = true - log.error('Server init timed out after', SERVER_TIMEOUT, 'ms') - try { child.kill() } catch {} - reject(new Error('Server init timed out')) - } - }, SERVER_TIMEOUT) - - child.on('exit', (code, signal) => { - if (!resolved) { - resolved = true - if (timer) clearTimeout(timer) - reject(new Error(`Server exited with code ${code} signal ${signal}`)) - } - }) - - child.on('error', (err) => { - if (!resolved) { - resolved = true - if (timer) clearTimeout(timer) - reject(err) - } - }) - - globalState.set('childPid', child.pid) - globalState.set('child', child) - - child.on('message', (m) => { - if (m && m.serverInited && !resolved) { - resolved = true - if (timer) clearTimeout(timer) - resolve(child) - } - }) - }) -} diff --git a/src/app/lib/init.js b/src/app/lib/init.js new file mode 100644 index 0000000..22b9687 --- /dev/null +++ b/src/app/lib/init.js @@ -0,0 +1,47 @@ +/** + * ipc main + */ + +import defaultSetting from '../common/config-default.js' +import { userConfigId } from '../common/constants.js' +import { isDev } from '../common/runtime-constants.js' +import { dbAction } from './db.js' +import installSrc from './install-src.js' +import * as langMap from '@electerm/electerm-locales' + +export async function getConfig () { + const userConfig = await dbAction('data', 'findOne', { + _id: userConfigId + }) || {} + delete userConfig._id + delete userConfig.host + delete userConfig.terminalTypes + delete userConfig.tokenElecterm + const config = { + ...defaultSetting, + ...userConfig, + port: process.env.PORT, + host: process.env.HOST, + wsHost: isDev ? process.env.DEV_HOST : process.env.HOST, + wsPort: isDev ? process.env.DEV_PORT : process.env.PORT, + server: process.env.SERVER, + useSystemTitleBar: true + } + return config +} + +export async function init () { + const config = await getConfig(true) + return { + config, + isPortable: true, + installSrc, + langs: Object.keys(langMap).map(id => { + return { + id, + ...langMap[id] + } + }), + langMap + } +} diff --git a/src/app/lib/install-src.js b/src/app/lib/install-src.js index e70e9a8..32fa05d 100644 --- a/src/app/lib/install-src.js +++ b/src/app/lib/install-src.js @@ -1,3 +1,31 @@ -// export install src +// install-src.js +// Determines the Android APK architecture identifier at runtime. +// Used to match the correct release asset when checking/downloading upgrades. +// +// The Android APK splits produce four flavors: +// arm64-v8a -> Node.js os.arch() === 'arm64' +// armeabi-v7a -> Node.js os.arch() === 'arm' +// x86_64 -> Node.js os.arch() === 'x64' +// universal -> (ignored; the device CPU resolves to one of the above) +// +// We resolve at runtime from os.arch() so the same bundled code works for +// every split without a build-time injection step: the APK the user installed +// only contains the native libraries for its target ABI, so os.arch() always +// reflects the ABI that is actually running on device. -module.exports = 'harmony-os' +import os from 'os' + +const archMap = { + arm64: 'arm64-v8a', + arm: 'armeabi-v7a', + x64: 'x86_64', + // 32-bit x86 is virtually nonexistent on Android; treat it as x86_64 so + // upgrade matching still resolves to a real asset. + ia32: 'x86_64', + x32: 'x86_64' +} + +const arch = os.arch() +const installSrc = 'electerm-android-' + (archMap[arch] || 'arm64-v8a') + +export default installSrc diff --git a/src/app/lib/ipc-sync.js b/src/app/lib/ipc-sync.js deleted file mode 100644 index 1173161..0000000 --- a/src/app/lib/ipc-sync.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * ipc main - */ - -const { - shell, - clipboard -} = require('electron') -// const log = require('../common/log') -const constants = require('../common/runtime-constants') -const appProps = require('../common/app-props') -const windowMove = require('./window-drag-move.js') -const globalState = require('./glob-state') -const { transferKeys } = require('../server/transfer') -const os = require('os') -const { - isTest -} = appProps -const { - getScreenSize -} = require('./window-control') -const _ = require('./lodash.js') -const { getStorageKey } = require('./storage-key') - -const isMaximized = () => { - const { - width: widthMax, - height: heightMax, - x: sx, - y: sy - } = getScreenSize() - const win = globalState.get('win') - const { width, height, x, y } = win.getBounds() - return widthMax === width && - heightMax === height && - x === sx && - y === sy -} - -module.exports = { - getStorageKey, - nodePtyCheck: () => { - return false - // try { - // return !!require('node-pty') - // } catch (err) { - // log.error('Failed to load node-pty:', err) - // return false - // } - }, - windowMove, - readClipboard: () => { - return clipboard.readText() - }, - writeClipboard: str => { - clipboard.writeText(str) - }, - resolve: (...args) => require('path').resolve(...args), - join: (...args) => require('path').join(...args), - basename: (...args) => require('path').basename(...args), - showItemInFolder: (href) => { - shell.showItemInFolder(href) - }, - openExternal: (url) => { - shell.openExternal(url) - }, - getArgs: () => { - return globalState.get('rawArgs') - }, - shouldAuth: () => globalState.get('requireAuth'), - getLoadTime: () => { - return globalState.get('loadTime') - ? { loadTime: globalState.get('loadTime') } - : { initTime: globalState.get('initTime') } - }, - setLoadTime: (loadTime) => { - globalState.set('loadTime', loadTime) - }, - getInitTime: () => { - return globalState.get('initTime') - }, - isMaximized, - isSecondInstance: () => { - return isTest ? false : globalState.get('isSecondInstance') - }, - osInfo: () => { - return Object.keys(os).map((k, i) => { - const vf = os[k] - if (!_.isFunction(vf)) { - return null - } - let v - try { - v = vf() - } catch (e) { - return null - } - if (!v) { - return null - } - v = JSON.stringify(v, null, 2) - return { k, v } - }).filter(d => d) - }, - getInitLocale: () => { - const config = globalState.get('config') - const langMap = globalState.get('langMap') - return { - language: config?.language || constants.defaultLang, - langMap: langMap || {} - } - }, - getConstants: () => { - return { - sep: require('path').sep, - ...constants, - homeOrTmp: appProps.homeOrTmp, - versions: JSON.stringify(process.versions), - transferKeys, - fsFunctions: [ - 'run', - 'runWinCmd', - 'access', - 'statAsync', - 'lstatAsync', - 'cp', - 'mv', - 'mkdir', - 'touch', - 'chmod', - 'rename', - 'unlink', - 'rmrf', - 'readdirAsync', - 'readFile', - 'readFileAsBase64', - 'writeFile', - 'openFile', - 'zipFolder', - 'unzipFile', - 'readCustom', - 'exists', - 'readdir', - 'mkdir', - 'realpath', - 'statCustom', - 'openCustom', - 'closeCustom', - 'writeCustom', - 'getFolderSize' - ] - } - } -} diff --git a/src/app/lib/ipc.js b/src/app/lib/ipc.js deleted file mode 100644 index 977b989..0000000 --- a/src/app/lib/ipc.js +++ /dev/null @@ -1,279 +0,0 @@ -/** - * ipc main - */ - -const { - ipcMain, - app, - BrowserWindow, - dialog, - powerMonitor, - globalShortcut, - shell -} = require('electron') -const globalState = require('./glob-state') -const ipcSyncFuncs = require('./ipc-sync') -const { dbAction } = require('./db') -const { listItermThemes } = require('./iterm-theme') -const installSrc = require('./install-src') -const { getConfig } = require('./get-config') -const loadSshConfig = require('./ssh-config') -const { - listWidgets, - runWidget, - stopWidget, - runWidgetFunc -} = require('../widgets/load-widget') -const { - setPassword, - checkPassword -} = require('./auth') -const initServer = require('./init-server') -const { - getLang, - loadLocales -} = require('./locales') -const { saveUserConfig } = require('./user-config-controller') -const { changeHotkeyReg, initShortCut } = require('./shortcut') -const lastStateManager = require('./last-state') -const { - registerDeepLink, - unregisterDeepLink, - checkProtocolRegistration, - getPendingDeepLink -} = require('./deep-link') -const { - packInfo, - appPath, - exePath, - isPortable, - sshKeysPath -} = require('../common/app-props') -const { - getScreenSize, - maximize, - unmaximize -} = require('./window-control') -const { openFileWithEditor } = require('./open-file-with-editor') -const { loadFontList } = require('./font-list') -const { checkDbUpgrade, doUpgrade } = require('../upgrade') -const { listSerialPorts } = require('./serial-port') -const initApp = require('./init-app') -const { encryptAsync, decryptAsync } = require('./enc') -const { safeEncrypt, safeDecrypt } = require('./safe-storage') -const { initCommandLine } = require('./command-line') -const { watchFile, unwatchFile } = require('./watch-file') -const lookup = require('../common/lookup') -const { AIchat, AIchatWithTools, getStreamContent, stopStream } = require('./ai') - -// Security: whitelist of safe environment variables for Linux/Mac/Windows -const SAFE_ENV_KEYS = [ - 'SHELL', 'TERM', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'COLORTERM', - 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_TERMINAL', 'LC_TERMINAL_VERSION', - 'HOME', 'USER', 'LOGNAME', 'USERNAME', - 'PATH', 'PATHEXT', - 'TMPDIR', 'TMP', 'TEMP', - 'DISPLAY', 'WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'XDG_RUNTIME_DIR', - 'XDG_DATA_DIRS', 'XDG_CONFIG_DIRS', 'XDG_CURRENT_DESKTOP', 'XDG_SEAT', 'XDG_VTNR', - 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'SSH_CLIENT', 'SSH_CONNECTION', 'SSH_TTY', - 'NODE_PATH', 'NODE_ENV', 'NVM_DIR', 'NVM_BIN', - 'NPM_CONFIG_PREFIX', 'NPM_CONFIG_CACHE', - 'GIT_EDITOR', 'GIT_PAGER', 'GIT_TERMINAL_PROMPT', - 'EDITOR', 'VISUAL', 'PAGER', - 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', - 'APPDATA', 'LOCALAPPDATA', 'ProgramFiles', 'ProgramFiles(x86)', 'CommonProgramFiles', - 'ComSpec', 'SystemRoot', 'SystemDrive', 'USERPROFILE', 'USERDOMAIN', - 'COMPUTERNAME', 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE', 'OS', - 'Apple_PubSub_Socket_Render', - 'DBUS_SESSION_BUS_ADDRESS', 'DESKTOP_SESSION', 'GNOME_DESKTOP_SESSION_ID', 'KDE_FULL_SESSION', - 'CI', 'DOCKER_HOST', 'CONTAINER', - // HarmonyOS: app sandbox data directory (set by bootstrap.js) - 'DATA_PATH' -] - -// Security: the dynamic IPC bridges (runGlobalAsync / runSync) only dispatch to -// functions that are explicitly wired into the dispatch object as own properties. -// Checking `hasOwnProperty` (instead of a hand-maintained name list) means the -// allowlist can never drift from the real exports, and it blocks prototype-chain -// pivots like 'constructor', 'toString', '__proto__', 'hasOwnProperty' (CWE-863 / CWE-749). -function isExportedIpcFunc (obj, name) { - return Object.prototype.hasOwnProperty.call(obj, name) && typeof obj[name] === 'function' -} - -// Only the main app window's webContents may use the dynamic IPC bridges. This blocks -// any other renderer frame (webviews, popups, or an attacker page that navigated the -// window) from reaching runGlobalAsync / runSync (CWE-863 / CWE-749). -function isTrustedIpcSender (event) { - const win = globalState.get('win') - return !!win && event.sender === win.webContents -} - -async function initAppServer () { - const { - config - } = await getConfig(globalState.get('serverInited')) - const { - langs, - langMap, - sysLocale - } = await loadLocales() - const language = getLang(config, sysLocale, langs) - config.language = language - globalState.set('langMap', langMap) - if (!globalState.get('serverInited')) { - const child = await initServer(config, { - ...process.env, - appPath, - sshKeysPath - }, sysLocale) - child.on('message', (m) => { - if (m && m.showFileInFolder) { - shell.showItemInFolder(m.showFileInFolder) - } - }) - globalState.set('serverInited', true) - } - globalState.set('config', config) -} - -function initIpc () { - powerMonitor.on('resume', () => { - globalState.get('win').webContents.send('power-resume', null) - }) - async function init () { - const { - langs, - langMap - } = await loadLocales() - const config = globalState.get('config') - const globs = { - config, - langs, - langMap, - installSrc, - appPath, - exePath, - isPortable - } - initApp(langMap, config) - initShortCut(globalShortcut, globalState.get('win'), config) - return globs - } - - ipcMain.on('sync-func', (event, { name, args }) => { - if (!isTrustedIpcSender(event) || !isExportedIpcFunc(ipcSyncFuncs, name)) { - console.error('[security] blocked IPC call: ' + name) - return - } - event.returnValue = ipcSyncFuncs[name](...args) - }) - const asyncGlobals = { - confirmExit: () => { - globalState.set('confirmExit', true) - }, - setPassword, - checkPassword, - lookup, - loadSshConfig, - init, - listSerialPorts, - loadFontList, - doUpgrade, - checkDbUpgrade, - getExitStatus: () => globalState.get('exitStatus'), - setExitStatus: (status) => { - globalState.set('exitStatus', status) - }, - encryptAsync, - decryptAsync, - safeEncrypt: (str) => safeEncrypt(str), - safeDecrypt: (str) => safeDecrypt(str), - dbAction, - getScreenSize, - closeApp: (closeAction = '') => { - globalState.set('closeAction', closeAction) - const win = globalState.get('win') - win && win.close() - }, - exit: () => { - const win = globalState.get('win') - win && win.close() - }, - restart: (closeAction = '') => { - globalState.set('closeAction', '') - globalState.get('win').close() - app.relaunch() - }, - setCloseAction: (closeAction = '') => { - globalState.set('closeAction', closeAction) - }, - minimize: () => { - globalState.get('win').minimize() - }, - listItermThemes, - maximize, - unmaximize, - openDevTools: () => { - globalState.get('win').webContents.openDevTools() - }, - setWindowSize: (update) => { - lastStateManager.set('windowSize', update) - }, - saveUserConfig, - AIchat, - AIchatWithTools, - getStreamContent, - stopStream, - setTitle: (title) => { - const win = globalState.get('win') - win && win.setTitle(packInfo.name + ' - ' + title) - }, - setBackgroundColor: (color = '#33333300') => { - const win = globalState.get('win') - win && win.setBackgroundColor(color) - }, - changeHotkey: changeHotkeyReg(globalShortcut, globalState.get('win')), - initCommandLine, - watchFile, - unwatchFile, - openFileWithEditor, - listWidgets, - runWidget, - stopWidget, - runWidgetFunc, - registerDeepLink, - unregisterDeepLink, - checkProtocolRegistration, - getPendingDeepLink, - checkMigrate: () => false, - migrate: () => false, - getEnv: (key) => { - if (key) { - return SAFE_ENV_KEYS.includes(key) ? process.env[key] : '' - } - return Object.fromEntries( - SAFE_ENV_KEYS - .filter(k => process.env[k] !== undefined) - .map(k => [k, process.env[k]]) - ) - } - } - ipcMain.handle('async', (event, { name, args }) => { - if (!isTrustedIpcSender(event) || !isExportedIpcFunc(asyncGlobals, name)) { - console.error('[security] blocked IPC call: ' + name) - return - } - return asyncGlobals[name](...args) - }) - ipcMain.handle('show-open-dialog-sync', async (event, ...args) => { - const win = BrowserWindow.fromWebContents(event.sender) - return dialog.showOpenDialogSync(win, ...args) - }) - ipcMain.handle('show-save-dialog', async (event, ...args) => { - const win = BrowserWindow.fromWebContents(event.sender) - return dialog.showSaveDialog(win, ...args) - }) -} - -exports.initIpc = initIpc -exports.initAppServer = initAppServer diff --git a/src/app/lib/iterm-theme.js b/src/app/lib/iterm-theme.js index 1ac089e..6b2bdfc 100644 --- a/src/app/lib/iterm-theme.js +++ b/src/app/lib/iterm-theme.js @@ -2,10 +2,12 @@ * read themes from https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/electerm */ -exports.listItermThemes = async () => { - const all = require('@electerm/electerm-themes/dist/index.js') +import log from '../common/log.js' + +export async function listItermThemes (ws, msg) { + const all = await import('@electerm/electerm-themes/dist/index.mjs').then(d => d.default) return Promise.all(all).catch(e => { - console.log(e) + log.error('list Iterm Themes error', e) return [] }) } diff --git a/src/app/lib/jwt.js b/src/app/lib/jwt.js new file mode 100644 index 0000000..73b2014 --- /dev/null +++ b/src/app/lib/jwt.js @@ -0,0 +1,33 @@ +import { expressjwt } from 'express-jwt' +import jwtb from 'jsonwebtoken' + +export const jwtAuth = expressjwt({ + secret: process.env.SERVER_SECRET, + algorithms: ['HS256'], + getToken: function fromHeaderOrQuerystring (req) { + return req.headers.token + } +}) + +export const errHandler = function (err, req, res, next) { + if (err && err.name === 'UnauthorizedError') { + res.status(401).send('invalid token...') + } else { + next() + } +} + +export function createToken ( + user = process.env.SERVER_USER, + pass = process.env.SERVER_SECRET, + expire = process.env.TOKEN_EXPIRED_TIME || '120y' +) { + const x = jwtb.sign({ + id: user + }, pass, { expiresIn: expire }) + return x +} + +export function verify (token) { + return jwtb.verify(token, process.env.SERVER_SECRET) +} diff --git a/src/app/lib/key-bind.js b/src/app/lib/key-bind.js deleted file mode 100644 index 7f108dc..0000000 --- a/src/app/lib/key-bind.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * disable some default keyboard shortcuts - */ - -exports.disableShortCuts = function (win) { - win.webContents.on('before-input-event', (event, input) => { - if ( - input.key.toLowerCase() === 'r' && - input.control && input.shift - ) { - event.preventDefault() - } - }) -} diff --git a/src/app/lib/last-state.js b/src/app/lib/last-state.js deleted file mode 100644 index 8ea1985..0000000 --- a/src/app/lib/last-state.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * set/get app last state - */ - -const { dbAction } = require('./db') -const log = require('../common/log') -let count = 0 -const set = (key, value) => { - count = count + 1 - if (count > 100) { - count = 0 - dbAction('compactDatafile').catch(log.error) - } - return dbAction('lastStates', 'update', { - _id: key - }, { - _id: key, - value - }, { - upsert: true - }) -} - -const get = async (key) => { - const res = await dbAction('lastStates', 'findOne', { - _id: key - }) - .catch(e => { - log.error(e) - log.error('last state get error') - }) - return res ? res.value : null -} - -const clear = (key) => { - const q = key - ? { _id: key } - : {} - return dbAction('lastStates', 'remove', q) -} - -module.exports = { - set, - get, - clear -} diff --git a/src/app/lib/locales.js b/src/app/lib/locales.js deleted file mode 100644 index 9951f22..0000000 --- a/src/app/lib/locales.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * multi language support - */ - -const { isDev, defaultLang } = require('../common/runtime-constants') -const { resolve } = require('path') - -async function loadLocales () { - // No system-locale detection on HarmonyOS: every detection attempt - // (os-locale-s, @ohos.intl, @ohos.i18n) returned a wrong ("en") value on - // real devices. Default to Simplified Chinese (defaultLang); users can - // still switch language in Settings. - const sysLocale = defaultLang - const path = (isDev - ? '../../' - : '') + - '../node_modules/@electerm/electerm-locales/dist/cjs' - const localeFolder = resolve(__dirname, path) - // languages array - const langs = require(resolve(localeFolder, 'list.json')) - .map(fileName => { - const filePath = resolve(localeFolder, fileName) - const lang = require(filePath) - return { - path: filePath, - id: fileName.replace('.js', ''), - name: lang.name, - reg: lang.match, - lang: lang.lang - } - }) - const langMap = langs.reduce((prev, l) => { - prev[l.id] = l - return prev - }, {}) - return { - langs, - langMap, - sysLocale - } -} - -function findLang (langs, la) { - let res = false - for (const l of langs) { - res = new RegExp(l.reg).test(la) - if (res) { - res = l.id - break - } - } - return res -} - -const getLang = (config, sysLocale, langs) => { - if (config.language) { - return config.language - } - let l = sysLocale - l = l ? l.toLowerCase().replace('-', '_') : defaultLang - return findLang(langs, l) || defaultLang -} - -exports.getLang = getLang -exports.loadLocales = loadLocales diff --git a/src/app/lib/lodash.js b/src/app/lib/lodash.js deleted file mode 100644 index 8becf8b..0000000 --- a/src/app/lib/lodash.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Simple lodash replacement with only the functions needed by the app - * This replaces the full lodash library to reduce bundle size - */ - -/** - * Creates a debounced function that delays invoking func until after wait milliseconds - * have elapsed since the last time the debounced function was invoked. - */ -function debounce (func, wait, immediate) { - let timeout - return function executedFunction (...args) { - const later = () => { - timeout = null - if (!immediate) func.apply(this, args) - } - const callNow = immediate && !timeout - clearTimeout(timeout) - timeout = setTimeout(later, wait) - if (callNow) func.apply(this, args) - } -} - -/** - * Creates a throttled function that only invokes func at most once per every wait milliseconds. - */ -function throttle (func, wait, options = {}) { - let timeout - let previous = 0 - - const later = function () { - previous = options.leading === false ? 0 : Date.now() - timeout = null - func.apply(this, arguments) - } - - return function throttled (...args) { - const now = Date.now() - if (!previous && options.leading === false) previous = now - const remaining = wait - (now - previous) - - if (remaining <= 0 || remaining > wait) { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - previous = now - func.apply(this, args) - } else if (!timeout && options.trailing !== false) { - timeout = setTimeout(() => later.apply(this, args), remaining) - } - } -} - -/** - * Creates an object composed of the picked object properties. - */ -function pick (object, paths) { - const result = {} - const keys = Array.isArray(paths) ? paths : [paths] - - for (const key of keys) { - if (object && Object.prototype.hasOwnProperty.call(object, key)) { - result[key] = object[key] - } - } - - return result -} - -/** - * Checks if value is an empty object, collection, map, or set. - */ -function isEmpty (value) { - if (value == null) { - return true - } - - if (Array.isArray(value) || typeof value === 'string') { - return value.length === 0 - } - - if (value instanceof Map || value instanceof Set) { - return value.size === 0 - } - - if (typeof value === 'object') { - return Object.keys(value).length === 0 - } - - return false -} - -/** - * Checks if value is classified as an Array object. - */ -function isArray (value) { - return Array.isArray(value) -} - -/** - * Checks if value is classified as a Function object. - */ -function isFunction (value) { - return typeof value === 'function' -} - -module.exports = { - debounce, - throttle, - pick, - isEmpty, - isArray, - isFunction -} diff --git a/src/app/lib/login.js b/src/app/lib/login.js new file mode 100644 index 0000000..6b595ca --- /dev/null +++ b/src/app/lib/login.js @@ -0,0 +1,18 @@ +/** + * simple login with password only + */ + +import { createToken } from './jwt.js' + +const { + SERVER_PASS +} = process.env + +export function login (req, res) { + const { password } = req.body + if (password !== SERVER_PASS) { + return res.status(401).send('pass not right') + } + const token = createToken() + res.send(token) +} diff --git a/src/app/common/lookup.js b/src/app/lib/lookup.js similarity index 91% rename from src/app/common/lookup.js rename to src/app/lib/lookup.js index 2b99ab5..1db8526 100644 --- a/src/app/common/lookup.js +++ b/src/app/lib/lookup.js @@ -1,9 +1,9 @@ /** * dns lookup */ +import dns from 'dns' -module.exports = (host) => { - const dns = require('dns') +export default (host) => { const v4 = new Promise((resolve, reject) => { dns.resolve4(host, function (err, result) { if (err) { diff --git a/src/app/lib/nedb.js b/src/app/lib/nedb.js deleted file mode 100644 index d41288f..0000000 --- a/src/app/lib/nedb.js +++ /dev/null @@ -1,246 +0,0 @@ -/** - * nedb api wrapper - * Accepts appPath and defaultUserName as parameters to avoid electron dependency - */ - -const { resolve } = require('path') -const fs = require('fs') -const Datastore = require('@electerm/nedb') - -// ── HarmonyOS fix: monkey-patch nedb storage ────────────────────────── -// nedb's storage.js uses fs.fsync in crashSafeWriteFile (called during -// loadDatabase compaction). On HarmonyOS's sandbox filesystem, fs.fsync -// — especially on directories — can fail, causing loadDatabase to fail. -// The default onload handler throws, but process.on('uncaughtException') -// swallows it. executor.processBuffer() is never called, so the executor -// stays "not ready" and ALL DB operations are buffered forever. -// -// Fix: make flushToStorage treat fsync failures as non-fatal (best-effort). -const nedbStorage = require('@electerm/nedb/lib/storage') -const _origFlush = nedbStorage.flushToStorage - -nedbStorage.flushToStorage = function (options, callback) { - // Wrap the callback to make fsync failures non-fatal. - // On HarmonyOS the sandbox filesystem may not support fsync - // (especially on directories). The actual write/rename in - // crashSafeWriteFile still works; we just skip the fsync guarantee. - const wrappedCb = function () { - callback(null) - } - - _origFlush.call(nedbStorage, options, wrappedCb) -} - -// Tables whose stored data values should be encrypted at rest -const ENC_TABLES = new Set(['bookmarks', 'profiles', 'data', 'history', 'terminalCommandHistory', 'aiChatHistory']) - -// Within the 'data' table, only this specific record is encrypted -const DATA_ENC_ID = 'userConfig' - -// Prefix added to stored strings to mark them as encrypted -const ENC_PREFIX = 'enc:' - -function createDb (appPath, defaultUserName, { enc, dec } = {}) { - const db = {} - - const appDataPath = process.env.DATA_PATH || resolve(appPath, 'electerm') - - if (!fs.existsSync(appDataPath)) { - fs.mkdirSync(appDataPath, { recursive: true }) - } - - const dbDir = resolve(appDataPath, 'users', defaultUserName) - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } - - const reso = (name) => { - return resolve(dbDir, `electerm.${name}.nedb`) - } - const tables = [ - 'bookmarks', - 'bookmarkGroups', - 'addressBookmarks', - 'terminalThemes', - 'lastStates', - 'data', - 'quickCommands', - 'log', - 'dbUpgradeLog', - 'profiles', - 'workspaces', - 'history', - 'terminalCommandHistory', - 'aiChatHistory', - 'autoRunWidgets' - ] - - tables.forEach(table => { - const conf = { - filename: reso(table), - autoload: true, - // Custom onload handler: log errors but DON'T throw. - // If loadDatabase fails (e.g. compaction step), we still - // force the executor to "ready" so DB operations can proceed. - onload: (err) => { - if (err) { - // Force executor ready so buffered operations execute. - // The data was already loaded into memory before the - // compaction step (persistCachedDatabase) ran. - if (db[table] && db[table].executor && !db[table].executor.ready) { - db[table].executor.processBuffer() - } - } - } - } - db[table] = new Datastore(conf) - }) - - /** - * Encrypt a plain JSON string for storage. - * Returns the original string when encryption is not configured. - */ - function encryptData (jsonStr) { - if (!enc) return jsonStr - return ENC_PREFIX + enc(jsonStr) - } - - /** - * Decrypt a stored string back to plain JSON. - * Returns the original string when decryption is not configured or the - * value was stored without encryption. - */ - function decryptData (stored) { - if (!dec || !stored) return stored - if (!stored.startsWith(ENC_PREFIX)) return stored - return dec(stored.slice(ENC_PREFIX.length)) - } - - /** - * Returns true when a specific document in a specific table should be - * encrypted. The 'data' table is selective: only _id === 'userConfig'. - */ - function needsEnc (dbName, id) { - if (!enc) return false - if (dbName === 'data') return id === DATA_ENC_ID - return ENC_TABLES.has(dbName) - } - - /** - * Wrap a result document by decrypting its `data` field when needed. - * nedb stores the full document object directly, so we JSON-parse the - * serialised data field that was encrypted during writes. - */ - function decryptDoc (dbName, doc) { - if (!dec || !doc || !needsEnc(dbName, doc._id)) return doc - if (!doc._encdata) return doc - try { - const plain = decryptData(doc._encdata) - const parsed = JSON.parse(plain) - const { _encdata: _, ...rest } = doc - return { ...rest, ...parsed } - } catch (e) { - return doc - } - } - - /** - * Wrap a document for storage by encrypting its payload when needed. - */ - function encryptDoc (dbName, doc) { - if (!needsEnc(dbName, doc._id)) return doc - const { _id, ...payload } = doc - const jsonStr = JSON.stringify(payload) - const encrypted = encryptData(jsonStr) - return _id !== undefined ? { _id, _encdata: encrypted } : { _encdata: encrypted } - } - - const dbAction = (dbName, op, ...args) => { - if (op === 'compactDatafile') { - db[dbName].persistence.compactDatafile() - return - } - return new Promise((resolve, reject) => { - if (op === 'find') { - db[dbName][op](...args, (err, results) => { - if (err) return reject(err) - resolve((results || []).map(doc => decryptDoc(dbName, doc))) - }) - } else if (op === 'findOne') { - db[dbName][op](...args, (err, result) => { - if (err) return reject(err) - resolve(decryptDoc(dbName, result)) - }) - } else if (op === 'insert') { - const original = args[0] - const toInsert = Array.isArray(original) - ? original.map(d => encryptDoc(dbName, d)) - : encryptDoc(dbName, original) - db[dbName][op](toInsert, (err, inserted) => { - if (err) { - // Handle unique constraint violation by falling back to update, - // matching SQLite's INSERT OR REPLACE behavior - if (err.errorType === 'uniqueViolated') { - const items = Array.isArray(toInsert) ? toInsert : [toInsert] - const origItems = Array.isArray(original) ? original : [original] - let pending = items.length - const results = [] - items.forEach((item, i) => { - db[dbName].update({ _id: item._id }, item, { upsert: true }, (uErr) => { - if (uErr) { - return reject(uErr) - } - results[i] = { ...origItems[i], _id: item._id } - if (--pending === 0) { - resolve(Array.isArray(original) ? results : results[0]) - } - }) - }) - return - } - return reject(err) - } - // Return documents with original (unencrypted) fields + _id - if (Array.isArray(original)) { - const origArr = Array.isArray(inserted) ? inserted : [inserted] - resolve(origArr.map((ins, i) => ({ ...original[i], _id: ins._id }))) - } else { - resolve({ ...original, _id: inserted._id }) - } - }) - } else if (op === 'update') { - const [query, updateObj, options] = args - const qid = query._id || query.id - if (needsEnc(dbName, qid)) { - const newData = updateObj.$set || updateObj - const { _id: _ignored, ...payload } = newData - const encDoc = encryptDoc(dbName, { _id: qid, ...payload }) - const finalUpdate = updateObj.$set ? { $set: encDoc } : encDoc - db[dbName][op](query, finalUpdate, options || {}, (err, result) => { - if (err) return reject(err) - resolve(result) - }) - } else { - db[dbName][op](...args, (err, result) => { - if (err) return reject(err) - resolve(result) - }) - } - } else { - db[dbName][op](...args, (err, result) => { - if (err) return reject(err) - resolve(result) - }) - } - }) - } - - return { - dbAction, - tables - } -} - -module.exports = { - createDb -} diff --git a/src/app/lib/npm.js b/src/app/lib/npm.js index 1304f86..c6fd533 100644 --- a/src/app/lib/npm.js +++ b/src/app/lib/npm.js @@ -1,8 +1,9 @@ -const path = require('path') -const fs = require('fs') -const tar = require('tar') -const axios = require('axios') -const { pipeline } = require('stream/promises') +import path from 'path' +import fs from 'fs' +import * as tar from 'tar' +import axios from 'axios' +import { pipeline } from 'stream/promises' +import zlib from 'zlib' const npmRegistry = (process.env.NPM_REGISTRY || 'https://registry.npmjs.org').replace(/\/$/, '') @@ -15,11 +16,20 @@ async function fetchManifest (packageName) { async function extractTarball (tarballUrl, destDir) { const { data: stream } = await axios.get(tarballUrl, { responseType: 'stream' }) fs.mkdirSync(destDir, { recursive: true }) - await pipeline( - stream, - require('zlib').createGunzip(), - tar.extract({ cwd: destDir, strip: 1 }) - ) + try { + await pipeline( + stream, + zlib.createGunzip(), + tar.extract({ cwd: destDir, strip: 1 }) + ) + } catch (err) { + fs.rmSync(destDir, { recursive: true, force: true }) + throw err + } +} + +function isPackageInstalled (packageDir) { + return fs.existsSync(path.join(packageDir, 'package.json')) } async function installPackage (packageName, targetFolder, visited = new Set()) { @@ -30,7 +40,7 @@ async function installPackage (packageName, targetFolder, visited = new Set()) { visited.add(cacheKey) const packageDir = path.join(targetFolder, 'node_modules', packageName) - if (fs.existsSync(packageDir)) { + if (isPackageInstalled(packageDir)) { return } @@ -52,9 +62,9 @@ async function installPackage (packageName, targetFolder, visited = new Set()) { } } -exports.downloadPackage = async (packageName, targetFolder) => { +export async function downloadPackage (packageName, targetFolder) { const npmPath = path.join(targetFolder, 'node_modules', packageName) - if (fs.existsSync(npmPath)) { + if (isPackageInstalled(npmPath)) { return npmPath } diff --git a/src/app/lib/on-close.js b/src/app/lib/on-close.js deleted file mode 100644 index 9102551..0000000 --- a/src/app/lib/on-close.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * on close app - */ - -const { dbAction } = require('./db') -const log = require('../common/log') -const globalState = require('./glob-state') - -exports.getExitStatus = async () => { - const res = await dbAction('data', 'findOne', { - _id: 'exitStatus' - }) - return res && res.value ? res.value : '' -} - -exports.onClose = async function (e) { - const config = globalState.get('config') - if (config.confirmBeforeExit && globalState.get('closeAction')) { - const win = globalState.get('win') - win?.webContents.send( - 'confirm-exit', - globalState.get('closeAction') - ) - globalState.set('closeAction', '') - return e.preventDefault() - } - log.debug('Closing app') - // Clean up all terminal sessions - try { - const { cleanupTerminals } = require('../server/session-process') - cleanupTerminals() - } catch (e) {} - // Kill the main server mock - const child = globalState.get('child') - if (child && typeof child.kill === 'function') { - try { child.kill() } catch (e) {} - } - globalState.set('serverInited', false) - log.debug('Sessions and server cleaned up') - // await dbAction('data', 'update', { - // _id: 'exitStatus' - // }, { - // value: 'ok', - // _id: 'exitStatus' - // }, { - // upsert: true - // }) - // await dbAction('data', 'update', { - // _id: 'sessions' - // }, { - // value: null, - // _id: 'sessions' - // }, { - // upsert: true - // }) - // log.debug('session saved') - clearTimeout(globalState.get('timer')) - globalState.set('win', null) - const app = globalState.get('app') - app.quit && app.quit() -} diff --git a/src/app/lib/open-file-with-editor.js b/src/app/lib/open-file-with-editor.js deleted file mode 100644 index fc87919..0000000 --- a/src/app/lib/open-file-with-editor.js +++ /dev/null @@ -1,120 +0,0 @@ -const { spawn } = require('child_process') - -function parseEditorCommand (command = '') { - const input = String(command).trim() - if (!input) { - throw new Error('Editor command is required') - } - - const args = [] - let current = '' - let quote = '' - - for (let index = 0; index < input.length; index++) { - const char = input[index] - - if (quote) { - if (char === quote) { - quote = '' - } else if (char === '\\' && input[index + 1] === quote) { - current += quote - index++ - } else { - current += char - } - continue - } - - if (char === '"' || char === '\'') { - quote = char - continue - } - - if (/\s/.test(char)) { - if (current) { - args.push(current) - current = '' - } - continue - } - - current += char - } - - if (quote) { - throw new Error('Editor command contains an unmatched quote') - } - - if (current) { - args.push(current) - } - - if (!args.length) { - throw new Error('Editor command is required') - } - - return { - command: args[0], - args: args.slice(1) - } -} - -function spawnDetachedEditor (command, args, options = {}) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - detached: false, - stdio: ['ignore', 'ignore', 'pipe'], - ...options - }) - let stderr = '' - - child.stderr.on('data', data => { - stderr += data.toString() - }) - child.on('error', reject) - - let settled = false - const settle = (err) => { - if (settled) { - return - } - settled = true - clearTimeout(timer) - child.unref() - if (err) { - reject(err) - } else { - resolve() - } - } - - child.on('close', code => { - if (code !== 0) { - settle(new Error(stderr.trim() || `Editor exited with code ${code}`)) - } else { - settle(null) - } - }) - - const timer = setTimeout(() => settle(null), 5000) - }) -} - -function openFileWithEditor (filePath, editorCommand) { - const parsed = parseEditorCommand(editorCommand) - - const userShell = process.env.SHELL || '/bin/sh' - - return spawnDetachedEditor(userShell, [ - '-l', - '-i', - '-c', - 'exec "$0" "$@"', - parsed.command, - ...parsed.args, - filePath - ]) -} - -exports.openFileWithEditor = openFileWithEditor -exports.parseEditorCommand = parseEditorCommand diff --git a/src/app/lib/proxy-agent.js b/src/app/lib/proxy-agent.js index 3c3f496..5f872c4 100644 --- a/src/app/lib/proxy-agent.js +++ b/src/app/lib/proxy-agent.js @@ -1,5 +1,9 @@ +import { HttpsProxyAgent } from 'https-proxy-agent' +import { SocksProxyAgent } from 'socks-proxy-agent' +import { getSystemCAsList } from './system-ca.js' + // common proxy agent creator -exports.createProxyAgent = (url = '') => { +export const createProxyAgent = (url = '', options = {}) => { if ( typeof url !== 'string' || (!url.startsWith('http') && !url.startsWith('socks')) @@ -7,9 +11,13 @@ exports.createProxyAgent = (url = '') => { return } const Cls = url.startsWith('http') - ? require('https-proxy-agent').HttpsProxyAgent - : require('socks-proxy-agent').SocksProxyAgent + ? HttpsProxyAgent + : SocksProxyAgent + const certs = getSystemCAsList() + const caOptions = certs.length ? { ca: certs } : {} return new Cls(url, { - keepAlive: true + keepAlive: true, + ...caOptions, + ...options }) } diff --git a/src/app/lib/run-sync.js b/src/app/lib/run-sync.js new file mode 100644 index 0000000..bcbecb6 --- /dev/null +++ b/src/app/lib/run-sync.js @@ -0,0 +1,103 @@ +/** + * serial port lib + */ +import log from '../common/log.js' +import { listItermThemes } from '../lib/iterm-theme.js' +import { listSerialPorts } from '../lib/serial-port.js' +import { dbAction } from './db.js' +import { encryptAsync, decryptAsync } from '../lib/enc.js' +import { loadFontList } from './font-list.js' +import { loadSshConfig } from './ssh-config.js' +import { saveUserConfig } from './user-config.js' +import { checkDbUpgrade, doUpgrade } from '../upgrade/index.js' +import { watchFile, unwatchFile } from './watch-file.js' +import lookup from './lookup.js' +import { init } from './init.js' +import { showItemInFolder } from './show-item-in-folder.js' +import { AIchat, AIchatWithTools, getStreamContent, stopStream } from './ai.js' +import { + listWidgets, + runWidget, + stopWidget, + runWidgetFunc +} from '../widgets/load-widget.js' +import globalState from './global-state.js' +import { getEnv } from './get-constants.js' + +const globs = { + AIchat, + AIchatWithTools, + getStreamContent, + stopStream, + encryptAsync, + decryptAsync, + showItemInFolder, + dbAction, + lookup, + watchFile, + unwatchFile, + listSerialPorts, + checkDbUpgrade, + doUpgrade, + loadSshConfig, + listItermThemes, + init, + initCommandLine: () => Promise.resolve(0), + getInitTime: () => { + return globalState.get('initTime') + }, + loadFontList, + saveUserConfig, + registerDeepLink: () => Promise.resolve(1), + setWindowSize: () => Promise.resolve(1), + getScreenSize: () => Promise.resolve({ width: 1920, height: 1080 }), + checkMigrate: () => Promise.resolve(false), + setBackgroundColor: () => { + return Promise.resolve(1) + }, + listWidgets, + runWidget, + stopWidget, + runWidgetFunc, + getPendingDeepLink: () => Promise.resolve(null), + getEnv: () => Promise.resolve(getEnv()) +} + +export function runSync (ws, msg) { + const { + id, + func, + args = [] + } = msg + // console.log('runSync', func, args) + // Security: only dispatch to functions that are explicitly wired into + // the globs object as own properties. Checking hasOwnProperty (instead of + // a hand-maintained name list) means the allowlist can never drift from the + // real exports, and it blocks prototype-chain pivots like 'constructor', + // 'toString', '__proto__', 'hasOwnProperty' (CWE-863 / CWE-749). + if (!Object.prototype.hasOwnProperty.call(globs, func) || typeof globs[func] !== 'function') { + log.error('[security] blocked runSync call: ' + func) + ws.s({ + error: { + message: 'invalid function: ' + func, + stack: '' + }, + id + }) + return + } + globs[func](...args) + .then(data => { + ws.s({ + data, + id: msg.id + }) + }) + .catch(err => { + log.error(id, func, args, err) + ws.s({ + error: err, + id + }) + }) +} diff --git a/src/app/lib/safe-storage.js b/src/app/lib/safe-storage.js deleted file mode 100644 index 7579620..0000000 --- a/src/app/lib/safe-storage.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Safe storage wrapper using Node.js crypto (AES-256-GCM). - * - * Replaces Electron's safeStorage API which relies on OS-level key - * services (macOS Keychain, Windows DPAPI, Linux libsecret) — none of - * which are available on HarmonyOS. - * - * The encryption key is derived (via SHA-256) from STORAGE_SECRET: - * - In CI builds: build/harmony/build.js replaces the placeholder - * string with secrets.OHOS_SERVER_SECRET at build time - * - In local dev: uses the static placeholder string below - * - * Encrypted values are stored as base64 strings prefixed with SAFE_PREFIX - * so they can be distinguished from plain-text or legacy-encrypted values. - * - * Format: v2:safe: - */ - -const crypto = require('crypto') - -const SAFE_PREFIX = 'v2:safe:' -const ALGO = 'aes-256-gcm' -const IV_LEN = 12 // 96-bit IV recommended for GCM - -// Default fallback secret for local development. -// At build time, build/harmony/build.js replaces this string with -// the value of process.env.SERVER_SECRET (sourced from .env which -// prepare-web.sh sets from GitHub Secret OHOS_SERVER_SECRET). -const STORAGE_SECRET = 'static-secret-string-safe-storage' - -/** - * Derive a 32-byte key from the secret string via SHA-256. - * @returns {Buffer} - */ -function getKey () { - return crypto.createHash('sha256').update(STORAGE_SECRET).digest() -} - -/** - * Encrypt a string using AES-256-GCM. - * Returns the original string unchanged on error. - * @param {string} str - * @returns {string} - */ -exports.safeEncrypt = function (str) { - if (typeof str !== 'string' || !str) return str - try { - const key = getKey() - const iv = crypto.randomBytes(IV_LEN) - const cipher = crypto.createCipheriv(ALGO, key, iv) - const encrypted = Buffer.concat([ - cipher.update(str, 'utf8'), - cipher.final() - ]) - const authTag = cipher.getAuthTag() - return SAFE_PREFIX + [ - iv.toString('base64'), - encrypted.toString('base64'), - authTag.toString('base64') - ].join(':') - } catch (e) { - console.error('[safe-storage] encrypt error:', e.message) - return str - } -} - -/** - * Decrypt a string that was encrypted with safeEncrypt. - * Returns the original string unchanged when it was not produced by safeEncrypt. - * @param {string} str - * @returns {string} - */ -exports.safeDecrypt = function (str) { - if (typeof str !== 'string' || !str) return str - if (!str.startsWith(SAFE_PREFIX)) return str - try { - const payload = str.slice(SAFE_PREFIX.length) - const parts = payload.split(':') - if (parts.length !== 3) return str - const [ivB64, encB64, tagB64] = parts - const key = getKey() - const decipher = crypto.createDecipheriv( - ALGO, - key, - Buffer.from(ivB64, 'base64') - ) - decipher.setAuthTag(Buffer.from(tagB64, 'base64')) - const decrypted = Buffer.concat([ - decipher.update(Buffer.from(encB64, 'base64')), - decipher.final() - ]) - return decrypted.toString('utf8') - } catch (e) { - console.error('[safe-storage] decrypt error:', e.message) - return str - } -} diff --git a/src/app/lib/serial-port.js b/src/app/lib/serial-port.js index 8cb99aa..075cc7c 100644 --- a/src/app/lib/serial-port.js +++ b/src/app/lib/serial-port.js @@ -1,19 +1,13 @@ /** * serial port lib */ +import log from '../common/log.js' -exports.listSerialPorts = async function () { - return [] - // try { - // const start = Date.now() - // const r = await require('serialport').SerialPort.list() - // const end = Date.now() - // if (end - start < 100) { - // await new Promise(resolve => setTimeout(resolve, 100)) // wait for 100ms to avoid potential issues on some platforms - // } - // return r - // } catch (error) { - // console.error('Error listing serial ports:', error) - // return Promise.resolve([]) // Return an empty array on error - // } +export async function listSerialPorts () { + return import('serialport') + .then(({ SerialPort }) => SerialPort.list()) + .catch(err => { + log.error('SerialPort not available or failed to list ports:', err) + return [] + }) } diff --git a/src/app/lib/shortcut.js b/src/app/lib/shortcut.js deleted file mode 100644 index 3c383b8..0000000 --- a/src/app/lib/shortcut.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * shortcut controll - */ - -const log = require('../common/log') - -let shortcut - -/** - * init hotkey - * @param {object} globalShortcut - * @param {object} win - * @param {object} config - */ -exports.initShortCut = (globalShortcut, win, config) => { - shortcut = config.hotkey || '' - if (shortcut) { - globalShortcut.register(shortcut, () => { - if (win.isFocused()) { - win.minimize() - } else { - win.restore() - } - }) - const ok = globalShortcut.isRegistered(shortcut) - if (!ok) { - log.warn('shortcut Registration failed.') - } - } -} - -exports.changeHotkeyReg = (globalShortcut, win) => { - return newHotkey => { - if (shortcut) { - globalShortcut.unregister(shortcut) - } - if (newHotkey) { - globalShortcut.register(newHotkey, () => { - win.show() - }) - const ok = globalShortcut.isRegistered(newHotkey) - if (ok) { - shortcut = newHotkey - } - return ok - } else { - shortcut = '' - return true - } - } -} diff --git a/src/app/lib/show-item-in-folder.js b/src/app/lib/show-item-in-folder.js new file mode 100644 index 0000000..0159e05 --- /dev/null +++ b/src/app/lib/show-item-in-folder.js @@ -0,0 +1,39 @@ +import { exec } from 'child_process' +import { + isWin, + isMac +} from '../common/runtime-constants.js' +import { dirname, resolve } from 'path' + +export async function showItemInFolder (filePath) { + const itemPath = resolve(filePath) + const folderPath = dirname(itemPath) + let command = '' + + if (isWin) { + // For Windows + command = `explorer.exe /select,"${itemPath}"` + } else if (isMac) { + // For macOS + command = `open -R "${folderPath}"` + } else { + // For Linux or other Unix-like systems + command = `xdg-open "${folderPath}"` + } + + return new Promise((resolve) => { + // Best-effort: the file manager may be unavailable (e.g. Android, headless + // Linux). Never reject — "show in folder" is purely cosmetic and a missing + // handler must not crash or surface an unhandled rejection. + exec(command, (error, _stdout, stderr) => { + if (error) { + resolve('no file manager available') + return + } + if (stderr) { + console.warn('showItemInFolder stderr:', stderr.toString()) + } + resolve('Item shown in folder successfully.') + }) + }) +} diff --git a/src/app/lib/single-instance.js b/src/app/lib/single-instance.js deleted file mode 100644 index b414d05..0000000 --- a/src/app/lib/single-instance.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Single instance lock with socket-based IPC - */ - -const net = require('net') -const fs = require('fs') -const path = require('path') -const { app } = require('electron') -const globalState = require('./glob-state') -const { tempDir } = require('../common/runtime-constants') - -function getSocketPath () { - return path.join(tempDir, `${app.getName()}-instance.sock`) -} - -// Clean up stale socket file -function cleanupSocket () { - const socketPath = getSocketPath() - if (fs.existsSync(socketPath)) { - try { - fs.unlinkSync(socketPath) - } catch (e) { - // Ignore errors - } - } -} - -/** - * Start socket server to receive data from second instances - * @param {Function} onSecondInstance - Callback when second instance sends data - */ -function startSocketServer (onSecondInstance) { - const socketPath = getSocketPath() - cleanupSocket() - - const server = net.createServer((socket) => { - let data = '' - socket.on('data', (chunk) => { - data += chunk.toString() - }) - socket.on('end', () => { - try { - const parsed = JSON.parse(data) - onSecondInstance(parsed) - } catch (e) { - console.error('Failed to parse second instance data:', e) - } - }) - }) - - server.on('error', (err) => { - console.error('Socket server error:', err) - }) - - server.listen(socketPath) - - // Clean up on app quit - app.on('will-quit', () => { - server.close() - cleanupSocket() - }) - - return server -} - -/** - * Send data to primary instance via socket - * @param {Object} data - Data to send - * @returns {Promise} - True if sent successfully - */ -function sendToFirstInstance (data) { - const socketPath = getSocketPath() - return new Promise((resolve) => { - let settled = false - const done = (result) => { - if (settled) return - settled = true - clearTimeout(timer) - resolve(result) - } - - // Timeout: if we can't connect or get a response within 3 seconds, - // the primary instance is likely dead (e.g. crashed). Clean up the - // stale socket and proceed as the primary instance. - const timer = setTimeout(() => { - try { client.destroy() } catch (e) {} - cleanupSocket() - done(false) - }, 3000) - - const client = net.createConnection(socketPath, () => { - client.write(JSON.stringify(data)) - client.end() - }) - - client.on('error', () => { - // No server listening, we are the first instance - cleanupSocket() - done(false) - }) - - client.on('close', () => { - done(true) - }) - }) -} - -/** - * Handle second instance connection - * @param {Object} progs - Parsed command line options - * @returns {Promise} - True if this is the primary instance - */ -async function handleSingleInstance (progs) { - // Try to send to existing instance first via socket - const sent = await sendToFirstInstance(progs) - if (sent) { - // Successfully sent to primary instance, quit this one - return false - } - - // We are the primary instance, start socket server - startSocketServer((data) => { - const win = globalState.get('win') - if (win) { - if (win.isMinimized()) { - win.restore() - } - win.focus() - win.webContents.send('add-tab-from-command-line', data) - } - }) - - return true -} - -module.exports = { - handleSingleInstance, - sendToFirstInstance, - startSocketServer -} diff --git a/src/app/lib/sqlite.js b/src/app/lib/sqlite.js new file mode 100644 index 0000000..c8a0c40 --- /dev/null +++ b/src/app/lib/sqlite.js @@ -0,0 +1,145 @@ +/** + * sqlite api wrapper + * Updated to use two database files: one for 'data' table, one for others + */ + +import { cwd } from '../common/runtime-constants.js' +import { resolve } from 'path' +import fs from 'fs' +import uid from '../common/uid.js' +import { DatabaseSync } from 'node:sqlite' + +// Define database folder and paths for two database files +const dbFolder = process.env.DB_PATH || resolve(cwd, 'data') +const baseFolder = resolve(dbFolder, 'sqlite') +const mainDbPath = resolve(baseFolder, 'electerm.db') +const dataDbPath = resolve(baseFolder, 'electerm_data.db') + +// Ensure parent directory exists +if (!fs.existsSync(baseFolder)) { + fs.mkdirSync(baseFolder, { recursive: true }) +} +// Create two database instances +const mainDb = new DatabaseSync(mainDbPath) +const dataDb = new DatabaseSync(dataDbPath) + +export const tables = [ + 'bookmarks', + 'bookmarkGroups', + 'addressBookmarks', + 'terminalThemes', + 'lastStates', + 'data', + 'quickCommands', + 'log', + 'dbUpgradeLog', + 'profiles', + 'workspaces', + 'history', + 'terminalCommandHistory', + 'aiChatHistory', + 'autoRunWidgets' +] + +// Create tables in appropriate databases +for (const table of tables) { + if (table === 'data') { + dataDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) + } else { + mainDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) + } +} + +// Helper function to get the appropriate database for a table +function getDatabase (dbName) { + return dbName === 'data' ? dataDb : mainDb +} + +function toDoc (row) { + if (!row) return null + let r = {} + try { + r = JSON.parse(row.data || '{}') + } catch (e) { + log.error(e) + } + return { + ...r, + _id: row._id + } +} + +function toRow (doc) { + const _id = doc._id || doc.id || uid() + const copy = { ...doc } + delete copy._id + delete copy.id + return { + _id, + data: JSON.stringify(copy) + } +} + +export async function dbAction (dbName, op, ...args) { + if (op === 'compactDatafile') { + return + } + if (!tables.includes(dbName)) { + throw new Error(`Table ${dbName} does not exist`) + } + + // Get the appropriate database for this table + const db = getDatabase(dbName) + + if (op === 'find') { + const sql = `SELECT * FROM \`${dbName}\`` + const stmt = db.prepare(sql) + const rows = stmt.all() + return (rows || []).map(toDoc).filter(Boolean) + } else if (op === 'findOne') { + const query = args[0] || {} + const sql = `SELECT * FROM \`${dbName}\` WHERE _id = ? LIMIT 1` + const params = [query._id] + const stmt = db.prepare(sql) + const row = stmt.get(...params) + return toDoc(row) + } else if (op === 'insert') { + const inserts = Array.isArray(args[0]) ? args[0] : [args[0]] + const inserted = [] + for (const doc of inserts) { + const { _id, data } = toRow(doc) + const stmt = db.prepare(`INSERT OR REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) + stmt.run(_id, data) + inserted.push({ ...doc, _id }) + } + return Array.isArray(args[0]) ? inserted : inserted[0] + } else if (op === 'remove') { + const query = args[0] || {} + const sql = `DELETE FROM \`${dbName}\` WHERE _id = ?` + const params = [query._id] + const stmt = db.prepare(sql) + const res = stmt.run(...params) + return res.changes + } else if (op === 'update') { + const query = args[0] + const updateObj = args[1] + const options = args[2] || {} + const { upsert = false } = options + const qid = query._id || query.id + const newData = updateObj.$set || updateObj + const { _id, data } = toRow({ + _id: qid, + ...newData + }) + let stmt + let res + if (upsert) { + stmt = db.prepare(`REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) + res = stmt.run(_id, data) + } else { + stmt = db.prepare(`UPDATE \`${dbName}\` SET data = ? WHERE _id = ?`) + res = stmt.run(data, qid) + } + return res.changes + } +} diff --git a/src/app/lib/ssh-config.js b/src/app/lib/ssh-config.js index 55f8620..15ca3bc 100644 --- a/src/app/lib/ssh-config.js +++ b/src/app/lib/ssh-config.js @@ -1,14 +1,8 @@ /** * read ssh config */ +import { loadAndConvert } from 'ssh-config-loader' -// const { app } = require('electron') -// const home = app.getPath('home') -// const { resolve } = require('path') - -function loadSshConfig () { - const { loadAndConvert } = require('ssh-config-loader') +export async function loadSshConfig () { return loadAndConvert() } - -module.exports = loadSshConfig diff --git a/src/app/lib/storage-key.js b/src/app/lib/storage-key.js deleted file mode 100644 index b4c2d93..0000000 --- a/src/app/lib/storage-key.js +++ /dev/null @@ -1,46 +0,0 @@ -const log = require('../common/log') -const { appPath, defaultUserName } = require('../common/app-props') -const { safeEncrypt, safeDecrypt } = require('./safe-storage') -const { resolve: pathResolve } = require('path') -const fs = require('fs') -const { randomBytes } = require('crypto') - -const appDataPath = process.env.DATA_PATH || pathResolve(appPath, 'electerm') -const keyFilePath = pathResolve(appDataPath, 'users', defaultUserName, 'storage-key.enc') - -let _cachedStorageKey = null - -function getStorageKey () { - if (_cachedStorageKey) return _cachedStorageKey - let key = null - try { - if (fs.existsSync(keyFilePath)) { - const enc = fs.readFileSync(keyFilePath, 'utf8').trim() - const dec = safeDecrypt(enc) - if (dec && dec !== enc) { - key = dec - } else if (dec && !enc.startsWith('v2:safe:')) { - key = dec - } - } - } catch (e) { - log.error('[storage-key] read error:', e.message) - } - if (!key) { - key = randomBytes(32).toString('base64') - try { - const dir = pathResolve(appDataPath, 'users', defaultUserName) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - const enc = safeEncrypt(key) - fs.writeFileSync(keyFilePath, enc, 'utf8') - } catch (e) { - log.error('[storage-key] write error:', e.message) - } - } - _cachedStorageKey = key - return key -} - -module.exports = { getStorageKey } diff --git a/src/app/lib/system-ca.js b/src/app/lib/system-ca.js new file mode 100644 index 0000000..2932319 --- /dev/null +++ b/src/app/lib/system-ca.js @@ -0,0 +1,137 @@ +/** + * Load system-trusted CA certificates for the main web app process. + */ + +import { execSync } from 'child_process' +import { existsSync, readdirSync, readFileSync } from 'fs' +import https from 'https' +import os from 'os' +import { join } from 'path' + +let cachedPem = null +let globalApplied = false + +function loadMacOS () { + try { + return execSync( + 'security find-certificate -a -p ' + + '/System/Library/Keychains/SystemRootCertificates.keychain ' + + '/Library/Keychains/System.keychain ' + + `${os.homedir()}/Library/Keychains/login.keychain-db`, + { encoding: 'utf8', timeout: 10000 } + ) + } catch { + return '' + } +} + +function loadLinux () { + const dirs = [ + '/etc/ssl/certs', + '/etc/pki/tls/certs', + '/etc/pki/ca-trust/extracted/pem', + '/usr/local/share/certs' + ] + const files = [] + for (const dir of dirs) { + if (!existsSync(dir)) { + continue + } + try { + for (const f of readdirSync(dir)) { + if (f.endsWith('.crt') || f.endsWith('.pem')) { + files.push(join(dir, f)) + } + } + break + } catch { + // try next directory + } + } + + if (files.length > 0) { + return files.map((f) => { + try { + return readFileSync(f, 'utf8') + } catch { + return '' + } + }).join('\n') + } + + const bundlePaths = [ + '/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem' + ] + for (const p of bundlePaths) { + if (existsSync(p)) { + return readFileSync(p, 'utf8') + } + } + return '' +} + +function loadWindows () { + try { + return execSync( + 'powershell -Command ' + + '"Get-ChildItem -Path Cert:\\LocalMachine\\Root, Cert:\\LocalMachine\\CA, Cert:\\CurrentUser\\Root, Cert:\\CurrentUser\\CA ' + + '| Where-Object { $_.NotAfter -gt (Get-Date) } ' + + '| ForEach-Object { \'-----BEGIN CERTIFICATE-----\'; ' + + '[System.Convert]::ToBase64String($_.RawData, \'InsertLineBreaks\'); ' + + '\'-----END CERTIFICATE-----\' }"', + { encoding: 'utf8', timeout: 10000, windowsHide: true } + ) + } catch { + return '' + } +} + +export function getSystemCAsPem () { + if (cachedPem !== null) { + return cachedPem + } + switch (os.platform()) { + case 'darwin': + cachedPem = loadMacOS() + break + case 'linux': + cachedPem = loadLinux() + break + case 'win32': + cachedPem = loadWindows() + break + default: + cachedPem = '' + break + } + return cachedPem +} + +export function getSystemCAsList () { + const pem = getSystemCAsPem() + if (!pem) { + return [] + } + return pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) || [] +} + +export function applySystemCAsToGlobalAgent () { + if (globalApplied) { + return 0 + } + const certs = getSystemCAsList() + if (!certs.length) { + return 0 + } + + const existing = https.globalAgent.options.ca + const existingList = Array.isArray(existing) + ? existing + : (typeof existing === 'string' ? [existing] : []) + const merged = Array.from(new Set(existingList.concat(certs))) + https.globalAgent.options.ca = merged + globalApplied = true + return certs.length +} diff --git a/src/app/lib/user-config-controller.js b/src/app/lib/user-config-controller.js deleted file mode 100644 index bbf5f07..0000000 --- a/src/app/lib/user-config-controller.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * user-controll.json controll - */ - -const { dbAction } = require('./db') -const { userConfigId, userNoEncryptConfigId } = require('../common/constants') -const { getDbConfig } = require('./get-config') -const globalState = require('./glob-state') - -const configNoEncryptFields = ['allowMultiInstance'] - -function hasNoEncryptFields (userConfig) { - for (const f of configNoEncryptFields) { - if (f in userConfig) { - return true - } - } - return false -} - -exports.saveUserConfig = async (userConfig) => { - const q = { - _id: userConfigId - } - delete userConfig.host - delete userConfig.terminalTypes - delete userConfig.tokenElecterm - delete userConfig.server - delete userConfig.port - globalState.update('config', userConfig) - const conf = await getDbConfig() - if (hasNoEncryptFields(userConfig)) { - const q1 = { - _id: userNoEncryptConfigId - } - const noEncryptConfig = {} - for (const f of configNoEncryptFields) { - if (f in userConfig) { - noEncryptConfig[f] = userConfig[f] - } - } - await dbAction('data', 'update', q1, noEncryptConfig, { - upsert: true - }) - } - return dbAction('data', 'update', q, { - ...q, - ...conf, - ...userConfig - }, { - upsert: true - }) -} diff --git a/src/app/lib/user-config.js b/src/app/lib/user-config.js new file mode 100644 index 0000000..b7026b0 --- /dev/null +++ b/src/app/lib/user-config.js @@ -0,0 +1,26 @@ +/** + * user-controll.json controll + */ + +import { dbAction } from './db.js' +import { userConfigId } from '../common/constants.js' + +export async function saveUserConfig (userConfig) { + const q = { + _id: userConfigId + } + delete userConfig.host + delete userConfig.terminalTypes + delete userConfig.tokenElecterm + delete userConfig.port + delete userConfig.server + delete userConfig.wsPort + delete userConfig.wsHost + delete userConfig.useSystemTitleBar + await dbAction('data', 'update', q, { + ...q, + ...userConfig + }, { + upsert: true + }) +} diff --git a/src/app/lib/view.js b/src/app/lib/view.js new file mode 100644 index 0000000..e7d3f8f --- /dev/null +++ b/src/app/lib/view.js @@ -0,0 +1,81 @@ +/** + * simple login with password only + */ + +import { + isDev, + isMac, + isWin, + packInfo, + home, + extIconPath, + defaultUserName, + cwd +} from '../common/runtime-constants.js' +import { migrationNotice } from './fancy-console.js' +import fsFunctions from '../common/fs-functions.js' +import copy from 'json-deep-copy' +import { createToken } from './jwt.js' +import { logDir } from '../server/session-log.js' +import { resolve } from 'path' +import fs from 'fs' + +const defaultAIPreset = { + baseURLAI: 'https://ai.electerm.org/api/ai', + apiPathAI: '/chat/completions', + modelAI: 'mistral-small-latest', + authHeaderNameAI: 'Authorization: Bearer', + id: 'ai.electerm.org', + nameAI: 'ai.electerm.org(default free)' +} + +function buildServer () { + return `http://${process.env.HOST}:${process.env.PORT}` +} + +export async function index (req, res) { + const server = process.env.SERVER || (isDev ? buildServer() : '') + const cdn = process.env.CDN || server + const hasNodePty = false + // All session types the app knows about. + const supportSessionTypes = [ + 'ssh', + 'telnet', + 'web', + 'rdp', + 'vnc', + 'ftp', + 'spice' + ] + const data = { + isDev, + isMac, + isWin, + packInfo, + home, + version: packInfo.version, + siteName: packInfo.name, + defaultAIPreset, + fsFunctions, + isWebApp: true, + disableUpgradeCheck: false, + versionFile: 'version-android.html', + downloadUpgradeFromBrowser: true, + extIconPath: cdn + extIconPath, + cdn, + sessionLogPath: logDir, + query: req.query, + server, + hasNodePty, + needMigrate: false, + supportSessionTypes + } + const { + ENABLE_AUTH + } = process.env + if (!ENABLE_AUTH) { + data.tokenElecterm = createToken() + } + data._global = copy(data) + res.render('index', data) +} diff --git a/src/app/lib/watch-file.js b/src/app/lib/watch-file.js index 4ef48cc..8a09e7c 100644 --- a/src/app/lib/watch-file.js +++ b/src/app/lib/watch-file.js @@ -1,6 +1,6 @@ -const fs = require('original-fs') -const globalState = require('./glob-state') -const _ = require('./lodash.js') +import fs from 'fs' +import globalState from './global-state.js' +import _ from 'lodash' const onWatch = _.debounce(() => { try { @@ -18,17 +18,17 @@ const onWatch = _.debounce(() => { } }, 300, { leading: false, trailing: true }) -exports.watchFile = (path) => { +export const watchFile = (path) => { globalState.set('watchFilePath', path) fs.watchFile(path, onWatch) } -exports.unwatchFile = (path) => { +export const unwatchFile = (path) => { globalState.set('watchFilePath', '') fs.unwatchFile(path, onWatch) } -exports.cleanWatchFile = () => { +const cleanWatchFile = () => { globalState.set('watchFilePath', '') const filePath = globalState.get('watchFilePath') if (!filePath) { @@ -37,4 +37,4 @@ exports.cleanWatchFile = () => { fs.unwatchFile(filePath, onWatch) } -process.on('exit', exports.cleanWatchFile) +process.on('exit', cleanWatchFile) diff --git a/src/app/lib/webview-handler.js b/src/app/lib/webview-handler.js deleted file mode 100644 index a1c841e..0000000 --- a/src/app/lib/webview-handler.js +++ /dev/null @@ -1,152 +0,0 @@ -const { ipcMain, webContents } = require('electron') - -// Store credentials per webContents ID -const credentialsMap = new Map() // webContentsId -> { username, password } -const authRequestMap = new Map() // requestId -> { webContentsId } -const initializedSessions = new Set() - -let authRequestId = 0 -let windowCount = 0 - -const onAuthResponse = (event, data) => { - const { id, username, password } = data - const entry = authRequestMap.get(id) - if (!entry) return - - const { webContentsId } = entry - authRequestMap.delete(id) - - if (username && password) { - credentialsMap.set(webContentsId, { username, password }) - // Reload the webview to apply new credentials - try { - const wc = webContents.fromId(webContentsId) - if (wc) { - wc.reload() - } - } catch (e) { - console.error('Failed to reload webview:', e) - } - } else { - credentialsMap.delete(webContentsId) - } -} - -function init (mainWindow) { - windowCount++ - - // Listen for auth response from renderer if not already listening - if (ipcMain.listenerCount('webview-auth-response') === 0) { - ipcMain.on('webview-auth-response', onAuthResponse) - } - - // Handle new webviews - mainWindow.webContents.on('did-attach-webview', (event, viewWebContents) => { - setupWebview(viewWebContents, mainWindow) - - // Clean up when webview is destroyed - viewWebContents.once('destroyed', () => { - credentialsMap.delete(viewWebContents.id) - // Remove any pending requests for this webview - for (const [reqId, entry] of authRequestMap.entries()) { - if (entry.webContentsId === viewWebContents.id) { - authRequestMap.delete(reqId) - } - } - }) - }) - - mainWindow.on('closed', () => { - windowCount-- - if (windowCount <= 0) { - ipcMain.removeListener('webview-auth-response', onAuthResponse) - credentialsMap.clear() - authRequestMap.clear() - initializedSessions.clear() - windowCount = 0 - } - }) -} - -function setupWebview (viewWebContents, mainWindow) { - const session = viewWebContents.session - - // Set up header injection if not already done for this session - if (!initializedSessions.has(session)) { - initializedSessions.add(session) - - session.webRequest.onBeforeSendHeaders((details, callback) => { - const wcId = details.webContentsId - const creds = credentialsMap.get(wcId) - const requestHeaders = { ...details.requestHeaders } - - if (creds) { - const auth = Buffer.from(`${creds.username}:${creds.password}`).toString('base64') - requestHeaders.Authorization = `Basic ${auth}` - } - - // eslint-disable-next-line n/no-callback-literal - callback({ requestHeaders }) - }) - } - - // Listen for navigation and check for auth challenges (text-based) - viewWebContents.on('dom-ready', () => { - checkAuthStatus(viewWebContents, mainWindow) - }) - - viewWebContents.on('did-navigate', () => { - // Small delay to ensure page is loaded - setTimeout(() => checkAuthStatus(viewWebContents, mainWindow), 100) - }) - - // Initial check - setTimeout(() => checkAuthStatus(viewWebContents, mainWindow), 500) -} - -async function checkAuthStatus (viewWebContents, mainWindow) { - if (viewWebContents.isDestroyed()) return - - try { - const result = await viewWebContents.executeJavaScript(` - (function() { - // Check for various ways the 401 page might appear - const bodyText = document.body ? document.body.textContent : ''; - const htmlText = document.documentElement ? document.documentElement.textContent : ''; - const allText = bodyText + htmlText; - - if (allText.includes('Access Error: Unauthorized')) { - return { status: 'unauthorized' }; - } else if (allText.includes('Authentication Successful')) { - return { status: 'authenticated' }; - } - return { status: 'unknown' }; - })() - `) - - if (result.status === 'unauthorized') { - // Check if we've already requested auth for this webview - const pendingRequest = Array.from(authRequestMap.values()).find(e => e.webContentsId === viewWebContents.id) - if (pendingRequest) return - - // Generate request ID - authRequestId++ - const id = authRequestId - - authRequestMap.set(id, { webContentsId: viewWebContents.id }) - - mainWindow.webContents.send('webview-auth-request', { - id, - url: viewWebContents.getURL(), - host: new URL(viewWebContents.getURL()).host, - isProxy: false - }) - } - } catch (error) { - // console.error('Check auth status error:', error); - } -} - -module.exports = { - init -} diff --git a/src/app/lib/window-control.js b/src/app/lib/window-control.js deleted file mode 100644 index 835d131..0000000 --- a/src/app/lib/window-control.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * manage window size save read and set - */ - -const lastStateManager = require('./last-state') -const { - isDev, - minWindowWidth, - minWindowHeight -} = require('../common/runtime-constants') -const globalState = require('./glob-state') -const { restoreWindowBounds } = require('./window-restore') - -exports.getScreenCurrent = () => { - const rect = globalState.get('win') - ? globalState.get('win').getBounds() - : { - x: 0, - y: 0, - height: minWindowHeight, - width: minWindowWidth - } - const { screen } = require('electron') - return screen.getDisplayMatching(rect) -} - -exports.getScreenSize = () => { - const screen = exports.getScreenCurrent() - return { - ...screen.workAreaSize, - x: screen.workArea.x, - y: screen.workArea.y - } -} - -exports.maximize = () => { - const win = globalState.get('win') - globalState.set('oldRectangle', win.getBounds()) - win.maximize() -} - -exports.unmaximize = () => { - const oldRectangle = globalState.get('oldRectangle') || { - width: minWindowWidth, - height: minWindowHeight, - x: 200, - y: 200 - } - globalState.get('win').unmaximize() - globalState.get('win').setBounds(oldRectangle) -} - -exports.getWindowSize = async () => { - return exports.getWindowSizeDep() -} - -exports.getWindowSizeDep = async () => { - const windowSizeLastState = await lastStateManager.get('windowSize') - const windowPosLastState = await lastStateManager.get('windowPos') - const { screen } = require('electron') - return restoreWindowBounds({ - screen, - windowSizeLastState, - windowPosLastState, - isDev, - minWindowWidth, - minWindowHeight - }) -} - -exports.setWindowPos = (pos) => { - lastStateManager.set('windowPos', pos) -} diff --git a/src/app/lib/window-drag-move.js b/src/app/lib/window-drag-move.js deleted file mode 100644 index 94c270a..0000000 --- a/src/app/lib/window-drag-move.js +++ /dev/null @@ -1,45 +0,0 @@ -// from https://zhuanlan.zhihu.com/p/112564936 - -const { screen } = require('electron') -const globalState = require('./glob-state') - -let mouseStartPosition = { x: 0, y: 0 } -let movingInterval = null -let dragCount = 0 - -function windowMove (canMoving) { - const win = globalState.get('win') - if (!win) { - return - } - const size = win.getBounds() - if (canMoving) { - win.setResizable(false) - mouseStartPosition = screen.getCursorScreenPoint() - - if (movingInterval) { - clearInterval(movingInterval) - } - - movingInterval = setInterval(() => { - dragCount = dragCount + 1 - if (dragCount > 1000) { - dragCount = 1000 - } - const cursorPosition = screen.getCursorScreenPoint() - const x = size.x + cursorPosition.x - mouseStartPosition.x - const y = size.y + cursorPosition.y - mouseStartPosition.y - win.setBounds({ - ...size, - x, - y - }) - }, 1) - } else { - win.setResizable(true) - dragCount = 0 // Reset the count when moving is not allowed - clearInterval(movingInterval) - } -} - -module.exports = windowMove diff --git a/src/app/lib/window-restore.js b/src/app/lib/window-restore.js deleted file mode 100644 index 6daf5a6..0000000 --- a/src/app/lib/window-restore.js +++ /dev/null @@ -1,205 +0,0 @@ -const minVisibleSize = 100 - -function clamp (value, min, max) { - return Math.min(Math.max(value, min), max) -} - -function finiteOr (value, fallback) { - return Number.isFinite(value) ? value : fallback -} - -function limitWindowSize (savedSize, savedScreenSize, workAreaSize, minSize) { - const ratio = savedSize / savedScreenSize - const restoredSize = Number.isFinite(ratio) && ratio > 0 - ? workAreaSize * ratio - : workAreaSize - return Math.min(Math.max(Math.round(restoredSize), minSize), workAreaSize) -} - -function limitWindowPosition (position, workAreaPosition, workAreaSize, windowSize) { - const visibleSize = Math.min(minVisibleSize, windowSize, workAreaSize) - const min = workAreaPosition - windowSize + visibleSize - const max = workAreaPosition + workAreaSize - visibleSize - return clamp(position, min, max) -} - -function isBoundsVisibleOnAnyDisplay (bounds, displays) { - return displays.some(display => { - const { workArea } = display - const visibleLeft = Math.max(bounds.x, workArea.x) - const visibleRight = Math.min(bounds.x + bounds.width, workArea.x + workArea.width) - const visibleTop = Math.max(bounds.y, workArea.y) - const visibleBottom = Math.min(bounds.y + bounds.height, workArea.y + workArea.height) - return visibleRight - visibleLeft >= minVisibleSize && - visibleBottom - visibleTop >= minVisibleSize - }) -} - -/** - * Check whether a given point (x, y) falls within the work area of - * any of the provided displays. This is used to detect whether the - * saved window position still refers to a connected monitor. - */ -function isPointOnAnyDisplay (point, displays) { - return displays.some(display => { - const { workArea } = display - return point.x >= workArea.x && - point.x < workArea.x + workArea.width && - point.y >= workArea.y && - point.y < workArea.y + workArea.height - }) -} - -exports.isBoundsVisibleOnAnyDisplay = isBoundsVisibleOnAnyDisplay -exports.isPointOnAnyDisplay = isPointOnAnyDisplay - -/** - * Safety net: after a window is created, verify it is actually visible - * on at least one currently-connected display. Electron may adjust the - * requested bounds, or the display configuration may have changed between - * getWindowSize() and window creation. If the window ends up off-screen - * (e.g. saved position was on a monitor that has since been unplugged), - * move it to the primary display so the user can always see and interact - * with the app. - * - * Two conditions are checked: - * 1. At least 100px of the window is visible on some display - * (catches windows that are completely off-screen). - * 2. The centre point of the window is on some display - * (catches windows that are only barely visible at the edge - * of a display, e.g. only a 100px sliver — which is technically - * "visible" but practically unusable to the user). - * If either condition fails, move the window to the primary display. - * - * @param {import('electron').BrowserWindow} win - * @param {import('electron').Screen} screen - */ -exports.ensureWindowVisible = function (win, screen) { - const allDisplays = screen.getAllDisplays() - const actualBounds = win.getBounds() - const centerX = actualBounds.x + Math.floor(actualBounds.width / 2) - const centerY = actualBounds.y + Math.floor(actualBounds.height / 2) - const centerOnDisplay = isPointOnAnyDisplay({ x: centerX, y: centerY }, allDisplays) - const boundsVisible = isBoundsVisibleOnAnyDisplay(actualBounds, allDisplays) - if (!centerOnDisplay || !boundsVisible) { - const { workArea } = screen.getPrimaryDisplay() - win.setBounds({ - x: workArea.x, - y: workArea.y, - width: Math.min(actualBounds.width, workArea.width), - height: Math.min(actualBounds.height, workArea.height) - }) - } -} - -exports.restoreWindowBounds = ({ - screen, - windowSizeLastState, - windowPosLastState, - isDev, - minWindowWidth, - minWindowHeight -}) => { - const defaultBounds = { - x: 0, - y: 0, - width: minWindowWidth, - height: minWindowHeight - } - - if (!windowSizeLastState || isDev) { - const { workArea } = screen.getDisplayMatching(defaultBounds) - return { - width: workArea.width, - height: workArea.height, - x: 0, - y: 0 - } - } - - const savedPosition = { - x: finiteOr(windowPosLastState && windowPosLastState.x, 0), - y: finiteOr(windowPosLastState && windowPosLastState.y, 0) - } - - const allDisplays = screen.getAllDisplays() - - // Determine whether the saved window position still falls within a - // currently connected display. When the monitor the window was last on - // has been disconnected, the saved position will be outside all connected - // displays. In that case we must NOT simply clamp the old position to the - // edge of the nearest display (which would leave the window almost - // entirely off-screen with only a tiny sliver visible). Instead we - // centre the window on the primary display so the user can always find - // and interact with it. - const savedPositionIsValid = isPointOnAnyDisplay(savedPosition, allDisplays) - - // Electron reports display bounds and window positions in DIP coordinates. - const targetDisplay = savedPositionIsValid - ? screen.getDisplayNearestPoint(savedPosition) - : screen.getPrimaryDisplay() - const { workArea } = targetDisplay - const width = limitWindowSize( - windowSizeLastState.innerWidth, - windowSizeLastState.screenWidth, - workArea.width, - minWindowWidth - ) - const height = limitWindowSize( - windowSizeLastState.height, - windowSizeLastState.screenHeight, - workArea.height, - minWindowHeight - ) - - let bounds - if (savedPositionIsValid) { - // The monitor the window was last on is still connected — restore - // the saved position, clamped so at least part of the window is visible. - bounds = { - width, - height, - x: limitWindowPosition( - savedPosition.x, - workArea.x, - workArea.width, - width - ), - y: limitWindowPosition( - savedPosition.y, - workArea.y, - workArea.height, - height - ) - } - } else { - // The saved position is on a disconnected monitor. Centre the - // window on the primary display so it is fully visible. - bounds = { - width, - height, - x: workArea.x + Math.floor((workArea.width - width) / 2), - y: workArea.y + Math.floor((workArea.height - height) / 2) - } - } - - // Safety net: verify the computed bounds are actually visible on at - // least one currently-connected display. This catches edge cases where - // the display returned by getDisplayNearestPoint is stale — for example - // an external monitor was disconnected but Electron has not yet updated - // its internal display list — or where the workArea has changed since - // the display was queried. Without this check the window could end up - // on a non-existent display and be completely invisible to the user. - if (!isBoundsVisibleOnAnyDisplay(bounds, allDisplays)) { - const primary = screen.getPrimaryDisplay() - const { workArea: primaryWorkArea } = primary - return { - width: Math.min(width, primaryWorkArea.width), - height: Math.min(height, primaryWorkArea.height), - x: primaryWorkArea.x, - y: primaryWorkArea.y - } - } - - return bounds -} diff --git a/src/app/lib/zod.js b/src/app/lib/zod.js index 072c6e8..338c156 100644 --- a/src/app/lib/zod.js +++ b/src/app/lib/zod.js @@ -205,4 +205,4 @@ const z = { } } -module.exports = { z, ZodType } +export { z, ZodType } diff --git a/src/app/mcp/server/mcp.js b/src/app/mcp/server/mcp.js index a155ed0..ba529b8 100644 --- a/src/app/mcp/server/mcp.js +++ b/src/app/mcp/server/mcp.js @@ -29,4 +29,4 @@ class McpServer { } } -module.exports = { McpServer } +export { McpServer } diff --git a/src/app/mcp/server/streamableHttp.js b/src/app/mcp/server/streamableHttp.js index c508cc6..4e962a6 100644 --- a/src/app/mcp/server/streamableHttp.js +++ b/src/app/mcp/server/streamableHttp.js @@ -1,4 +1,4 @@ -const { z } = require('../../lib/zod') +import { z } from '../../lib/zod.js' function zodToJsonSchema (zodSchema) { if (!zodSchema) { @@ -316,4 +316,4 @@ class StreamableHTTPServerTransport { } } -module.exports = { StreamableHTTPServerTransport } +export { StreamableHTTPServerTransport } diff --git a/src/app/mcp/server/tasks.js b/src/app/mcp/server/tasks.js index 3dc2b9d..04b482b 100644 --- a/src/app/mcp/server/tasks.js +++ b/src/app/mcp/server/tasks.js @@ -19,7 +19,7 @@ * onSweep(task) — clean up resources when a terminal task is swept */ -const uid = require('../../common/uid') +import uid from '../../common/uid.js' const STATUS = { working: 'working', @@ -215,4 +215,4 @@ class TaskManager { } } -module.exports = { TaskManager, STATUS, TERMINAL_STATUSES } +export { TaskManager, STATUS, TERMINAL_STATUSES } diff --git a/src/app/preload/preload.js b/src/app/preload/preload.js deleted file mode 100644 index 42b1935..0000000 --- a/src/app/preload/preload.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * preload - */ - -const { ipcRenderer, contextBridge, webFrame, webUtils } = require('electron') - -contextBridge.exposeInMainWorld( - 'api', { - getZoomFactor: () => webFrame.getZoomFactor(), - setZoomFactor: (nl) => webFrame.setZoomFactor(nl), - getPathForFile: (file) => { - try { - return webUtils.getPathForFile(file) - } catch (error) { - console.warn('webUtils.getPathForFile failed:', error) - return null - } - }, - openDialog: (opts) => { - return ipcRenderer.invoke('show-open-dialog-sync', opts) - }, - saveDialog: (opts) => { - return ipcRenderer.invoke('show-save-dialog', opts) - }, - ipcOnEvent: (event, cb) => { - ipcRenderer.on(event, cb) - }, - ipcOffEvent: (event, cb) => { - ipcRenderer.removeListener(event, cb) - }, - runGlobalAsync: (name, ...args) => { - return ipcRenderer.invoke('async', { - name, - args - }) - }, - runSync: (name, ...args) => { - return ipcRenderer.sendSync('sync-func', { - name, - args - }) - }, - sendMcpResponse: (response) => { - ipcRenderer.send('mcp-response', response) - }, - onWebviewAuthRequest: (cb) => { - const handler = (event, data) => cb(data) - ipcRenderer.on('webview-auth-request', handler) - return () => ipcRenderer.removeListener('webview-auth-request', handler) - }, - sendWebviewAuthResponse: (response) => { - ipcRenderer.send('webview-auth-response', response) - } - } -) diff --git a/src/app/routes/file-transfer.js b/src/app/routes/file-transfer.js new file mode 100644 index 0000000..445178b --- /dev/null +++ b/src/app/routes/file-transfer.js @@ -0,0 +1,71 @@ +/** + * file download/upload routes + */ + +import multer from 'multer' +import fs from 'fs' +import path, { resolve } from 'path' +import { spawn } from 'child_process' +import { + jwtAuth, + errHandler +} from '../lib/jwt.js' + +const uploadDir = resolve(process.env.DB_PATH || resolve(process.cwd(), 'data'), 'uploads') +fs.mkdirSync(uploadDir, { recursive: true }) +const upload = multer({ dest: uploadDir }) + +export function fileTransferRoutes (app) { + app.get('/api/download', jwtAuth, errHandler, (req, res) => { + const filePath = req.query.path + if (!filePath) { + return res.status(400).json({ error: 'path is required' }) + } + try { + const stat = fs.statSync(filePath) + if (stat.isFile()) { + const fileName = path.basename(filePath) + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`) + res.setHeader('Content-Type', 'application/octet-stream') + fs.createReadStream(filePath).pipe(res) + } else if (stat.isDirectory()) { + const dirName = path.basename(filePath) + const parentDir = path.dirname(filePath) + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`) + res.setHeader('Content-Type', 'application/gzip') + const tar = spawn('tar', ['czf', '-', '-C', parentDir, dirName]) + tar.stdout.pipe(res) + tar.stderr.on('data', (data) => { + console.error('tar stderr:', data.toString()) + }) + tar.on('error', (err) => { + console.error('tar error:', err) + if (!res.headersSent) { + res.status(500).json({ error: err.message }) + } + }) + } else { + res.status(400).json({ error: 'path is not a file or directory' }) + } + } catch (err) { + console.error('download error:', err) + res.status(500).json({ error: err.message }) + } + }) + + app.post('/api/upload', jwtAuth, errHandler, upload.single('file'), (req, res) => { + const targetDir = req.body.path + if (!targetDir || !req.file) { + return res.status(400).json({ error: 'path and file are required' }) + } + try { + const originalName = Buffer.from(req.file.originalname, 'latin1').toString('utf8') + const destPath = path.join(targetDir, originalName) + fs.renameSync(req.file.path, destPath) + res.json({ success: true, path: destPath }) + } catch (err) { + console.error('upload error:', err) + res.status(500).json({ error: err.message }) + } + }) +} diff --git a/src/app/routes/http.js b/src/app/routes/http.js new file mode 100644 index 0000000..e8ddd82 --- /dev/null +++ b/src/app/routes/http.js @@ -0,0 +1,33 @@ +import express from 'express' +import { login } from '../lib/login.js' +import { index } from '../lib/view.js' +import { getConstants } from '../lib/get-constants.js' +import { resolve } from 'path' +import { + cwd, + isDev +} from '../common/runtime-constants.js' +import { + jwtAuth, + errHandler +} from '../lib/jwt.js' +import { fileTransferRoutes } from './file-transfer.js' + +export function httpRoutes (app) { + app.get('/', index) + app.post('/api/login', login) + app.get('/api/get-constants', jwtAuth, errHandler, getConstants) + fileTransferRoutes(app) + if (isDev) { + app.use(express.static( + resolve(cwd, 'node_modules') + )) + app.use(express.static( + resolve(cwd, 'src/client/statics') + )) + } else { + app.use(express.static( + resolve(cwd, 'dist/assets') + )) + } +} diff --git a/src/app/routes/ws.js b/src/app/routes/ws.js new file mode 100644 index 0000000..fcb55ec --- /dev/null +++ b/src/app/routes/ws.js @@ -0,0 +1,356 @@ +import log from '../common/log.js' +import expressWs from 'express-ws' +import { + isWin +} from '../common/runtime-constants.js' +import { verifyWs, initWs } from '../server/dispatch-center.js' +import { + terminals, + cleanAllSessions +} from '../server/remote-common.js' +import { zmodemManager } from '../server/zmodem.js' +import { trzszManager } from '../server/trzsz.js' +import { xmodemManager } from '../server/xmodem.js' + +function cleanup () { + cleanAllSessions() +} + +// True when the buffered data ends mid-way through a multi-byte UTF-8 +// sequence (CJK chars are 3 bytes). Slow SSH servers (embedded router CLIs) +// often deliver one char split across TCP segments; flushing such a buffer +// right away would push a partial char to the client. Only the tail of the +// last buffer is inspected (at most 4 bytes), so this is O(1). +function hasIncompleteTrailingUtf8 (bufs) { + const last = bufs[bufs.length - 1] + if (!last) { + return false + } + const buf = Buffer.isBuffer(last) ? last : Buffer.from(last) + const len = buf.length + if (!len) { + return false + } + // Count trailing continuation bytes (10xxxxxx), at most 3 + let cont = 0 + while (cont < 3 && cont < len && (buf[len - 1 - cont] & 0xc0) === 0x80) { + cont++ + } + const leadIdx = len - 1 - cont + if (leadIdx < 0) { + // Whole buffer is continuation bytes; the lead byte was in a chunk that + // was already flushed, so holding can not reassemble anything. + return false + } + const lead = buf[leadIdx] + if (lead < 0xc0) { + // ASCII last byte, or stray continuations after ASCII: nothing to wait for + return false + } + // Expected continuation count for this lead byte: + // 110xxxxx -> 1, 1110xxxx -> 2, 11110xxx -> 3 + const needed = lead < 0xe0 ? 1 : lead < 0xf0 ? 2 : 3 + return cont < needed +} + +export function wsRoutes (app) { + expressWs(app, undefined, { + wsOptions: { + perMessageDeflate: false + } + }) + app.ws('/spice/:pid', function (ws, req) { + const { query } = req + verifyWs(req) + const { pid } = req.params + const term = terminals(pid) + log.debug('ws: connected to spice session ->', pid) + term.start(query, ws) + ws.on('error', (err) => { + log.error(err) + }) + }) + app.ws('/terminals/:pid', function (ws, req) { + verifyWs(req) + const term = terminals(req.params.pid) + const { pid } = term + log.debug('ws: connected to terminal ->', pid) + + const dataBuffer = [] + let sendTimeout = null + // Time of the last actual flush. Lets a chunk arriving after an idle gap + // (keystroke echo, command result) skip the coalescing delay entirely, + // so only chunks arriving inside an active burst (floods) pay the 10ms + // wait. Mirrors the client-side coalescing fast path. + let lastFlushTime = 0 + const flushIntervalMs = 10 + + // Auto-trigger XMODEM when the serial device sends a marker message. + // The serial-shell.js sends these markers when the user types tx/rx. + function detectXmodemMarker (text) { + const txMatch = text.match(/\[XMODEM:TX:(.+?)\]/) + if (txMatch) { + ws.s({ + action: 'xmodem-event', + event: 'auto-trigger-receive', + name: txMatch[1] + }) + return + } + const rxMatch = text.match(/\[XMODEM:RX\]/) + if (rxMatch) { + ws.s({ + action: 'xmodem-event', + event: 'auto-trigger-send' + }) + } + } + + const flushBufferedData = () => { + if (!dataBuffer.length) { + sendTimeout = null + return + } + lastFlushTime = Date.now() + const combinedData = Buffer.concat(dataBuffer.splice(0).map(d => Buffer.isBuffer(d) ? d : Buffer.from(d))) + + // Write to log (keep this) + term.writeLog(combinedData) + + // Detect XMODEM auto-trigger markers from serial device + if (term.port) { + detectXmodemMarker(combinedData.toString('utf8')) + } + + // Check for zmodem escape sequence before sending to client + const zmodemConsumed = zmodemManager.handleData(pid, combinedData, term, ws) + if (zmodemConsumed) { + sendTimeout = null + return + } + + // Check for trzsz magic key before sending to client + const trzszConsumed = trzszManager.handleData(pid, combinedData, term, ws) + if (trzszConsumed) { + sendTimeout = null + return + } + + // Check for xmodem protocol before sending to client + const xmodemConsumed = xmodemManager.handleData(pid, combinedData, term, ws) + if (xmodemConsumed) { + sendTimeout = null + return + } + + // Not zmodem, trzsz, or xmodem data, send to WebSocket + ws.send(combinedData) + sendTimeout = null + } + + // Create ws.s function for zmodem to send messages to client + ws.s = (data) => { + ws.send(JSON.stringify(data)) + } + + // In the WebSocket setup, replace the data handler: + term.on('data', function (data) { + // Check if zmodem session is active and handle data + if (zmodemManager.isActive(pid)) { + // Let zmodem handle the data, but still log it + term.writeLog(data) + zmodemManager.handleData(pid, data, term, ws) + return + } + + // Check if trzsz session is active and handle data + if (trzszManager.isActive(pid)) { + // Let trzsz handle the data, but still log it + term.writeLog(data) + trzszManager.handleData(pid, data, term, ws) + return + } + + // Check if xmodem session is active and handle data. + // For serial terminals (term.port exists) a raw port listener (registered below) + // bypasses rxLineEnding transformation and feeds raw bytes to xmodem. + if (xmodemManager.isActive(pid)) { + if (!term.port) { + // Non-serial fallback (should not normally happen) + term.writeLog(data) + xmodemManager.handleData(pid, data, term, ws) + } + return + } + + // Detect XMODEM auto-trigger markers from serial device + if (term.port) { + const text = Buffer.isBuffer(data) ? data.toString('utf8') : data + detectXmodemMarker(text) + } + + const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data) + const shouldBypassBatch = chunk.length > 16384 + + // Bypass batching for very large chunks to avoid parser desync. + if (shouldBypassBatch) { + if (sendTimeout) { + clearTimeout(sendTimeout) + sendTimeout = null + } + if (dataBuffer.length) { + flushBufferedData() + } + term.writeLog(chunk) + const zmodemConsumed = zmodemManager.handleData(pid, chunk, term, ws) + if (zmodemConsumed) { + return + } + const trzszConsumed = trzszManager.handleData(pid, chunk, term, ws) + if (trzszConsumed) { + return + } + const xmodemConsumed = xmodemManager.handleData(pid, chunk, term, ws) + if (xmodemConsumed) { + return + } + ws.send(chunk) + return + } + + // Buffer incoming data instead of sending immediately for normal text workload + dataBuffer.push(chunk) + + // Idle fast path: if nothing has been flushed within the coalescing + // window, this is the start of a new burst (or a lone interactive + // echo) rather than a continuation of a flood - send it right away + // instead of paying the fixed delay. Only chunks arriving while a + // burst is already in flight (elapsed < flushIntervalMs) get batched. + const elapsed = Date.now() - lastFlushTime + if (elapsed >= flushIntervalMs) { + // Never fast-flush a buffer that ends mid-way through a multi-byte + // UTF-8 char: a slow peer (router CLI) may deliver one char split + // across TCP segments, and the remaining bytes usually land within a + // few ms. Hold one coalescing window so they get concatenated first + // (the completing chunk then flushes immediately via this same fast + // path). Bounded by the timeout, so it can not stick. + if (hasIncompleteTrailingUtf8(dataBuffer)) { + if (!sendTimeout) { + sendTimeout = setTimeout(flushBufferedData, flushIntervalMs) + } + return + } + if (sendTimeout) { + clearTimeout(sendTimeout) + sendTimeout = null + } + flushBufferedData() + return + } + + // If no timeout is pending, schedule a batched send + if (!sendTimeout) { + sendTimeout = setTimeout(flushBufferedData, flushIntervalMs - elapsed) + } + }) + + // For serial terminals, register a raw data listener directly on the port to + // feed binary XMODEM data to xmodemManager without rxLineEnding transformation. + if (term.port) { + term.port.on('data', function (rawData) { + if (xmodemManager.isActive(pid)) { + term.writeLog(rawData) + xmodemManager.handleData(pid, rawData, term, ws) + } + }) + } + + function onClose () { + // Cancel any pending batched send + if (sendTimeout) { + clearTimeout(sendTimeout) + sendTimeout = null + } + // Clean up zmodem session + zmodemManager.destroySession(pid) + // Clean up trzsz session + trzszManager.destroySession(pid) + // Clean up xmodem session + xmodemManager.destroySession(pid) + term.kill() + log.debug('Closed terminal ' + pid) + // Clean things up + ws.close && ws.close() + cleanup() + } + + term.on('close', onClose) + if (term.isLocal && isWin) { + term.on('exit', onClose) + } + + ws.on('message', function (msg) { + try { + // Check if message is a zmodem or trzsz control message (JSON) + if (typeof msg === 'string') { + try { + const parsed = JSON.parse(msg) + if (parsed.action === 'zmodem-event') { + zmodemManager.handleMessage(pid, parsed, term, ws) + return + } + if (parsed.action === 'trzsz-event') { + trzszManager.handleMessage(pid, parsed, term, ws) + return + } + if (parsed.action === 'xmodem-event') { + xmodemManager.handleMessage(pid, parsed, term, ws) + return + } + if (parsed.action === 'keepalive') { + // Write \n to the PTY. In canonical mode the TTY line discipline + // only delivers data to read() when a newline completes the line, + // so \x00 (NUL) sits in the buffer and never wakes bash up. + // A newline wakes bash's read(), resets the TMOUT alarm, and bash + // simply re-displays the prompt. The client suppresses that echo. + term.write('\n\r\x1b[K') + return + } + } catch (e) { + // Not JSON, treat as regular terminal input + } + } + term.write(msg) + } catch (ex) { + log.error(ex) + } + }) + + ws.on('error', (err) => { + log.error(err) + }) + + ws.on('close', onClose) + }) + app.ws('/rdp/:pid', function (ws, req) { + const { width, height } = req.query + verifyWs(req) + const term = terminals(req.params.pid) + term.ws = ws + term.start(width, height) + const { pid } = term + log.debug('ws: connected to rdp session ->', pid) + ws.on('error', log.error) + }) + app.ws('/vnc/:pid', function (ws, req) { + const { query } = req + verifyWs(req) + const { pid } = req.params + const term = terminals(pid) + term.ws = ws + term.start(query) + log.debug('ws: connected to vnc session ->', pid) + ws.on('error', log.error) + }) + initWs(app) +} diff --git a/src/app/server/app-wrap.js b/src/app/server/app-wrap.js deleted file mode 100644 index c2e4fe8..0000000 --- a/src/app/server/app-wrap.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require('express') - -module.exports = function (app) { - // parse application/x-www-form-urlencoded - app.use(express.urlencoded({ extended: false })) - - // parse application/json - app.use(express.json()) - - require('express-ws')(app, undefined, { - wsOptions: { - perMessageDeflate: false - } - }) -} diff --git a/src/app/server/child-process.js b/src/app/server/child-process.js deleted file mode 100644 index 6356b35..0000000 --- a/src/app/server/child-process.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Start the main Express server in-process. - * - * No child process — everything runs in the same Node.js/Electron process. - * Returns a mock "child" object with EventEmitter interface for compatibility - * with init-server.js. - */ - -const EventEmitter = require('events') -const log = require('../common/log') - -// --use-system-ca is supported since Node.js 24.3.0 -function supportsSystemCa () { - const [major, minor] = process.versions.node.split('.').map(Number) - return major > 24 || (major === 24 && minor >= 3) -} - -module.exports = (config, env, sysLocale) => { - // Set environment variables that server.js reads - process.env.electermPort = String(config.port) - process.env.electermHost = config.host || '127.0.0.1' - process.env.requireAuth = config.requireAuth || '' - process.env.tokenElecterm = config.tokenElecterm - process.env.sshKeysPath = env.sshKeysPath - // Normalize to canonical "_.UTF-8" for the remote shell, - // e.g. "zh-cn" → "zh_CN.UTF-8". sysLocale is lowercased upstream for - // language-pack matching, so restore conventional territory casing here. - const [langPart, regionPart] = sysLocale.split(/[-_]/) - const sshLocale = regionPart ? `${langPart}_${regionPart.toUpperCase()}` : langPart - process.env.LANG = `${sshLocale}.UTF-8` - - // Handle system CAs - const nodeOpts = [env.NODE_OPTIONS, supportsSystemCa() ? '--use-system-ca' : ''] - .filter(Boolean).join(' ').trim() - if (nodeOpts) { - process.env.NODE_OPTIONS = nodeOpts - } - - // Create a mock child object for init-server.js compatibility - const child = new EventEmitter() - child.pid = process.pid - child.killed = false - child.stdout = { on: () => {} } - child.stderr = { on: () => {} } - child.kill = () => { - child.killed = true - child.emit('exit', 0, 'SIGTERM') - return true - } - child.send = (msg) => { - child.emit('message', msg) - return true - } - - // Require server.js (auto-starts) and wait for it to be ready - try { - const { startServer } = require('./server') - startServer().then(() => { - child.emit('message', { serverInited: true }) - }).catch(err => { - child.emit('error', err) - }) - } catch (err) { - setImmediate(() => { - child.emit('error', err) - }) - } - - log.info('Server starting in-process, port:', config.port) - return child -} diff --git a/src/app/server/dispatch-center.js b/src/app/server/dispatch-center.js index f89ba7e..6e06126 100644 --- a/src/app/server/dispatch-center.js +++ b/src/app/server/dispatch-center.js @@ -3,13 +3,25 @@ * run functions in seprate process, avoid using electron.remote directly */ -const fs = require('./fs') -const log = require('../common/log') -const { Upgrade } = require('./download-upgrade') -const { transferKeys } = require('./transfer') -const fetch = require('./fetch') -const sync = require('./sync') -const { +import { Sftp } from './session-sftp.js' +import { Ftp } from './session-ftp.js' +import { instSftpKeys } from '../common/constants.js' +import { + sftp, + transfer, + onDestroySftp, + onDestroyTransfer +} from './remote-common.js' +import { Transfer, transferKeys } from './transfer.js' +import { FtpTransfer } from './ftp-transfer.js' +import { Upgrade } from './download-upgrade.js' +import fs from './fs.js' +import log from '../common/log.js' +import fetch from './fetch.js' +import sync from './sync.js' +import { verify } from '../lib/jwt.js' +import { runSync } from '../lib/run-sync.js' +import { createTerm, testTerm, resize, @@ -19,69 +31,206 @@ const { toggleTerminalLogTimestamp, setTerminalLogPath, startTerminalLogFile -} = require('./terminal-api') -const globalState = require('./global-state') -const wsDec = require('./ws-dec') +} from './terminal-api.js' +import globalState from './global-state.js' -const { tokenElecterm } = process.env +const { + SERVER_USER +} = process.env -function verify (req) { - const { token: to } = req.query - if (to !== tokenElecterm) { - throw new Error('not valid request') +/** + * add ws.s function + * @param {*} ws + */ +const wsDec = (ws) => { + ws.s = msg => { + try { + ws.send(JSON.stringify(msg)) + } catch (e) { + log.error('ws send error') + log.error(e) + } } - if (process.env.requireAuth === 'yes' && !globalState.authed) { - throw new Error('auth required') + ws.on('error', log.error) + ws.once = (callack, id) => { + const func = (evt) => { + const arg = JSON.parse(evt.data) + if (id === arg.id) { + callack(arg) + ws.removeEventListener('message', func) + } + } + ws.addEventListener('message', func) } + ws._socket.setKeepAlive(true, 30 * 1000) } -const initWs = function (app) { - // upgrade - app.ws('/upgrade/:id', (ws, req) => { - verify(req) +export function verifyWs (req) { + const { token } = req.query + const data = verify(token) + if (SERVER_USER !== data.id) { + throw new Error('not valid request') + } +} + +export function initWs (app) { + // sftp function + app.ws('/sftp/:id', (ws, req) => { + verifyWs(req) wsDec(ws) const { id } = req.params ws.on('close', () => { - const inst = globalState.getUpgradeInst(id) - if (inst) { - inst.destroy() - } + onDestroySftp(id) }) - ws.on('message', async (message) => { + ws.on('message', (message) => { const msg = JSON.parse(message) const { action } = msg - if (action === 'upgrade-new') { + if (action === 'sftp-new') { + const { id, terminalId, type } = msg + const Cls = type === 'ftp' ? Ftp : Sftp + sftp(id, new Cls({ + uid: id, + terminalId, + type + })) + } else if (action === 'sftp-func') { + const { id, args, func, uid } = msg + const inst = sftp(id) + if (inst) { + if (!instSftpKeys.includes(func) || typeof inst[func] !== 'function') { + ws.s({ + id: uid, + error: { + message: 'invalid sftp function: ' + func, + stack: '' + } + }) + return + } + inst[func](...args) + .then(data => { + ws.s({ + id: uid, + data + }) + }) + .catch(err => { + ws.s({ + id: uid, + error: { + message: err.message, + stack: err.stack + } + }) + }) + } + } else if (action === 'sftp-destroy') { const { id } = msg + ws.close() + onDestroySftp(id) + } + }) + // end + }) + + // transfer function + app.ws('/transfer/:id', (ws, req) => { + verifyWs(req) + wsDec(ws) + const { id } = req.params + const { sftpId } = req.query + ws.on('close', () => { + onDestroyTransfer(id, sftpId) + }) + ws.on('message', (message) => { + const msg = JSON.parse(message) + const { action } = msg + + if (action === 'transfer-new') { + const { sftpId, id, isFtp } = msg + const session = sftp(sftpId) + const encode = session.initOptions?.encode || 'utf8' const opts = Object.assign({}, msg, { - ws + sftp: session.sftp, + conn: session.client, + ftpSession: isFtp ? session : null, + sftpId, + ws, + encode }) - const inst = new Upgrade(opts) - globalState.setUpgradeInst(id, inst) - await inst.init() - } else if (action === 'upgrade-func') { - const { id, func, args } = msg - const inst = globalState.getUpgradeInst(id) - if (!inst) { + const Cls = isFtp ? FtpTransfer : Transfer + transfer(id, sftpId, new Cls(opts)) + } else if (action === 'transfer-func') { + const { id, func, args, sftpId } = msg + if (func === 'destroy') { + return onDestroyTransfer(id, sftpId) + } + if (!transferKeys.includes(func)) { return } - if (!transferKeys.includes(func) || typeof inst[func] !== 'function') { - log.error('invalid upgrade function:', func) + const tr = transfer(id, sftpId) + if (!tr || typeof tr[func] !== 'function') { return } - inst[func](...args) + tr[func](...args) } }) + // end + }) + + // upgrade + app.ws('/upgrade/:id', (ws, req) => { + verifyWs(req) + wsDec(ws) + const { id } = req.params + ws.on('close', () => { + const inst = globalState.getUpgradeInst(id) + if (inst) { + inst.destroy() + } + }) + ws.on('message', async (message) => { + try { + const msg = JSON.parse(message) + const { action } = msg + + if (action === 'upgrade-new') { + const { id } = msg + const opts = Object.assign({}, msg, { + ws + }) + const inst = new Upgrade(opts) + globalState.setUpgradeInst(id, inst) + await inst.init() + } else if (action === 'upgrade-func') { + const { id, func, args } = msg + const inst = globalState.getUpgradeInst(id) + if (!inst) { + return + } + if (!transferKeys.includes(func) || typeof inst[func] !== 'function') { + log.error('invalid upgrade function:', func) + return + } + inst[func](...args) + } + } catch (err) { + log.error('upgrade ws error', err) + } + }) + // end }) // common functions app.ws('/common/s', (ws, req) => { - verify(req) + verifyWs(req) wsDec(ws) + globalState.setCommonWs(ws) ws.on('message', async (message) => { try { const msg = JSON.parse(message) - const { action } = msg + const { action, body = {}, id } = msg if (action === 'fetch') { fetch(ws, msg) } else if (action === 'sync') { @@ -89,6 +238,15 @@ const initWs = function (app) { } else if (action === 'fs') { fs(ws, msg) } else if (action === 'create-terminal') { + if (body.termType === 'ftp') { + ws.s({ + id, + data: { + pid: 'ok' + } + }) + return + } createTerm(ws, msg) } else if (action === 'test-terminal') { testTerm(ws, msg) @@ -106,14 +264,13 @@ const initWs = function (app) { runCmd(ws, msg) } else if (action === 'exec-cmd') { execCmd(ws, msg) + } if (action === 'runSync') { + runSync(ws, msg) } - } catch (err) { - log.error('common ws error', err) + } catch (e) { + log.error(e) } }) }) // end } - -exports.verifyWs = verify -exports.initWs = initWs diff --git a/src/app/server/download-upgrade.js b/src/app/server/download-upgrade.js index 6c67b86..e7bc7d2 100644 --- a/src/app/server/download-upgrade.js +++ b/src/app/server/download-upgrade.js @@ -1,20 +1,29 @@ /** * download upgrade class + * + * Ported from the desktop electerm source. Adapted for the + * electerm-android ESM backend: + * - ESM imports + * - message ids aligned with the @electerm/electerm-react client + * contract (upgrade:data / upgrade:end / upgrade:err) + * - `process.send` (Electron IPC) replaced with `showItemInFolder` + * so it works under the on-device Node runtime */ -const fs = require('fs') -const { resolve } = require('path') -const _ = require('../lib/lodash.js') -const rp = require('axios') -const { packInfo, tempDir } = require('../common/runtime-constants') -const installSrc = require('../lib/install-src') -const { fsExport } = require('../lib/fs') -const { createProxyAgent } = require('../lib/proxy-agent') +import fs from 'fs' +import { resolve } from 'path' +import axios from 'axios' +import _ from 'lodash' +import { packInfo, tempDir } from '../common/runtime-constants.js' +import installSrc from '../lib/install-src.js' +import { fsExport } from '../lib/fs.js' +import { createProxyAgent } from '../lib/proxy-agent.js' +import { showItemInFolder } from '../lib/show-item-in-folder.js' +import log from '../common/log.js' +import globalState from './global-state.js' + +axios.defaults.proxy = false const { openFile, rmrf } = fsExport -const log = require('../common/log') -const globalState = require('./global-state') - -rp.defaults.proxy = false function getUrl (url, mirror) { if (mirror === 'gh-proxy') { @@ -30,18 +39,15 @@ function getUrl (url, mirror) { } } -function getReleaseInfo ( - filter, releaseInfoUrl, agent -) { +function getReleaseInfo (filter, releaseInfoUrl, agent) { const conf = { url: releaseInfoUrl, timeout: 15000 } if (agent) { - conf.httpAgent = agent conf.httpsAgent = agent } - return rp(conf) + return axios(conf) .then((res) => { return res.data .release @@ -62,24 +68,15 @@ class Upgrade { proxy, mirror } = this.options + // register id early so destroy() works even if init() is aborted + this.id = id const agent = createProxyAgent(proxy) const releaseInfoUrl = `${packInfo.homepage}/data/electerm-github-release.json?_=${+new Date()}` const filter = r => { - return r.name.endsWith(installSrc) + return r.name.includes(installSrc) } - // if (isWin) { - // filter = r => /electerm-\d+\.\d+\.\d+-win-x64\.tar\.gz/.test(r.name) - // } else if (isArm) { - // filter = r => { - // return /arm64\.dmg$/.test(r.name) - // } - // } else if (isMac) { - // filter = r => { - // return /mac\.dmg$/.test(r.name) - // } - // } const releaseInfo = await getReleaseInfo(filter, releaseInfoUrl, agent) - .catch(this.onError) + .catch(err => this.onError(err, id, ws)) if (!releaseInfo) { return } @@ -87,11 +84,9 @@ class Upgrade { const remotePath = getUrl(releaseInfo.browser_download_url, mirror) await rmrf(localPath).catch(log.error) const { size } = releaseInfo - this.id = id this.localPath = localPath - const readSteam = await rp({ + const readSteam = await axios({ url: remotePath, - httpAgent: agent, httpsAgent: agent, responseType: 'stream' }) @@ -149,21 +144,20 @@ class Upgrade { } onEnd (id, ws) { - if (!this.onDestroy) { - openFile(this.localPath) - process.send({ - showFileInFolder: this.localPath - }) - ws.s({ - id: 'transfer:end:' + id, - data: this.dir - }) + if (this.onDestroy) { + return } + openFile(this.localPath).catch(log.error) + // showItemInFolder(this.localPath).catch(log.error) + ws.s({ + id: 'upgrade:end:' + id, + data: this.localPath + }) } onError (err, id, ws) { ws.s({ - wid: 'upgrade:err:' + id, + id: 'upgrade:err:' + id, error: { message: err.message, stack: err.stack @@ -191,4 +185,4 @@ class Upgrade { // end } -exports.Upgrade = Upgrade +export { Upgrade } diff --git a/src/app/server/fetch.js b/src/app/server/fetch.js index cd95135..587d6de 100644 --- a/src/app/server/fetch.js +++ b/src/app/server/fetch.js @@ -2,11 +2,12 @@ * node fetch in server side */ -const { createProxyAgent } = require('../lib/proxy-agent') +import rp from 'axios' +import { createProxyAgent } from '../lib/proxy-agent.js' + +rp.defaults.proxy = false function fetch (options) { - const rp = require('axios') - rp.defaults.proxy = false return rp(options) .then((res) => { return res.data @@ -18,12 +19,14 @@ function fetch (options) { }) } -async function wsFetchHandler (ws, msg) { +export default async function wsFetchHandler (ws, msg) { const { id, options, proxy } = msg const agent = createProxyAgent(proxy) if (agent) { options.httpAgent = agent options.httpsAgent = agent + } else { + options.proxy = false } const res = await fetch(options) if (res.error) { @@ -39,5 +42,3 @@ async function wsFetchHandler (ws, msg) { }) } } - -module.exports = wsFetchHandler diff --git a/src/app/server/fs.js b/src/app/server/fs.js index debe309..1fd26d9 100644 --- a/src/app/server/fs.js +++ b/src/app/server/fs.js @@ -2,9 +2,9 @@ * fs in child process */ -const { fsExport: fs } = require('../lib/fs') +import { fsExport as fs } from '../lib/fs.js' -function handleFs (ws, msg) { +export default function handleFs (ws, msg) { const { id, args, func } = msg // only dispatch to fs helpers defined on the export itself, never to // anything reached through the prototype chain @@ -34,5 +34,3 @@ function handleFs (ws, msg) { }) }) } - -module.exports = handleFs diff --git a/src/app/server/ftp-client.js b/src/app/server/ftp-client.js index 128f589..5fc4d61 100644 --- a/src/app/server/ftp-client.js +++ b/src/app/server/ftp-client.js @@ -1,13 +1,56 @@ -const ftp = require('basic-ftp') -const iconv = require('iconv-lite') +import ftp from 'basic-ftp' +import iconv from 'iconv-lite' -class FtpClientWrapper { +export class FtpClientWrapper { constructor () { this.client = new ftp.Client() this.queue = Promise.resolve() this.encoding = 'utf-8' } + async access (options) { + return this.enqueue(async () => { + if (options.proxy) { + return this._accessViaProxy(options) + } + const { proxy, readyTimeout, ...ftpOptions } = options + return this.client.access(ftpOptions) + }) + } + + async _accessViaProxy (options) { + const proxySock = require('./socks') + const { FTPError } = require('basic-ftp') + const proxyResult = await proxySock({ + readyTimeout: options.readyTimeout || 10000, + host: options.host, + port: options.port || 21, + proxy: options.proxy + }) + const ftpClient = this.client + ftpClient.ftp.reset() + ftpClient.ftp.socket = proxyResult.socket + // Wait for FTP welcome response (mirrors Client._handleConnectResponse) + const welcome = await ftpClient.ftp.handle(undefined, (res, task) => { + if (res instanceof Error) { + task.reject(res) + } else if (res.code >= 200 && res.code < 300) { + task.resolve(res) + } else { + task.reject(new FTPError(res)) + } + }) + if (options.secure === true) { + const secureOptions = { ...(options.secureOptions || {}) } + secureOptions.host = secureOptions.host || options.host + await ftpClient.useTLS(secureOptions) + } + await ftpClient.sendIgnoringError('OPTS UTF8 ON') + await ftpClient.login(options.user || 'anonymous', options.password || 'guest') + await ftpClient.useDefaultSettings() + return welcome + } + setEncoding (encoding) { this.encoding = encoding || 'utf-8' // When using non-UTF-8 encoding, set the FTP control connection to use latin1 (binary) @@ -62,49 +105,6 @@ class FtpClientWrapper { return this.client.ftp.verbose } - async access (options) { - return this.enqueue(async () => { - if (options.proxy) { - return this._accessViaProxy(options) - } - const { proxy, readyTimeout, ...ftpOptions } = options - return this.client.access(ftpOptions) - }) - } - - async _accessViaProxy (options) { - const proxySock = require('./socks') - const { FTPError } = require('basic-ftp') - const proxyResult = await proxySock({ - readyTimeout: options.readyTimeout || 10000, - host: options.host, - port: options.port || 21, - proxy: options.proxy - }) - const ftpClient = this.client - ftpClient.ftp.reset() - ftpClient.ftp.socket = proxyResult.socket - // Wait for FTP welcome response (mirrors Client._handleConnectResponse) - const welcome = await ftpClient.ftp.handle(undefined, (res, task) => { - if (res instanceof Error) { - task.reject(res) - } else if (res.code >= 200 && res.code < 300) { - task.resolve(res) - } else { - task.reject(new FTPError(res)) - } - }) - if (options.secure === true) { - const secureOptions = { ...(options.secureOptions || {}) } - secureOptions.host = secureOptions.host || options.host - await ftpClient.useTLS(secureOptions) - } - await ftpClient.sendIgnoringError('OPTS UTF8 ON') - await ftpClient.login(options.user || 'anonymous', options.password || 'guest') - await ftpClient.useDefaultSettings() - return welcome - } - async pwd () { const result = await this.enqueue(() => this.client.pwd()) return this.decodeString(result) @@ -163,5 +163,3 @@ class FtpClientWrapper { return this.client.trackProgress(handler) } } - -module.exports = FtpClientWrapper diff --git a/src/app/server/ftp-file.js b/src/app/server/ftp-file.js index 74987f9..7b192e5 100644 --- a/src/app/server/ftp-file.js +++ b/src/app/server/ftp-file.js @@ -1,6 +1,6 @@ -const { Readable, Writable } = require('stream') +import { Readable, Writable } from 'stream' -async function readRemoteFile (client, remotePath) { +export async function readRemoteFile (client, remotePath) { return new Promise((resolve, reject) => { let data = '' const writable = new Writable({ @@ -16,7 +16,7 @@ async function readRemoteFile (client, remotePath) { }) } -async function writeRemoteFile (client, remotePath, str) { +export async function writeRemoteFile (client, remotePath, str) { const readable = new Readable({ read () { this.push(str) @@ -26,8 +26,3 @@ async function writeRemoteFile (client, remotePath, str) { return client.uploadFrom(readable, remotePath) } - -module.exports = { - readRemoteFile, - writeRemoteFile -} diff --git a/src/app/server/ftp-transfer.js b/src/app/server/ftp-transfer.js index 19aac3b..52c6010 100644 --- a/src/app/server/ftp-transfer.js +++ b/src/app/server/ftp-transfer.js @@ -4,7 +4,7 @@ * Note: basic-ftp only supports one active transfer per client connection */ -class Transfer { +export class FtpTransfer { constructor ({ remotePath, localPath, @@ -128,7 +128,3 @@ class Transfer { } } } - -module.exports = { - Transfer -} diff --git a/src/app/server/global-state.js b/src/app/server/global-state.js index 2de7629..2713bc0 100644 --- a/src/app/server/global-state.js +++ b/src/app/server/global-state.js @@ -1,8 +1,17 @@ // global-state.js class GlobalState { + #commonWs = null #sessions = {} #upgradeInsts = {} - #authed = false + + // Common WebSocket management + getCommonWs () { + return this.#commonWs + } + + setCommonWs (ws) { + this.#commonWs = ws + } // Sessions management getSession (id) { @@ -30,22 +39,13 @@ class GlobalState { delete this.#upgradeInsts[id] } - get authed () { - return this.#authed - } - - set authed (val) { - this.#authed = val - } - get data () { return { sessions: this.#sessions, - upgradeInsts: this.#upgradeInsts, - authed: this.#authed + upgradeInsts: this.#upgradeInsts } } } // Export a singleton instance -module.exports = new GlobalState() +export default new GlobalState() diff --git a/src/app/server/rdp-proxy.js b/src/app/server/rdp-proxy.js index 69ad110..4da1e01 100644 --- a/src/app/server/rdp-proxy.js +++ b/src/app/server/rdp-proxy.js @@ -1,12 +1,16 @@ -const net = require('net') -const forge = require('node-forge') -const log = require('../common/log') -const proxySock = require('./socks') +import net from 'net' +import tls from 'tls' +import log from '../common/log.js' +import proxySock from './socks.js' // Debug prefix for all RDP proxy messages const LOG_PREFIX = '[RDP-PROXY]' -// ── RDCleanPath ASN.1 DER Constants ── +// We use Node.js built-in tls module with rejectUnauthorized: false +// to accept self-signed RDP server certificates. +// This works because electerm-web runs in standard Node.js (not Electron with BoringSSL). + +// RDCleanPath ASN.1 DER Constants const VERSION_1 = 3390 // 3389 + 1 // ASN.1 tag constants @@ -18,9 +22,7 @@ const TAG_UTF8STRING = 0x0c // Context-specific EXPLICIT tags used by RDCleanPath const TAG_CTX = (n) => 0xa0 + n -// ──────────────────────────────────────────────────── // ASN.1 DER Low-Level Helpers -// ──────────────────────────────────────────────────── /** * Encode ASN.1 DER length bytes. @@ -141,9 +143,7 @@ function derDecodeChildren (buf) { return children } -// ──────────────────────────────────────────────────── // RDCleanPath PDU Parsing & Encoding -// ──────────────────────────────────────────────────── /** * Parse an RDCleanPath Request PDU from DER-encoded bytes. @@ -229,7 +229,7 @@ function buildRDCleanPathResponse (serverAddr, x224Response, certChain) { // [6] x224_connection_pdu parts.push(derWrapContext(6, derEncodeOctetString(x224Response))) - // [7] server_cert_chain — SEQUENCE OF OCTET STRING + // [7] server_cert_chain - SEQUENCE OF OCTET STRING const certOctets = certChain.map((cert) => derEncodeOctetString(cert)) const certSeq = derWrap(TAG_SEQUENCE, Buffer.concat(certOctets)) parts.push(derWrapContext(7, certSeq)) @@ -269,9 +269,7 @@ function buildRDCleanPathError (errorCode, httpStatusCode) { return derWrap(TAG_SEQUENCE, Buffer.concat(parts)) } -// ──────────────────────────────────────────────────── // Network: Destination Parsing -// ──────────────────────────────────────────────────── /** * Parse a destination string into { host, port }. @@ -302,14 +300,7 @@ function parseDestination (destination) { return { host, port } } -// ──────────────────────────────────────────────────── -// Network: TCP + X.224 + TLS (node-forge) + Cert Extraction -// ──────────────────────────────────────────────────── -// -// We use node-forge's pure-JS TLS implementation instead of Node's -// built-in tls module. In Electron, Node's tls uses BoringSSL which -// enforces strict KEY_USAGE_BIT_INCORRECT checks that reject typical -// RDP server certificates. node-forge avoids this entirely. +// Network: TCP + X.224 + TLS + Cert Extraction /** * Create a TCP connection (direct or through proxy) @@ -332,22 +323,22 @@ async function createTcpConnection (host, port, options, x224Request, logPrefix) proxy: options.proxy }) const tcpSocket = proxyResult.socket - log.debug(`${logPrefix} ✓ Proxy connection established`) + log.debug(`${logPrefix} Proxy connection established`) // Send X.224 Connection Request over proxied connection tcpSocket.write(x224Request, () => { - log.debug(`${logPrefix} ✓ Sent X.224 Connection Request (${x224Request.length} bytes)`) + log.debug(`${logPrefix} Sent X.224 Connection Request (${x224Request.length} bytes)`) }) return tcpSocket } return new Promise((resolve, reject) => { const tcpSocket = net.createConnection({ host, port }, () => { - log.debug(`${logPrefix} ✓ TCP connection established`) + log.debug(`${logPrefix} TCP connection established`) // Send X.224 Connection Request over raw TCP tcpSocket.write(x224Request, () => { - log.debug(`${logPrefix} ✓ Sent X.224 Connection Request (${x224Request.length} bytes)`) + log.debug(`${logPrefix} Sent X.224 Connection Request (${x224Request.length} bytes)`) }) resolve(tcpSocket) }) @@ -358,11 +349,11 @@ async function createTcpConnection (host, port, options, x224Request, logPrefix) } /** - * Perform the RDCleanPath proxy handshake: + * Perform the RDP proxy handshake: * 1. TCP connect to RDP server (optionally through proxy) * 2. Send X.224 Connection Request (raw TCP) * 3. Read X.224 Connection Confirm (raw TCP) - * 4. TLS handshake via node-forge (bypasses BoringSSL) + * 4. TLS handshake via Node.js tls module (with rejectUnauthorized: false) * 5. Extract server certificates * * @param {string} host @@ -371,7 +362,7 @@ async function createTcpConnection (host, port, options, x224Request, logPrefix) * @param {object} options - Optional settings * @param {string} options.proxy - Proxy URL (e.g., 'socks5://127.0.0.1:1080' or 'http://proxy:8080') * @param {number} options.readyTimeout - Connection timeout in ms - * @returns {Promise<{ x224Response: Buffer, certChain: Buffer[], forgeTls: object, tcpSocket: net.Socket }>} + * @returns {Promise<{ x224Response: Buffer, certChain: Buffer[], tlsSocket: tls.TLSSocket, tcpSocket: net.Socket }>} */ async function performRDPHandshake (host, port, x224Request, options = {}) { const logPrefix = `${LOG_PREFIX} [${host}:${port}]` @@ -400,7 +391,7 @@ async function performRDPHandshake (host, port, x224Request, options = {}) { // Step 3: Read X.224 Connection Confirm tcpSocket.once('data', (x224Response) => { - log.debug(`${logPrefix} ✓ Received X.224 Connection Confirm (${x224Response.length} bytes)`) + log.debug(`${logPrefix} Received X.224 Connection Confirm (${x224Response.length} bytes)`) if (x224Response.length === 0) { tcpSocket.destroy() @@ -412,88 +403,50 @@ async function performRDPHandshake (host, port, x224Request, options = {}) { tcpSocket.removeAllListeners('error') tcpSocket.removeAllListeners('data') - // Step 4: TLS handshake via node-forge (pure JS — no BoringSSL) - log.debug(`${logPrefix} Starting TLS handshake via node-forge`) - - // Capture the cert chain from the verify callback - let capturedCertChain = [] - - const forgeTls = forge.tls.createConnection({ - server: false, - verify: function (connection, verified, depth, certs) { - // Accept all certificates (RDP servers use self-signed certs) - log.debug(`${logPrefix} TLS verify callback: depth=${depth}, verified=${verified}, certs=${certs.length}`) - // Capture the full chain on the first call (depth = deepest) - if (certs && certs.length > capturedCertChain.length) { - capturedCertChain = certs - } - return true - }, - connected: function (connection) { - log.debug(`${logPrefix} ✓ node-forge TLS handshake completed`) - - // The handshake-deadline timer set below (in the outer function) - // is a `net.Socket` idle-inactivity timer, not a one-shot deadline - // - it re-arms on every read/write and was never cleared once the - // handshake finished. Left alone, it destroys this same socket - // (reused for the whole session relay) after any 15s stretch with - // no bytes in either direction - e.g. a static remote desktop with - // no mouse/keyboard activity - killing otherwise-healthy sessions. - // Disable it now that the handshake is done; a real dead/half-open - // connection is instead caught by the TCP keepalive enabled below. - tcpSocket.setTimeout(0) - tcpSocket.setNoDelay(true) - tcpSocket.setKeepAlive(true, 10000) - - // Step 5: Convert captured certificates to DER - const certChain = forgeCertsToDer(capturedCertChain) - log.debug(`${logPrefix} ✓ Extracted ${certChain.length} certificate(s) from forge`) - - settle(null, { - x224Response: Buffer.from(x224Response), - certChain, - forgeTls, - tcpSocket - }) - }, - tlsDataReady: function (connection) { - // Encrypted data ready to send to the RDP server over TCP - const data = connection.tlsData.getBytes() - const buf = Buffer.from(data, 'binary') - try { - tcpSocket.write(buf) - } catch (err) { - log.error(`${logPrefix} Error writing TLS data to TCP: ${err.message}`) - } - }, - dataReady: function (connection) { - // Decrypted data from RDP server — handled by setupForgeRelay - }, - closed: function () { - log.debug(`${logPrefix} node-forge TLS connection closed`) - }, - error: function (connection, error) { - log.error(`${logPrefix} node-forge TLS error: ${error.message}`) - settle(new Error(`TLS handshake failed: ${error.message}`)) - } + // Step 4: TLS handshake via Node.js tls module + log.debug(`${logPrefix} Starting TLS handshake`) + + const tlsSocket = tls.connect({ + socket: tcpSocket, + rejectUnauthorized: false // Accept self-signed RDP certificates + }, () => { + log.debug(`${logPrefix} TLS handshake completed`) + + // The handshake-deadline timer set below (in the outer function) + // is a `net.Socket` idle-inactivity timer, not a one-shot deadline + // - it re-arms on every read/write and was never cleared once the + // handshake finished. Left alone, it destroys this same socket + // (reused for the whole session relay) after any 15s stretch with + // no bytes in either direction - e.g. a static remote desktop with + // no mouse/keyboard activity - killing otherwise-healthy sessions. + // Disable it now that the handshake is done; a real dead/half-open + // connection is instead caught by the TCP keepalive enabled below. + tcpSocket.setTimeout(0) + tcpSocket.setNoDelay(true) + tcpSocket.setKeepAlive(true, 10000) + + // Step 5: Extract certificate chain + const certChain = extractCertChain(tlsSocket) + log.debug(`${logPrefix} Extracted ${certChain.length} certificate(s)`) + + settle(null, { + x224Response: Buffer.from(x224Response), + certChain, + tlsSocket, + tcpSocket + }) }) - // Feed received TCP data into forge TLS engine - tcpSocket.on('data', (data) => { - try { - forgeTls.process(data.toString('binary')) - } catch (err) { - log.error(`${logPrefix} forge process error: ${err.message}`) - } + tlsSocket.once('error', (err) => { + log.error(`${logPrefix} TLS error: ${err.message}`) + settle(new Error(`TLS handshake failed: ${err.message}`)) }) - tcpSocket.on('error', (err) => { - log.error(`${logPrefix} TCP error during TLS: ${err.message}`) - settle(new Error(`TCP error: ${err.message}`)) + // Timeout for the TLS handshake + tlsSocket.setTimeout(15000, () => { + tlsSocket.destroy() + settle(new Error('TLS handshake timed out')) }) - - // Initiate the TLS handshake - forgeTls.handshake() }) // Timeout for the whole handshake @@ -505,87 +458,89 @@ async function performRDPHandshake (host, port, x224Request, options = {}) { } /** - * Convert an array of node-forge certificate objects to DER-encoded Buffers. + * Extract the certificate chain from a TLS socket. + * Returns an array of DER-encoded certificates. */ -function forgeCertsToDer (certs) { +function extractCertChain (tlsSocket) { const result = [] - for (const cert of certs) { - try { - const asn1 = forge.pki.certificateToAsn1(cert) - const derBytes = forge.asn1.toDer(asn1).getBytes() - result.push(Buffer.from(derBytes, 'binary')) - } catch (e) { - log.error(`${LOG_PREFIX} Error converting cert to DER: ${e.message}`) + try { + const peerCert = tlsSocket.getPeerCertificate(true) + if (peerCert) { + // The 'raw' property contains the DER-encoded certificate + if (peerCert.raw) { + result.push(peerCert.raw) + } + // Check for issuer certificate in the chain + let cert = peerCert + while (cert.issuerCertificate && cert.issuerCertificate !== cert) { + if (cert.issuerCertificate.raw) { + result.push(cert.issuerCertificate.raw) + } + cert = cert.issuerCertificate + } } + } catch (e) { + log.error(`${LOG_PREFIX} Error extracting cert chain: ${e.message}`) } return result } -// ──────────────────────────────────────────────────── -// Bidirectional Relay: WebSocket ↔ forge TLS ↔ TCP -// ──────────────────────────────────────────────────── +// Bidirectional Relay: WebSocket <-> TLS Socket <-> TCP /** - * Set up bidirectional relay between a WebSocket and a forge TLS connection. + * Set up bidirectional relay between a WebSocket and a TLS socket. * - * Browser (WASM) → WebSocket → Proxy → forge TLS → TCP → RDP Server - * RDP Server → TCP → forge TLS → Proxy → WebSocket → Browser (WASM) + * Browser (WASM) -> WebSocket -> Proxy -> TLS Socket -> TCP -> RDP Server + * RDP Server -> TCP -> TLS Socket -> Proxy -> WebSocket -> Browser (WASM) * * @param {WebSocket} ws - The WebSocket connection to the browser - * @param {object} forgeTls - The node-forge TLS connection + * @param {tls.TLSSocket} tlsSocket - The TLS socket connected to RDP server * @param {net.Socket} tcpSocket - The underlying TCP socket */ -function setupForgeRelay (ws, forgeTls, tcpSocket) { +function setupTlsRelay (ws, tlsSocket, tcpSocket) { let wsBytesForwarded = 0 let tlsBytesForwarded = 0 const logPrefix = `${LOG_PREFIX} [relay]` - // Override forge's dataReady to forward decrypted data to WebSocket - forgeTls.dataReady = function (connection) { - const data = connection.data.getBytes() - const buf = Buffer.from(data, 'binary') - tlsBytesForwarded += buf.length + // TLS Socket -> WebSocket (RDP server -> browser) + tlsSocket.on('data', (data) => { + tlsBytesForwarded += data.length try { if (ws.readyState === 1 /* OPEN */) { - ws.send(buf) + ws.send(data) } } catch (err) { - log.error(`${logPrefix} TLS→WS write error:`, err.message) + log.error(`${logPrefix} TLS->WS write error:`, err.message) } - } - - // Override forge's closed/error for relay phase - forgeTls.closed = function () { - log.debug(`${logPrefix} forge TLS closed`) - cleanup('forge TLS') - } - forgeTls.error = function (connection, error) { - log.error(`${logPrefix} forge TLS error during relay: ${error.message}`) - cleanup('forge TLS (error)') - } + }) - // WebSocket → forge TLS → TCP (browser → RDP server) + // WebSocket -> TLS Socket (browser -> RDP server) ws.on('message', (data) => { const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) wsBytesForwarded += buf.length try { - forgeTls.prepare(buf.toString('binary')) + tlsSocket.write(buf) } catch (err) { - log.error(`${logPrefix} WS→TLS write error:`, err.message) + log.error(`${logPrefix} WS->TLS write error:`, err.message) } }) // Cleanup on close const cleanup = (source) => { - log.debug(`${logPrefix} ${source} closed — WS→TLS: ${wsBytesForwarded} bytes, TLS→WS: ${tlsBytesForwarded} bytes`) + log.debug(`${logPrefix} ${source} closed - WS->TLS: ${wsBytesForwarded} bytes, TLS->WS: ${tlsBytesForwarded} bytes`) if (!tcpSocket.destroyed) tcpSocket.destroy() - try { forgeTls.close() } catch (_) {} if (ws.readyState === 1) { try { ws.close() } catch (_) {} } } + tlsSocket.on('end', () => cleanup('TLS')) + tlsSocket.on('error', (err) => { + log.error(`${logPrefix} TLS error:`, err.message) + cleanup('TLS (error)') + }) + tcpSocket.on('end', () => cleanup('TCP')) tcpSocket.on('error', (err) => { log.error(`${logPrefix} TCP error:`, err.message) @@ -599,9 +554,7 @@ function setupForgeRelay (ws, forgeTls, tcpSocket) { }) } -// ──────────────────────────────────────────────────── // Main Handler: Process a WebSocket connection -// ──────────────────────────────────────────────────── /** * Handle a new WebSocket connection from the browser's WASM RDP client. @@ -612,7 +565,7 @@ function setupForgeRelay (ws, forgeTls, tcpSocket) { * 3. TCP connect to RDP server, send X.224, receive X.224 confirm * 4. TLS handshake, extract server certificates * 5. Send RDCleanPath Response back to browser - * 6. Bidirectional relay: WebSocket ↔ TLS + * 6. Bidirectional relay: WebSocket <-> TLS * * @param {WebSocket} ws - The WebSocket connection * @param {object} options - Optional settings @@ -620,7 +573,7 @@ function setupForgeRelay (ws, forgeTls, tcpSocket) { * @param {number} options.readyTimeout - Connection timeout in ms */ function handleConnection (ws, options = {}, bufferedMessages = []) { - log.debug(`${LOG_PREFIX} New WebSocket connection for RDCleanPath proxy`) + log.debug(`${LOG_PREFIX} New WebSocket connection for RDP proxy`) const handleFirstMessage = async (data) => { try { @@ -629,14 +582,14 @@ function handleConnection (ws, options = {}, bufferedMessages = []) { // Step 1: Parse RDCleanPath request const request = parseRDCleanPathRequest(requestData) - log.debug(`${LOG_PREFIX} RDCleanPath Request → destination: ${request.destination}, proxyAuth: ${request.proxyAuth}`) + log.debug(`${LOG_PREFIX} RDCleanPath Request -> destination: ${request.destination}, proxyAuth: ${request.proxyAuth}`) // Step 2: Parse destination const { host, port } = parseDestination(request.destination) log.debug(`${LOG_PREFIX} Connecting to RDP server at ${host}:${port}`) - // Step 3-5: TCP + X.224 + TLS (node-forge) + Certs - const { x224Response, certChain, forgeTls, tcpSocket } = await performRDPHandshake( + // Step 3-5: TCP + X.224 + TLS + Certs + const { x224Response, certChain, tlsSocket, tcpSocket } = await performRDPHandshake( host, port, request.x224ConnectionRequest, @@ -646,15 +599,15 @@ function handleConnection (ws, options = {}, bufferedMessages = []) { // Step 6: Build and send RDCleanPath response const serverAddr = `${host}:${port}` const responsePdu = buildRDCleanPathResponse(serverAddr, x224Response, certChain) - log.debug(`${LOG_PREFIX} ✓ Sending RDCleanPath response (${responsePdu.length} bytes) to browser`) + log.debug(`${LOG_PREFIX} Sending RDCleanPath response (${responsePdu.length} bytes) to browser`) ws.send(responsePdu) - log.debug(`${LOG_PREFIX} ✓ RDCleanPath handshake complete — starting bidirectional relay`) + log.debug(`${LOG_PREFIX} RDP proxy handshake complete - starting bidirectional relay`) - // Step 7: Bidirectional relay via node-forge - setupForgeRelay(ws, forgeTls, tcpSocket) + // Step 7: Bidirectional relay + setupTlsRelay(ws, tlsSocket, tcpSocket) } catch (err) { - log.error(`${LOG_PREFIX} RDCleanPath handshake error:`, err.message) + log.error(`${LOG_PREFIX} RDP proxy handshake error:`, err.message) log.error(`${LOG_PREFIX} Stack:`, err.stack) // Try to send error response to client @@ -680,12 +633,16 @@ function handleConnection (ws, options = {}, bufferedMessages = []) { }) } -module.exports = { +// Backward compatibility alias +const setupForgeRelay = setupTlsRelay + +export { handleConnection, parseRDCleanPathRequest, buildRDCleanPathResponse, buildRDCleanPathError, parseDestination, performRDPHandshake, - setupForgeRelay + setupTlsRelay, + setupForgeRelay // backward compatibility } diff --git a/src/app/server/remote-common.js b/src/app/server/remote-common.js index aa6a948..506c113 100644 --- a/src/app/server/remote-common.js +++ b/src/app/server/remote-common.js @@ -3,9 +3,14 @@ * for sftp, terminal and transfer */ -const globalState = require('./global-state') +// const _ = require('loadsh') +import globalState from './global-state.js' -function sftp (id, inst) { +export function session (id) { + return globalState.getSession(id) +} + +export function sftp (id, inst) { if (inst) { globalState.setSession(id, inst) return inst @@ -13,7 +18,7 @@ function sftp (id, inst) { return globalState.getSession(id) } -function terminals (id, inst) { +export function terminals (id, inst) { if (inst) { globalState.setSession(id, inst) return inst @@ -21,7 +26,7 @@ function terminals (id, inst) { return globalState.getSession(id) } -function transfer (id, sftpId, inst) { +export function transfer (id, sftpId, inst) { const ss = sftp(sftpId) if (!ss) { return @@ -33,19 +38,16 @@ function transfer (id, sftpId, inst) { return ss.transfers[id] } -function onDestroySftp (id) { +export function onDestroySftp (id) { const inst = sftp(id) inst && inst.kill && inst.kill() } -function onDestroyTransfer (id, sftpId) { - const sftpInst = sftp(sftpId) - const inst = transfer(id, sftpId) - inst && inst.destroy && inst.destroy() - sftpInst && delete sftpInst.transfers[id] +export function onDestroyTerminal (id) { + onDestroySftp(id) } -function cleanAllSessions () { +export function cleanAllSessions () { const { sessions } = globalState.data for (const id in sessions) { const inst = sessions[id] @@ -53,12 +55,9 @@ function cleanAllSessions () { } } -module.exports = { - sftp, - transfer, - onDestroySftp, - onDestroyTerminal: onDestroySftp, - onDestroyTransfer, - terminals, - cleanAllSessions +export function onDestroyTransfer (id, sftpId) { + const sftpInst = sftp(sftpId) + const inst = transfer(id, sftpId) + inst && inst.destroy && inst.destroy() + sftpInst && delete sftpInst.transfers[id] } diff --git a/src/app/server/server.js b/src/app/server/server.js index e32ee3a..fa1fff2 100644 --- a/src/app/server/server.js +++ b/src/app/server/server.js @@ -1,54 +1,52 @@ -const express = require('express') -const globalState = require('./global-state') -const app = express() -const log = require('../common/log') -const { initWs } = require('./dispatch-center') -const { - isDev -} = require('../common/runtime-constants') -const initFileServer = require('../lib/file-server') -const appDec = require('./app-wrap') +import express from 'express' +import pug from 'pug' +import { wsRoutes } from '../routes/ws.js' +import { httpRoutes } from '../routes/http.js' +import { applyExtensions } from '../lib/extensions.js' +import morgan from 'morgan' +import { + isDev, + cwd +} from '../common/runtime-constants.js' +import { resolve } from 'path' +import log from '../common/log.js' +import { applySystemCAsToGlobalAgent } from '../lib/system-ca.js' -appDec(app) - -app.get('/run', function (req, res) { - res.send('ok') -}) -app.post('/auth', function (req, res) { - const { token } = req.body - if (token === process.env.requireAuth) { - globalState.authed = true +export async function createApp () { + const loadedCount = applySystemCAsToGlobalAgent() + if (loadedCount > 0) { + log.info(`[TLS] loaded ${loadedCount} system CA certificate(s) into main process`) } - res.send('ok') -}) -if (!isDev) { - initFileServer(app) -} -initWs(app) -// --- Server lifecycle --- -let _startPromise = null + const app = express() + // parse application/x-www-form-urlencoded + app.use(express.urlencoded({ extended: true })) -/** - * Start the Express server. Returns a Promise that resolves when - * the server is listening. Safe to call multiple times — returns - * the same Promise. - */ -function startServer () { - if (_startPromise) return _startPromise - _startPromise = new Promise((resolve, reject) => { - const { electermPort, electermHost } = process.env - app.listen(electermPort, electermHost, () => { - log.info('server', 'runs on', electermHost, electermPort) - // process.send may not exist (in-process mode) - try { process.send({ serverInited: true }) } catch {} - resolve(app) - }) - }) - return _startPromise -} + // parse application/json + app.use(express.json()) -// Auto-start when required -startServer() + app.use(morgan( + ':method :url :status :res[content-length] - :response-time ms' + )) + app.set('view engine', 'pug') + // Register the pug engine explicitly so Express uses the bundled pug + // directly instead of lazily `require('pug')` at render time. The lazy + // require breaks bundled builds (esbuild can't see the dynamic string + // require, so "pug" is missing at runtime -> GET / hangs forever). + app.engine('pug', pug.__express) + app.set( + 'views', + process.env.VIEW_FOLDER || + ( + !isDev + ? resolve(cwd, 'dist/views') + : resolve(cwd, 'src/app/views') + ) + ) + app.set('x-powered-by', false) -module.exports = { startServer, app } + httpRoutes(app) + wsRoutes(app) + await applyExtensions(app) + return app +} diff --git a/src/app/server/session-api.js b/src/app/server/session-api.js deleted file mode 100644 index 0b9d78e..0000000 --- a/src/app/server/session-api.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * run cmd with terminal - */ - -const { - terminals -} = require('./remote-common') -const { startSession } = require('./session') - -async function runCmd (body) { - const { pid, cmd } = body - const term = terminals(pid) - let txt = '' - if (term) { - txt = await term.runCmd(cmd) - } - return txt -} - -async function execCmd (body) { - const { pid, cmd, timeoutMs } = body - const term = terminals(pid) - if (!term || typeof term.execCommand !== 'function') { - throw new Error('Exec channel not supported for this session type') - } - return term.execCommand(cmd, { timeoutMs }) -} - -async function resize (body) { - const { pid, cols, rows } = body - const term = terminals(pid) - if (term) { - term.resize(cols, rows) - } - return 'ok' -} - -async function toggleTerminalLog (body) { - const { pid } = body - const term = terminals(pid) - if (term) { - term.toggleTerminalLog() - } - return 'ok' -} - -async function toggleTerminalLogTimestamp (body) { - const { pid } = body - const term = terminals(pid) - if (term) { - term.toggleTerminalLogTimestamp() - } - return 'ok' -} - -async function createTerm (body, ws) { - const t = await startSession(body, ws) - return t.pid -} - -async function testTerm (body, ws) { - const r = await startSession(body, ws, 'test') - if (r) { - return r - } else { - throw new Error('test failed') - } -} - -async function setTerminalLogPath (body) { - const { pid, logPath } = body - const term = terminals(pid) - if (term) { - term.setTerminalLogPath(logPath) - } - return 'ok' -} - -async function startTerminalLogFile (body) { - const { pid, logFilePath, addTimeStampToTermLog } = body - const term = terminals(pid) - if (term) { - term.startTerminalLogFile(logFilePath, addTimeStampToTermLog) - } - return 'ok' -} - -exports.createTerm = createTerm -exports.testTerm = testTerm -exports.resize = resize -exports.runCmd = runCmd -exports.execCmd = execCmd -exports.toggleTerminalLog = toggleTerminalLog -exports.toggleTerminalLogTimestamp = toggleTerminalLogTimestamp -exports.setTerminalLogPath = setTerminalLogPath -exports.startTerminalLogFile = startTerminalLogFile diff --git a/src/app/server/session-base.js b/src/app/server/session-base.js index 353a32d..4047c02 100644 --- a/src/app/server/session-base.js +++ b/src/app/server/session-base.js @@ -1,25 +1,24 @@ /** * terminal/sftp/serial class */ -const generate = require('../common/uid') -const { createLogFileName } = require('../common/create-session-log-file-path') -const SessionLog = require('./session-log') -const time = require('../common/time.js') -const globalState = require('./global-state') - -// const { MockBinding } = require('@serialport/binding-mock') -// MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) +import uid from '../common/uid.js' +import { createLogFileName } from '../common/create-session-log-file-path.js' +import { SessionLog } from './session-log.js' +import globalState from './global-state.js' +import time from '../common/time.js' +import path from 'path' +import pkg from '@xterm/headless' +const { Terminal } = pkg function createVtParser (cols = 4096) { - const { Terminal } = require('@xterm/headless') const term = new Terminal({ cols, rows: 50, allowProposedApi: true }) return term } -class TerminalBase { +export class TerminalBase { constructor (initOptions, ws, isTest) { this.type = initOptions.termType || initOptions.type - this.pid = initOptions.uid || generate() + this.pid = initOptions.uid || uid() this.initOptions = initOptions if (initOptions.saveTerminalLogToFile) { this.sessionLogger = new SessionLog({ @@ -36,6 +35,8 @@ class TerminalBase { } } + cache = '' + prevNewLine = true _initVtParser () { this._vtTerm = createVtParser(this.initOptions.cols || 4096) this._vtLastRow = 0 @@ -54,34 +55,45 @@ class TerminalBase { }) } - toggleTerminalLogTimestamp () { - this.initOptions.addTimeStampToTermLog = !this.initOptions.addTimeStampToTermLog + parse (rawText) { + let result = '' + const len = rawText.length + for (let i = 0; i < len; i++) { + if (rawText[i] === '\b') { + result = result.slice(0, -1) + } else { + result += rawText[i] + } + } + return result } - toggleTerminalLog () { - if (this.sessionLogger) { - this.sessionLogger.destroy() - delete this.sessionLogger - if (this._vtTerm) { - this._vtTerm.dispose() - delete this._vtTerm - } + writeLog (data) { + if (!this.sessionLogger || !this._vtTerm) { + return + } + // Normalize bare \r (carriage return, not part of \r\n) to \r\n. + // Embedded devices (UART/telnet) often use \r-only line endings which + // don't trigger xterm's onLineFeed, causing timestamps to be missing + // for every line except the first. + if (Buffer.isBuffer(data)) { + const str = data.toString('binary') + const normalized = str.replace(/\r(?!\n)/g, '\r\n') + this._vtTerm.write(normalized) } else { - this.sessionLogger = new SessionLog({ - logDir: this.initOptions.sessionLogPath, - fileName: createLogFileName(this.initOptions.logName) - }) - this._initVtParser() + const normalized = String(data).replace(/\r(?!\n)/g, '\r\n') + this._vtTerm.write(normalized) } } + toggleTerminalLogTimestamp () { + this.initOptions.addTimeStampToTermLog = !this.initOptions.addTimeStampToTermLog + } + setTerminalLogPath (logPath) { - if (!logPath) { - return - } + if (!logPath) { return } this.initOptions.sessionLogPath = logPath if (this.sessionLogger) { - // Reopen the log under the new path this.sessionLogger.destroy() if (this._vtTerm) { this._vtTerm.dispose() @@ -99,7 +111,7 @@ class TerminalBase { if (!logFilePath) { return } - const { dirname, basename } = require('path') + const { dirname, basename } = path const logDir = dirname(logFilePath) const fileName = basename(logFilePath) if (this.sessionLogger) { @@ -115,21 +127,20 @@ class TerminalBase { this._initVtParser() } - writeLog (data) { - if (!this.sessionLogger || !this._vtTerm) { - return - } - // Normalize bare \r (carriage return, not part of \r\n) to \r\n. - // Embedded devices (UART/telnet) often use \r-only line endings which - // don't trigger xterm's onLineFeed, causing timestamps to be missing - // for every line except the first. - if (Buffer.isBuffer(data)) { - const str = data.toString('binary') - const normalized = str.replace(/\r(?!\n)/g, '\r\n') - this._vtTerm.write(normalized) + toggleTerminalLog () { + if (this.sessionLogger) { + this.sessionLogger.destroy() + delete this.sessionLogger + if (this._vtTerm) { + this._vtTerm.dispose() + delete this._vtTerm + } } else { - const normalized = String(data).replace(/\r(?!\n)/g, '\r\n') - this._vtTerm.write(normalized) + this.sessionLogger = new SessionLog({ + logDir: this.initOptions.sessionLogPath, + fileName: createLogFileName(this.initOptions.logName) + }) + this._initVtParser() } } @@ -154,5 +165,3 @@ class TerminalBase { globalState.removeSession(pid) } } - -exports.TerminalBase = TerminalBase diff --git a/src/app/server/session-common.js b/src/app/server/session-common.js index 1340af4..f10b65d 100644 --- a/src/app/server/session-common.js +++ b/src/app/server/session-common.js @@ -2,7 +2,7 @@ * terminal/sftp/serial class */ -exports.commonExtends = function (Cls) { +export function commonExtends (Cls) { Cls.prototype.customEnv = function (envs) { if (!envs) { return {} diff --git a/src/app/server/session-ftp.js b/src/app/server/session-ftp.js index 0cc9790..368662c 100644 --- a/src/app/server/session-ftp.js +++ b/src/app/server/session-ftp.js @@ -1,12 +1,12 @@ -const FtpClientWrapper = require('./ftp-client') -const { TerminalBase } = require('./session-base') -const { commonExtends } = require('./session-common') -const { readRemoteFile, writeRemoteFile } = require('./ftp-file') -const { Readable, PassThrough } = require('stream') -const { posix: path } = require('path') -const globalState = require('./global-state') - -class Ftp extends TerminalBase { +import { FtpClientWrapper } from './ftp-client.js' +import { TerminalBase } from './session-base.js' +import { commonExtends } from './session-common.js' +import { readRemoteFile, writeRemoteFile } from './ftp-file.js' +import { Readable, PassThrough } from 'stream' +import { posix as path } from 'path' +import globalState from './global-state.js' + +export class FtpSession extends TerminalBase { constructor (initOptions) { super({ ...initOptions, @@ -303,4 +303,4 @@ class Ftp extends TerminalBase { } } -exports.Ftp = commonExtends(Ftp) +export const Ftp = commonExtends(FtpSession) diff --git a/src/app/server/session-hop.js b/src/app/server/session-hop.js index 5fc4be2..9426960 100644 --- a/src/app/server/session-hop.js +++ b/src/app/server/session-hop.js @@ -7,12 +7,13 @@ * Used by both VNC and RDP sessions. */ -const uid = require('../common/uid') -const { session } = require('./session-ssh') +import uid from '../common/uid.js' +import { terminalSsh } from './session-ssh.js' +import findFreePort from 'find-free-port' function getPort (fromPort = 12023) { return new Promise((resolve, reject) => { - require('find-free-port')(fromPort, '127.0.0.1', function (err, freePort) { + findFreePort(fromPort, '127.0.0.1', function (err, freePort) { if (err) { reject(err) } else { @@ -26,11 +27,11 @@ function getPort (fromPort = 12023) { * Set up an SSH hop tunnel if connectionHoppings are configured. * * @param {object} initOptions - Session init options - * @param {Array} initOptions.connectionHoppings - Hop server definitions (mutated: last item is popped) + * @param {Array} initOptions.connectionHoppings - Hop server definitions (mutated: last item is popped) * @param {string} [initOptions.proxy] - Existing proxy URL to chain through * @returns {Promise<{ proxyUrl: string|null, ssh: object|null }>} * proxyUrl - SOCKS5 URL to use for the final connection, or original proxy, or null - * ssh - SSH session that must be killed on cleanup, or null + * ssh - SSH session that must be killed on cleanup, or null */ async function createHopProxy (initOptions) { const { @@ -68,8 +69,8 @@ async function createHopProxy (initOptions) { ] } - const ssh = await session(initOpts) + const ssh = await terminalSsh(initOpts) return { proxyUrl: `socks5://127.0.0.1:${fp}`, ssh } } -module.exports = { createHopProxy, getPort } +export { createHopProxy, getPort } diff --git a/src/app/server/session-local.js b/src/app/server/session-local.js index 39136e9..4e559c1 100644 --- a/src/app/server/session-local.js +++ b/src/app/server/session-local.js @@ -1,96 +1,118 @@ /** * terminal/sftp/serial class */ +import { resolve as pathResolve } from 'path' +import globalState from './global-state.js' +import { TerminalBase } from './session-base.js' +import log from '../common/log.js' + +// `node-pty` is a native module that is not built for Android yet. Load it +// lazily and tolerate its absence so the server can still start; the local +// terminal is also disabled via DISABLE_LOCAL_TERMINAL. +let nodePtyPromise = null +function loadNodePty () { + if (!nodePtyPromise) { + nodePtyPromise = import('node-pty') + .then(m => m.default) + .catch(err => { + log.warn('node-pty is not available, local terminal disabled:', err.message) + return null + }) + } + return nodePtyPromise +} -// const { resolve: pathResolve } = require('path') -const { TerminalBase } = require('./session-base') -// const globalState = require('./global-state') // const { MockBinding } = require('@serialport/binding-mock') // MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) class TerminalLocal extends TerminalBase { - init () { - throw new Error('Local not supported') - // const { - // cols, - // rows, - // execWindows, - // execMac, - // execLinux, - // execWindowsArgs, - // execMacArgs, - // execLinuxArgs, - // termType, - // term - // } = this.initOptions - // this.isLocal = true - // const { platform } = process - // const isWin = platform.startsWith('win') - // const exec = isWin - // ? pathResolve( - // process.env.windir, - // execWindows - // ) - // : platform === 'darwin' ? execMac : execLinux - // if ((exec || '').includes('..')) { - // return Promise.reject(new Error('execWindows should not contain ".."')) - // } - // const arg = isWin - // ? execWindowsArgs - // : platform === 'darwin' ? execMacArgs : execLinuxArgs - // const cwd = process.env[platform === 'win32' ? 'USERPROFILE' : 'HOME'] - // const argv = platform.startsWith('darwin') ? ['--login', ...arg] : arg - // const pty = require('node-pty') - // const env = Object.assign({}, process.env) - // delete env.ELECTRON_RUN_AS_NODE - // delete env.NODE_OPTIONS - // delete env.ELECTRON_NO_ATTACH_CONSOLE + async init () { + const pty = await loadNodePty() + if (!pty) { + return Promise.reject(new Error('Local terminal is not available on this platform')) + } + const { + cols, + rows, + execWindows, + execMac, + execLinux, + execWindowsArgs, + execMacArgs, + execLinuxArgs, + termType, + term + } = this.initOptions + this.isLocal = true + const { platform } = process + const isWin = platform.startsWith('win') + const exec = isWin + ? pathResolve( + process.env.windir, + execWindows + ) + : platform === 'darwin' ? execMac : execLinux + if ((exec || '').includes('..')) { + return Promise.reject(new Error('execWindows should not contain ".."')) + } + const arg = isWin + ? execWindowsArgs + : platform === 'darwin' ? execMacArgs : execLinuxArgs + const cwd = process.env[platform === 'win32' ? 'USERPROFILE' : 'HOME'] + const argv = platform.startsWith('darwin') ? ['--login', ...(arg || [])] : arg + const env = Object.assign({}, process.env) + delete env.ELECTRON_RUN_AS_NODE + delete env.NODE_OPTIONS + delete env.ELECTRON_NO_ATTACH_CONSOLE // temp PEM of system CAs for the server process (WebDAV sync, #4347) — // not meant for user shells, and a bad keychain cert makes any Node/bun // tool in the terminal print "ignoring extra certs ... load failed" - // delete env.NODE_EXTRA_CA_CERTS - // this.term = pty.spawn(exec, argv, { - // name: term, - // encoding: null, - // cols: cols || 80, - // rows: rows || 24, - // cwd, - // env, - // // Use the OpenConsole conpty.dll shipped with node-pty instead of the - // // legacy Windows Console Host (kernel32 CreatePseudoConsole) conpty. - // // The legacy console-host conpty can stall output and deliver Ctrl+C to - // // the whole process group (killing the shell too) after a full-screen - // // TUI like opencode exits, leaving the terminal tab unresponsive. - // // The OpenConsole conpty.dll does not have this problem. - // useConptyDll: true - // }) - // this.term.termType = termType - // globalState.setSession(this.pid, this) - // return Promise.resolve(this) + delete env.NODE_EXTRA_CA_CERTS + this.term = pty.spawn(exec, argv, { + name: term, + encoding: null, + cols: cols || 80, + rows: rows || 24, + cwd, + env, + // Use the OpenConsole conpty.dll shipped with node-pty instead of the + // legacy Windows Console Host (kernel32 CreatePseudoConsole) conpty. + // The legacy console-host conpty can stall output and deliver Ctrl+C to + // the whole process group (killing the shell too) after a full-screen + // TUI like opencode exits, leaving the terminal tab unresponsive. + // The OpenConsole conpty.dll does not have this problem. + useConptyDll: true + }) + this.term.termType = termType + globalState.setSession(this.pid, this) + return Promise.resolve(this) } - // resize (cols, rows) { - // this.term.resize(cols, rows) - // } + resize (cols, rows) { + this.term.resize(cols, rows) + } - // on (event, cb) { - // this.term.on(event, cb) - // } + on (event, cb) { + this.term.on(event, cb) + } - // write (data) { - // this.term.write(data) - // } + write (data) { + this.term.write(data) + } - // kill () { - // if (this.sessionLogger) { - // this.sessionLogger.destroy() - // } - // this.term && this.term.kill() - // this.onEndConn() - // } + kill () { + if (this.sessionLogger) { + this.sessionLogger.destroy() + } + this.term && this.term.kill() + this.onEndConn() + } } -exports.session = function (initOptions, ws) { +export const terminalLocal = function (initOptions, ws) { + if (process.env.DISABLE_LOCAL_TERMINAL) { + return Promise.reject(new Error('Local terminal is disabled')) + } return (new TerminalLocal(initOptions, ws)).init() } @@ -98,6 +120,12 @@ exports.session = function (initOptions, ws) { * test ssh connection * @param {object} options */ -exports.test = (initOptions) => { +export const testConnectionLocal = (initOptions) => { + if (process.env.DISABLE_LOCAL_TERMINAL) { + return Promise.reject(new Error('Local terminal is disabled')) + } return Promise.resolve(true) } + +export const terminal = terminalLocal +export const testConnection = testConnectionLocal diff --git a/src/app/server/session-log.js b/src/app/server/session-log.js index ecb5d05..67e3c4e 100644 --- a/src/app/server/session-log.js +++ b/src/app/server/session-log.js @@ -2,25 +2,27 @@ * log ssh output to file */ -const { resolve } = require('path') -const { existsSync, mkdirSync, createWriteStream } = require('fs') +import { resolve, dirname } from 'path' +import { createWriteStream, existsSync, mkdirSync } from 'fs' +import { cwd } from '../common/runtime-constants.js' -function mkLogDir (logDir) { - try { - if (!existsSync(logDir)) { - mkdirSync(logDir) - } - } catch (e) { - console.debug('read default user name error') +function mkdirP (resolvedPath) { + if (!existsSync(resolvedPath)) { + mkdirP(dirname(resolvedPath)) + mkdirSync(resolvedPath) } } -class SessionLog { +const { DB_PATH } = process.env +const dataPath = DB_PATH || resolve(cwd, 'data') + +export const logDir = resolve(dataPath, 'electerm_session_logs') + +export class SessionLog { constructor (options) { - this.options = options const { logDir } = options const logPath = resolve(logDir, options.fileName) - mkLogDir(logDir) + mkdirP(logDir) this.stream = createWriteStream(logPath, { flags: 'a' }) } @@ -32,5 +34,3 @@ class SessionLog { this.stream.destroy() } } - -module.exports = SessionLog diff --git a/src/app/server/session-process.js b/src/app/server/session-process.js deleted file mode 100644 index 443fad4..0000000 --- a/src/app/server/session-process.js +++ /dev/null @@ -1,274 +0,0 @@ -/** - * session-process.js — manages terminal session servers in-process. - * - * Each session is an Express app on its own port, created by - * createSessionServer() from session-server.js. No child processes. - * Communication is via EventEmitter channels. - */ - -const { createSessionServer } = require('./session-server') - -// Map to store active terminal processes (pid -> {session, port, ws}) -const activeTerminals = new Map() - -// Track the last port assigned -let lastPort = 30975 -const MIN_PORT = 30975 -const MAX_PORT = 65534 -// Add a set to track ports that are currently being assigned -const pendingPorts = new Set() - -function getPort (fromPort = MIN_PORT) { - // Use the last port + 1 or start over if we've reached MAX_PORT - let startPort = lastPort >= MAX_PORT ? MIN_PORT : lastPort + 1 - - // Skip ports that are currently being assigned - while (pendingPorts.has(startPort)) { - startPort = startPort >= MAX_PORT ? MIN_PORT : startPort + 1 - } - - // Mark this port as pending - pendingPorts.add(startPort) - - return new Promise((resolve, reject) => { - require('find-free-port')(startPort, '127.0.0.1', function (err, freePort) { - if (err) { - pendingPorts.delete(startPort) - reject(err) - } else { - lastPort = freePort - pendingPorts.delete(startPort) - resolve(freePort) - } - }) - }) -} - -const electermHost = process.env.electermHost || '127.0.0.1' - -async function runSessionServer (type, port) { - return new Promise((resolve, reject) => { - const session = createSessionServer(type, port, electermHost) - - session.channel.on('ready', () => { - resolve(session) - }) - - // Timeout: if server doesn't start within 10s, reject - setTimeout(() => { - if (!session.server.listening) { - session.kill() - reject(new Error('Session server startup timed out')) - } - }, 10000) - }) -} - -/** - * Send a command to a session and wait for the response. - * Works the same as the old sendMsgToChildProcess but via channel. - */ -async function sendMsgToSession (session, msg) { - return new Promise((resolve, reject) => { - const responseHandler = (response) => { - // Only match command responses (not SSH data relay which has type:'common') - if (response.id === msg.id && !response.type) { - session.channel.removeListener('to-parent', responseHandler) - if (response.error) { - reject(response.error) - } else { - resolve(response.data) - } - } - } - - session.channel.on('to-parent', responseHandler) - session.channel.toChild({ - type: 'common', - data: msg - }) - }) -} - -exports.terminal = async function (initOptions, ws, uid) { - const type = initOptions.termType || initOptions.type || 'terminal' - const port = await getPort() - const session = await runSessionServer(type, port) - const pid = initOptions.uid - const isSsh = ![ - 'telnet', - 'serial', - 'local', - 'rdp', - 'vnc', - 'spice', - 'ftp' - ].includes(type) - - if (isSsh) { - // Relay SSH data between session and client WebSocket - session.channel.on('to-parent', (m) => { - if (m.type === 'common') { - ws.s(m.data) - ws.once((data) => { - session.channel.toChild(data) - }, m.data.id) - } - }) - } - - session.channel.on('exit', () => { - session.channel.removeAllListeners('to-parent') - activeTerminals.delete(pid) - }) - - if (type !== 'ftp') { - try { - await sendMsgToSession(session, { - id: uid, - action: 'create-terminal', - body: initOptions - }) - } catch (err) { - session.kill() - throw err - } - } - - // Kill any existing session for this pid before overwriting - const existingEntry = activeTerminals.get(pid) - if (existingEntry) { - existingEntry.session.kill() - activeTerminals.delete(pid) - } - - activeTerminals.set(pid, { - session, - port, - ws - }) - - return { - pid, - port - } -} - -exports.testConnection = async function (initOptions, ws, uid) { - const type = initOptions.termType || initOptions.type || 'terminal' - const port = await getPort() - const session = await runSessionServer(type, port) - - const isSsh = ![ - 'telnet', - 'serial', - 'local', - 'rdp', - 'vnc', - 'spice', - 'ftp' - ].includes(type) - if (isSsh && ws) { - session.channel.on('to-parent', (m) => { - if (m.type === 'common') { - ws.s(m.data) - ws.once((respData) => { - session.channel.toChild(respData) - }, m.data.id) - } - }) - } - - const res = await sendMsgToSession(session, { - id: uid, - action: 'test-terminal', - body: initOptions - }) - - session.kill() - return res -} - -/** - * Get terminal instance by pid - * @param {string} pid - Process ID of the terminal - * @returns {object|null} Terminal instance or null if not found - */ -exports.terminals = function (pid) { - const terminal = activeTerminals.get(pid) - if (!terminal) { - return null - } - - return { - runCmd: async (cmd, id) => { - return sendMsgToSession(terminal.session, { - id, - action: 'run-cmd', - body: { cmd, pid } - }) - }, - execCommand: async (cmd, timeoutMs, id) => { - return sendMsgToSession(terminal.session, { - id, - action: 'exec-cmd', - body: { cmd, pid, timeoutMs } - }) - }, - resize: (cols, rows, id) => { - sendMsgToSession(terminal.session, { - id, - action: 'resize-terminal', - body: { cols, rows, pid } - }) - }, - toggleTerminalLog: (id) => { - sendMsgToSession(terminal.session, { - id, - action: 'toggle-terminal-log', - body: { pid } - }) - }, - toggleTerminalLogTimestamp: (id) => { - sendMsgToSession(terminal.session, { - id, - action: 'toggle-terminal-log-timestamp', - body: { pid } - }) - }, - setTerminalLogPath: (id, logPath) => { - sendMsgToSession(terminal.session, { - id, - action: 'set-terminal-log-path', - body: { pid, logPath } - }) - }, - startTerminalLogFile: (id, logFilePath, addTimeStampToTermLog) => { - sendMsgToSession(terminal.session, { - id, - action: 'start-terminal-log-file', - body: { pid, logFilePath, addTimeStampToTermLog } - }) - } - } -} - -/** - * Clean up all active terminals - */ -exports.cleanupTerminals = function () { - for (const [pid, terminal] of activeTerminals) { - terminal.session.kill() - activeTerminals.delete(pid) - } -} - -// Clean up on process exit -process.on('SIGINT', () => { - exports.cleanupTerminals() - process.exit() -}) -process.on('SIGTERM', () => { - exports.cleanupTerminals() - process.exit() -}) diff --git a/src/app/server/session-rdp.js b/src/app/server/session-rdp.js index 57d292d..4524f7d 100644 --- a/src/app/server/session-rdp.js +++ b/src/app/server/session-rdp.js @@ -1,24 +1,15 @@ /** - * RDP session using IronRDP WASM + RDCleanPath proxy - * - * Architecture: - * Browser (IronRDP WASM) <--WebSocket--> This Proxy <--TLS--> RDP Server - * - * The WASM client handles all RDP protocol logic. - * This server-side code acts as a RDCleanPath proxy: - * 1. Receives RDCleanPath Request from WASM client (ASN.1 DER binary) - * 2. TCP connects to the RDP server (optionally through proxy) - * 3. Performs X.224 handshake + TLS upgrade - * 4. Sends RDCleanPath Response (with certs) back to WASM client - * 5. Bidirectional relay: WebSocket <-> TLS + * terminal/sftp/serial class */ -const log = require('../common/log') -const { TerminalBase } = require('./session-base') -const globalState = require('./global-state') -const { +import log from '../common/log.js' +import { TerminalBase } from './session-base.js' +import globalState from './global-state.js' +import { handleConnection -} = require('./rdp-proxy') -const { createHopProxy } = require('./session-hop') +} from './rdp-proxy.js' +import { createHopProxy } from './session-hop.js' +import proxySock from './socks.js' +import net from 'net' class TerminalRdp extends TerminalBase { init = async () => { @@ -66,8 +57,6 @@ class TerminalRdp extends TerminalBase { } test = async () => { - const net = require('net') - const proxySock = require('./socks') const { host, port = 3389, @@ -132,23 +121,27 @@ class TerminalRdp extends TerminalBase { } } -exports.session = async function (initOptions, ws) { +export const terminalRdp = async function (initOptions, ws) { const term = new TerminalRdp(initOptions, ws) await term.init() return term } /** - * test RDP connection (TCP connectivity check) + * test ssh connection * @param {object} options */ -exports.test = (options) => { +export const testConnectionRdp = (options) => { return (new TerminalRdp(options, undefined, true)) .test() - .then(() => { + .then((res) => { + res.close() return true }) .catch(() => { return false }) } + +export const terminal = terminalRdp +export const testConnection = testConnectionRdp diff --git a/src/app/server/session-serial.js b/src/app/server/session-serial.js index c9c197e..7fcb09c 100644 --- a/src/app/server/session-serial.js +++ b/src/app/server/session-serial.js @@ -1,123 +1,142 @@ /** * terminal/sftp/serial class */ -const { TerminalBase } = require('./session-base') -// const log = require('../common/log') -// const globalState = require('./global-state') +import { TerminalBase } from './session-base.js' +import log from '../common/log.js' +import globalState from './global-state.js' + +// `serialport` is a native module that is not built for Android yet. Load it +// lazily and tolerate its absence so the server can still start. +let serialPortPromise = null +function loadSerialPort () { + if (!serialPortPromise) { + serialPortPromise = import('serialport') + .then(m => m.SerialPort) + .catch(err => { + log.warn('serialport is not available, serial terminals disabled:', err.message) + return null + }) + } + return serialPortPromise +} // const { MockBinding } = require('@serialport/binding-mock') // MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) class TerminalSerial extends TerminalBase { async init () { - throw new Error('Serial not supported') - // const { SerialPort } = require('serialport') - // // https://serialport.io/docs/api-stream - // const { - // autoOpen = true, - // baudRate = 9600, - // dataBits = 8, - // lock = true, - // stopBits = 1, - // parity = 'none', - // rtscts = false, - // xon = false, - // xoff = false, - // xany = false, - // txLineEnding = '\r', - // rxLineEnding = 'none', - // path - // } = this.initOptions - // this.txLineEnding = txLineEnding - // this.rxLineEnding = rxLineEnding - // await new Promise((resolve, reject) => { - // this.port = new SerialPort({ - // // binding: MockBinding, - // path, - // autoOpen, - // baudRate, - // dataBits, - // lock, - // stopBits, - // parity, - // rtscts, - // xon, - // xoff, - // xany - // }, (err) => { - // if (err) { - // reject(err) - // } else { - // resolve('ok') - // } - // }) - // }) - // if (this.isTest) { - // this.kill() - // return true - // } - // globalState.setSession(this.pid, this) - // return Promise.resolve(this) + // https://serialport.io/docs/api-stream + const { + autoOpen = true, + baudRate = 9600, + dataBits = 8, + lock = true, + stopBits = 1, + parity = 'none', + rtscts = false, + xon = false, + xoff = false, + xany = false, + txLineEnding = '\r', + rxLineEnding = 'none', + path + } = this.initOptions + const SerialPort = await loadSerialPort() + if (!SerialPort) { + return Promise.reject(new Error('Serial port support is not available on this platform')) + } + this.txLineEnding = txLineEnding + this.rxLineEnding = rxLineEnding + await new Promise((resolve, reject) => { + this.port = new SerialPort({ + // binding: MockBinding, + path, + autoOpen, + baudRate, + dataBits, + lock, + stopBits, + parity, + rtscts, + xon, + xoff, + xany + }, (err) => { + if (err) { + reject(err) + } else { + resolve('ok') + } + }) + }) + if (this.isTest) { + this.kill() + return true + } + globalState.setSession(this.pid, this) } - // resize () { + resize () { - // } + } - // on (event, cb) { - // if (event === 'data' && this.rxLineEnding && this.rxLineEnding !== 'none') { - // this.port.on('data', (data) => { - // const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) - // let processed - // if (this.rxLineEnding === 'lf_to_crlf') { - // processed = str.replace(/\r?\n/g, '\r\n') - // } else if (this.rxLineEnding === 'cr_to_crlf') { - // processed = str.replace(/\r(?!\n)/g, '\r\n') - // } else { - // processed = str - // } - // cb(Buffer.isBuffer(data) ? Buffer.from(processed, 'latin1') : processed) - // }) - // } else { - // this.port.on(event, cb) - // } - // } + on (event, cb) { + if (event === 'data' && this.rxLineEnding && this.rxLineEnding !== 'none') { + this.port.on('data', (data) => { + const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) + let processed + if (this.rxLineEnding === 'lf_to_crlf') { + processed = str.replace(/\r?\n/g, '\r\n') + } else if (this.rxLineEnding === 'cr_to_crlf') { + processed = str.replace(/\r(?!\n)/g, '\r\n') + } else { + processed = str + } + cb(Buffer.isBuffer(data) ? Buffer.from(processed, 'latin1') : processed) + }) + } else { + this.port.on(event, cb) + } + } - // write (data) { - // try { - // const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) - // let out = str - // if (this.txLineEnding && this.txLineEnding !== '\r') { - // out = str.replace(/\r\n|\r|\n/g, this.txLineEnding) - // } - // this.port.write(Buffer.isBuffer(data) ? Buffer.from(out, 'latin1') : out) - // } catch (e) { - // log.error(e) - // } - // } + write (data) { + try { + const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) + let out = str + if (this.txLineEnding && this.txLineEnding !== '\r') { + out = str.replace(/\r\n|\r|\n/g, this.txLineEnding) + } + this.port.write(Buffer.isBuffer(data) ? Buffer.from(out, 'latin1') : out) + if (this.sessionLogger) { + this.sessionLogger.write(data) + } + } catch (e) { + log.error(e) + } + } - // /** - // * Write raw bytes directly to the serial port, bypassing txLineEnding transformation. - // * Used by binary protocols (XMODEM) to avoid corruption of protocol bytes. - // */ - // writeRaw (data) { - // try { - // this.port.write(data) - // } catch (e) { - // log.error(e) - // } - // } + /** + * Write raw bytes directly to the serial port, bypassing txLineEnding transformation. + * Used by binary protocols (XMODEM) to avoid corruption of protocol bytes. + */ + writeRaw (data) { + try { + this.port.write(data) + } catch (e) { + log.error(e) + } + } - // kill () { - // if (this.sessionLogger) { - // this.sessionLogger.destroy() - // } - // this.port && this.port.isOpen && this.port.close() - // delete this.port - // this.onEndConn() - // } + kill () { + if (this.sessionLogger) { + this.sessionLogger.destroy() + } + this.port && this.port.isOpen && this.port.close() + delete this.port + this.onEndConn() + } } -exports.session = async function (initOptions, ws) { +export async function terminalSerial (initOptions, ws) { const term = new TerminalSerial(initOptions, ws) await term.init() return term @@ -127,7 +146,7 @@ exports.session = async function (initOptions, ws) { * test ssh connection * @param {object} options */ -exports.test = (initOptions) => { +export function testConnectionSerial (initOptions) { return (new TerminalSerial(initOptions, undefined, true)) .init() .then(() => true) @@ -135,3 +154,6 @@ exports.test = (initOptions) => { return false }) } + +export const terminal = terminalSerial +export const testConnection = testConnectionSerial diff --git a/src/app/server/session-server.js b/src/app/server/session-server.js deleted file mode 100644 index 6714923..0000000 --- a/src/app/server/session-server.js +++ /dev/null @@ -1,615 +0,0 @@ -/** - * session-server.js — factory for creating in-process session servers. - * - * Each call to createSessionServer() creates a new Express app listening - * on its own port, with WebSocket routes for terminal/sftp/transfer. - * Communication with the parent (session-process.js) is via an - * EventEmitter channel instead of process IPC. - */ - -const EventEmitter = require('events') -const express = require('express') -const { Sftp } = require('./session-sftp') -const { instSftpKeys } = require('../common/constants') -const { Ftp } = require('./session-ftp') -const { - sftp, - transfer, - onDestroySftp, - onDestroyTransfer, - terminals -} = require('./remote-common') -const { Transfer, transferKeys } = require('./transfer') -const { Transfer: FtpTransfer } = require('./ftp-transfer') -const log = require('../common/log') -const appDec = require('./app-wrap') -const { - createTerm, - testTerm, - resize, - runCmd, - execCmd, - toggleTerminalLog, - toggleTerminalLogTimestamp, - setTerminalLogPath, - startTerminalLogFile -} = require('./session-api') -const wsDec = require('./ws-dec') -const { zmodemManager } = require('./zmodem') -const { trzszManager } = require('./trzsz') -const { xmodemManager } = require('./xmodem') - -// True when the buffered data ends mid-way through a multi-byte UTF-8 -// sequence (CJK chars are 3 bytes). Slow SSH servers (embedded router CLIs) -// often deliver one char split across TCP segments; flushing such a buffer -// right away would push a partial char to the client. Only the tail of the -// last buffer is inspected (at most 4 bytes), so this is O(1). -function hasIncompleteTrailingUtf8 (bufs) { - const last = bufs[bufs.length - 1] - if (!last) { - return false - } - const buf = Buffer.isBuffer(last) ? last : Buffer.from(last) - const len = buf.length - if (!len) { - return false - } - // Count trailing continuation bytes (10xxxxxx), at most 3 - let cont = 0 - while (cont < 3 && cont < len && (buf[len - 1 - cont] & 0xc0) === 0x80) { - cont++ - } - const leadIdx = len - 1 - cont - if (leadIdx < 0) { - // Whole buffer is continuation bytes; the lead byte was in a chunk that - // was already flushed, so holding can not reassemble anything. - return false - } - const lead = buf[leadIdx] - if (lead < 0xc0) { - // ASCII last byte, or stray continuations after ASCII: nothing to wait for - return false - } - // Expected continuation count for this lead byte: - // 110xxxxx -> 1, 1110xxxx -> 2, 11110xxx -> 3 - const needed = lead < 0xe0 ? 1 : lead < 0xf0 ? 2 : 3 - return cont < needed -} - -let _pidCounter = 100001 - -/** - * Create a session server running in-process. - * - * @param {string} type - session type: terminal, rdp, vnc, spice, etc. - * @param {number} wsPort - port to listen on - * @param {string} electermHost - host to bind to - * @returns {{ channel: EventEmitter, kill: Function, pid: number, port: number }} - */ -function createSessionServer (type, wsPort, electermHost) { - const app = express() - const channel = new EventEmitter() - channel.setMaxListeners(100) - - // Helper methods on channel: - // channel.toParent(msg) — child → parent (replaces process.send) - // channel.toChild(msg) — parent → child (replaces process.on('message')) - channel.toParent = (msg) => channel.emit('to-parent', msg) - channel.toChild = (msg) => channel.emit('to-child', msg) - - const tokenElecterm = process.env.tokenElecterm - - // Track whether any WebSocket has connected to detect orphaned servers - let firstWsConnected = false - function markConnected () { - firstWsConnected = true - } - - function verify (req) { - const { token: to } = req.query - if (to !== tokenElecterm) { - throw new Error('not valid request') - } - } - - appDec(app) - - // --- WebSocket routes (same logic as original, using local `app`) --- - - if (type === 'rdp') { - app.ws('/rdp/:pid', function (ws, req) { - const { width, height } = req.query - verify(req) - markConnected() - const term = terminals(req.params.pid) - term.ws = ws - log.debug('ws: connected to rdp session ->', term.pid, 'width=', width, 'height=', height) - term.start(width, height) - ws.on('error', (err) => { - log.error('rdp ws error:', err) - }) - ws.on('close', () => { - log.debug('ws: rdp session ws closed ->', term.pid) - cleanup() - }) - }) - } else if (type === 'vnc') { - app.ws('/vnc/:pid', function (ws, req) { - const { query } = req - verify(req) - markConnected() - const { pid } = req.params - const term = terminals(pid) - term.ws = ws - term.start(query) - log.debug('ws: connected to vnc session ->', pid) - ws.on('error', (err) => { - log.error(err) - }) - ws.on('close', () => { - cleanup() - }) - }) - } else if (type === 'spice') { - app.ws('/spice/:pid', function (ws, req) { - const { query } = req - verify(req) - markConnected() - const { pid } = req.params - const term = terminals(pid) - log.debug('ws: connected to spice session ->', pid) - term.start(query, ws) - ws.on('error', (err) => { - log.error(err) - }) - }) - } else { - app.ws('/terminals/:pid', function (ws, req) { - verify(req) - markConnected() - const term = terminals(req.params.pid) - const { pid } = term - log.debug('ws: connected to terminal ->', pid) - - const dataBuffer = [] - let sendTimeout = null - // Time of the last actual flush. Lets a chunk arriving after an idle gap - // (keystroke echo, command result) skip the coalescing delay entirely, - // so only chunks arriving inside an active burst (floods) pay the 10ms - // wait. Mirrors the client-side coalescing fast path. - let lastFlushTime = 0 - const flushIntervalMs = 10 - - const flushBufferedData = () => { - if (!dataBuffer.length) { - sendTimeout = null - return - } - lastFlushTime = Date.now() - const combinedData = Buffer.concat(dataBuffer.splice(0).map(d => Buffer.isBuffer(d) ? d : Buffer.from(d))) - - term.writeLog(combinedData) - - const zmodemConsumed = zmodemManager.handleData(pid, combinedData, term, ws) - if (zmodemConsumed) { - sendTimeout = null - return - } - - const trzszConsumed = trzszManager.handleData(pid, combinedData, term, ws) - if (trzszConsumed) { - sendTimeout = null - return - } - - if (term.port) { - detectXmodemMarker(combinedData.toString('utf8')) - } - - const xmodemConsumed = xmodemManager.handleData(pid, combinedData, term, ws) - if (xmodemConsumed) { - sendTimeout = null - return - } - - ws.send(combinedData) - sendTimeout = null - } - - ws.s = (data) => { - ws.send(JSON.stringify(data)) - } - - function detectXmodemMarker (text) { - const txMatch = text.match(/\[XMODEM:TX:(.+?)\]/) - if (txMatch) { - ws.s({ - action: 'xmodem-event', - event: 'auto-trigger-receive', - name: txMatch[1] - }) - return - } - const rxMatch = text.match(/\[XMODEM:RX\]/) - if (rxMatch) { - ws.s({ - action: 'xmodem-event', - event: 'auto-trigger-send' - }) - } - } - - term.on('data', function (data) { - if (zmodemManager.isActive(pid)) { - term.writeLog(data) - zmodemManager.handleData(pid, data, term, ws) - return - } - - if (trzszManager.isActive(pid)) { - term.writeLog(data) - trzszManager.handleData(pid, data, term, ws) - return - } - - if (term.port) { - const text = Buffer.isBuffer(data) ? data.toString('utf8') : data - detectXmodemMarker(text) - } - - if (xmodemManager.isActive(pid)) { - if (!term.port) { - term.writeLog(data) - xmodemManager.handleData(pid, data, term, ws) - } - return - } - - const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data) - - if (chunk.length > 16384) { - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - if (dataBuffer.length) { - flushBufferedData() - } - term.writeLog(chunk) - const zmodemConsumed = zmodemManager.handleData(pid, chunk, term, ws) - if (zmodemConsumed) { - return - } - const trzszConsumed = trzszManager.handleData(pid, chunk, term, ws) - if (trzszConsumed) { - return - } - const xmodemConsumed = xmodemManager.handleData(pid, chunk, term, ws) - if (xmodemConsumed) { - return - } - ws.send(chunk) - return - } - - dataBuffer.push(chunk) - - // Idle fast path: if nothing has been flushed within the coalescing - // window, this is the start of a new burst (or a lone interactive - // echo) rather than a continuation of a flood - send it right away - // instead of paying the fixed delay. Only chunks arriving while a - // burst is already in flight (elapsed < flushIntervalMs) get batched. - const elapsed = Date.now() - lastFlushTime - if (elapsed >= flushIntervalMs) { - // Never fast-flush a buffer that ends mid-way through a multi-byte - // UTF-8 char: a slow peer (router CLI) may deliver one char split - // across TCP segments, and the remaining bytes usually land within a - // few ms. Hold one coalescing window so they get concatenated first - // (the completing chunk then flushes immediately via this same fast - // path). Bounded by the timeout, so it can not stick. - if (hasIncompleteTrailingUtf8(dataBuffer)) { - if (!sendTimeout) { - sendTimeout = setTimeout(flushBufferedData, flushIntervalMs) - } - return - } - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - flushBufferedData() - return - } - - // If no timeout is pending, schedule a batched send - if (!sendTimeout) { - sendTimeout = setTimeout(flushBufferedData, flushIntervalMs - elapsed) - } - }) - - if (term.port) { - term.port.on('data', function (rawData) { - if (xmodemManager.isActive(pid)) { - term.writeLog(rawData) - xmodemManager.handleData(pid, rawData, term, ws) - } - }) - } - - let onCloseCalled = false - function onClose () { - if (onCloseCalled) return - onCloseCalled = true - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - dataBuffer.length = 0 - zmodemManager.destroySession(pid) - trzszManager.destroySession(pid) - xmodemManager.destroySession(pid) - term.kill() - log.debug('Closed terminal ' + pid) - ws.close && ws.close() - cleanup() - } - - term.on('close', onClose) - - ws.on('message', function (msg) { - try { - if (typeof msg === 'string') { - try { - const parsed = JSON.parse(msg) - if (parsed.action === 'zmodem-event') { - zmodemManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'trzsz-event') { - trzszManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'xmodem-event') { - xmodemManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'keepalive') { - term.write('\n\r\x1b[K') - return - } - } catch (e) { - // Not JSON, treat as regular terminal input - } - } - // Let an active zmodem session observe Ctrl-C (transfer abort); - // the keystroke itself is still written through untouched. - zmodemManager.handleUserInput(pid, msg) - term.write(msg) - } catch (ex) { - log.error(ex) - } - }) - - ws.on('error', (err) => { - log.error(err) - }) - - ws.on('close', onClose) - }) - - // sftp function - app.ws('/sftp/:id', (ws, req) => { - verify(req) - wsDec(ws) - const { id } = req.params - ws.on('close', () => { - onDestroySftp(id) - }) - ws.on('message', (message) => { - const msg = JSON.parse(message) - const { action } = msg - - if (action === 'sftp-new') { - const { id, terminalId, type } = msg - const Cls = type === 'ftp' ? Ftp : Sftp - sftp(id, new Cls({ - uid: id, - terminalId, - type - })) - } else if (action === 'sftp-func') { - const { id, args, func, uid } = msg - const inst = sftp(id) - if (inst) { - if (!instSftpKeys.includes(func) || typeof inst[func] !== 'function') { - ws.s({ - id: uid, - error: { - message: 'invalid sftp function: ' + func, - stack: '' - } - }) - return - } - inst[func](...args) - .then(data => { - ws.s({ - id: uid, - data - }) - }) - .catch(err => { - ws.s({ - id: uid, - error: { - message: err.message, - stack: err.stack - } - }) - }) - } - } else if (action === 'sftp-destroy') { - const { id } = msg - ws.close() - onDestroySftp(id) - } - }) - }) - - // transfer function - app.ws('/transfer/:id', (ws, req) => { - verify(req) - wsDec(ws) - const { id } = req.params - const { sftpId } = req.query - - ws.on('close', () => { - onDestroyTransfer(id, sftpId) - }) - - ws.on('message', (message) => { - const msg = JSON.parse(message) - const { action } = msg - - if (action === 'transfer-new') { - const { sftpId, id, isFtp } = msg - const session = sftp(sftpId) - const encode = session.initOptions?.encode || 'utf8' - const opts = Object.assign({}, msg, { - sftp: session.sftp, - conn: session.client, - ftpSession: isFtp ? session : null, - sftpId, - ws, - encode - }) - const Cls = isFtp ? FtpTransfer : Transfer - transfer(id, sftpId, new Cls(opts)) - } else if (action === 'transfer-func') { - const { id, func, args, sftpId } = msg - if (func === 'destroy') { - return onDestroyTransfer(id, sftpId) - } - if (!transferKeys.includes(func)) { - return - } - const tr = transfer(id, sftpId) - if (!tr || typeof tr[func] !== 'function') { - return - } - tr[func](...args) - } - }) - }) - } - - // --- Message handler (replaces process.on('message')) --- - channel.on('to-child', async (message) => { - if (message.type === 'common') { - const msg = message.data - const { action, id, body } = msg - - let promise - - // ws mock: s() sends to parent, once() waits for parent response - const ws = { - s: (data) => { - channel.toParent({ type: 'common', data }) - }, - once: (callack, msgId) => { - const func = (arg) => { - if (msgId === arg.id) { - callack(arg) - channel.removeListener('to-child', func) - } - } - channel.on('to-child', func) - } - } - - if (action === 'create-terminal') { - promise = createTerm(body, ws) - } else if (action === 'test-terminal') { - promise = testTerm(body, ws) - } else if (action === 'resize-terminal') { - promise = resize(body) - } else if (action === 'toggle-terminal-log') { - promise = toggleTerminalLog(body) - } else if (action === 'toggle-terminal-log-timestamp') { - promise = toggleTerminalLogTimestamp(body) - } else if (action === 'set-terminal-log-path') { - promise = setTerminalLogPath(body) - } else if (action === 'start-terminal-log-file') { - promise = startTerminalLogFile(body) - } else if (action === 'run-cmd') { - promise = runCmd(body) - } else if (action === 'exec-cmd') { - promise = execCmd(body) - } - - const result = await promise - .then(r => { - return { - id, - data: r - } - }) - .catch(err => { - log.error('common message error', err) - return { - id, - error: { - message: err.message, - stack: err.stack - } - } - }) - - channel.toParent(result) - } - }) - - // --- Server lifecycle --- - let httpServer = null - let cleanupCalled = false - - function cleanup () { - if (cleanupCalled) return - cleanupCalled = true - if (noConnectionTimer) { - clearTimeout(noConnectionTimer) - } - if (httpServer) { - try { httpServer.close() } catch {} - } - channel.emit('exit', 0) - } - - // Start listening - httpServer = app.listen(wsPort, electermHost, () => { - log.info('session server', 'runs on', electermHost, wsPort) - channel.toParent({ serverInited: true }) - channel.emit('ready') - }) - - // Self-terminate if no WebSocket connects within 2 minutes - const noConnectionTimer = setTimeout(() => { - if (!firstWsConnected) { - log.warn('session-server: no WS connection within 2min timeout, terminating') - cleanup() - } - }, 120000) - if (noConnectionTimer.unref) noConnectionTimer.unref() - - const pid = _pidCounter++ - - return { - channel, - kill: cleanup, - pid, - port: wsPort, - server: httpServer - } -} - -module.exports = { createSessionServer } diff --git a/src/app/server/session-sftp.js b/src/app/server/session-sftp.js index a6bd5bb..0700b61 100644 --- a/src/app/server/session-sftp.js +++ b/src/app/server/session-sftp.js @@ -1,19 +1,18 @@ /** * terminal/sftp/serial class */ -const { +import { readRemoteFile, writeRemoteFile -} = require('./sftp-file') -const { commonExtends } = require('./session-common.js') -const { TerminalBase } = require('./session-base.js') -const { - getSizeCount, - getSizeCountWin -} = require('../common/get-folder-size-and-file-count.js') -const globalState = require('./global-state') - -class Sftp extends TerminalBase { +} from './sftp-file.js' +import { commonExtends } from './session-common.js' +import { TerminalBase } from './session-base.js' +import { getSizeCount, getSizeCountWin } from '../common/count-folder-data.js' +import globalState from './global-state.js' +import { SshFs } from 'ssh2-scp' +import iconv from 'iconv-lite' + +class SftpBase extends TerminalBase { connect (initOptions) { return this.remoteInitSftp(initOptions) } @@ -35,12 +34,11 @@ class Sftp extends TerminalBase { } initSshFsFallback = (conn) => { - const { SshFs } = require('ssh2-scp') const opts = {} const encode = this.initOptions?.encode || 'utf8' if (encode !== 'utf8') { opts.encoding = encode - opts.iconv = require('iconv-lite') + opts.iconv = iconv } const sshFs = new SshFs(conn, opts) this.applySshFsOverride(sshFs) @@ -631,4 +629,4 @@ class Sftp extends TerminalBase { // end } -exports.Sftp = commonExtends(Sftp) +export const Sftp = commonExtends(SftpBase) diff --git a/src/app/server/session-spice.js b/src/app/server/session-spice.js index 6bf2ee6..429c4dd 100644 --- a/src/app/server/session-spice.js +++ b/src/app/server/session-spice.js @@ -1,13 +1,14 @@ -const log = require('../common/log') -const { TerminalBase } = require('./session-base') -const globalState = require('./global-state') -const { handleConnection } = require('./spice-proxy') +import log from '../common/log.js' +import { TerminalBase } from './session-base.js' +import globalState from './global-state.js' +import { handleConnection } from './spice-proxy.js' +import net from 'net' +import proxySock from './socks.js' class TerminalSpice extends TerminalBase { - channelCounter = 0 - init = async () => { this.wsMap = new Map() + this.channelCounter = 0 globalState.setSession(this.pid, this) return Promise.resolve(this) } @@ -53,8 +54,6 @@ class TerminalSpice extends TerminalBase { } test = async () => { - const net = require('net') - const proxySock = require('./socks') const { host, port = 5900, @@ -111,19 +110,27 @@ class TerminalSpice extends TerminalBase { } } -exports.session = async function (initOptions, ws) { +export const terminalSpice = async function (initOptions, ws) { const term = new TerminalSpice(initOptions, ws) await term.init() return term } -exports.test = (options) => { +/** + * test spice connection + * @param {object} options + */ +export const testConnectionSpice = (options) => { return (new TerminalSpice(options, undefined, true)) .test() - .then(() => { + .then((res) => { + res.close() return true }) .catch(() => { return false }) } + +export const terminal = terminalSpice +export const testConnection = testConnectionSpice diff --git a/src/app/server/session-ssh.js b/src/app/server/session-ssh.js index d18a33a..1b9da2c 100644 --- a/src/app/server/session-ssh.js +++ b/src/app/server/session-ssh.js @@ -2,23 +2,27 @@ * terminal/sftp/serial class */ -const proxySock = require('./socks') -const _ = require('../lib/lodash.js') -const generate = require('../common/uid') -const { resolve: pathResolve } = require('path') -const net = require('net') -const { exec } = require('child_process') -const log = require('../common/log') -const { algDefault, algAlt } = require('./ssh2-alg') -const { createHostVerifier } = require('./ssh-known-hosts') -const { maybeProxyCommand } = require('./ssh-proxy-command') -const sshTunnelFuncs = require('./ssh-tunnel') -const deepCopy = require('json-deep-copy') -const { TerminalBase } = require('./session-base') -const { commonExtends } = require('./session-common') -const globalState = require('./global-state') -const iconv = require('iconv-lite') -const os = require('os') +import { Client } from '@electerm/ssh2' +import proxySock from './socks.js' +import _ from 'lodash' +import generate from '../common/uid.js' +import { resolve as pathResolve } from 'path' +import net from 'net' +import { exec } from 'child_process' +import log from '../common/log.js' +import fs from 'fs' +import { algDefault, algAlt } from './ssh2-alg.js' +import * as sshTunnelFuncs from './ssh-tunnel.js' +import deepCopy from 'json-deep-copy' +import { TerminalBase } from './session-base.js' +import { commonExtends } from './session-common.js' +import globalState from './global-state.js' +import { + sshKeysPath +} from '../common/runtime-constants.js' +import { createHostVerifier } from './ssh-known-hosts.js' +import iconv from 'iconv-lite' +import { maybeProxyCommand } from './ssh-proxy-command.js' // Encodings that are equivalent to UTF-8 (no conversion needed) const utf8Aliases = new Set(['utf-8', 'utf8', 'utf-8-strict']) @@ -387,7 +391,6 @@ class TerminalSshBase extends TerminalBase { sock, ...hopping } - const { Client } = require('@electerm/ssh2') this.nextConn = new Client() // If we have an agent and no explicit privateKey/password, try agent first // by skipping reading private keys from jump server @@ -497,7 +500,7 @@ class TerminalSshBase extends TerminalBase { return reject(err) } this.channel = channel - this.setNoDelay(true) + this.conn.setNoDelay(true) globalState.setSession(this.pid, this) resolve(this) } @@ -521,14 +524,10 @@ class TerminalSshBase extends TerminalBase { } getSSHKeys () { - // os.homedir() is overridden by bootstrap.js to return the - // sandbox DATA_PATH, so this resolves to /.ssh. - const keysDir = pathResolve(os.homedir(), '.ssh') try { - return require('fs') - .readdirSync(keysDir) + return fs.readdirSync(sshKeysPath) .filter(file => file.endsWith('.pub')) - .map(file => pathResolve(keysDir, file.replace('.pub', ''))) + .map(file => pathResolve(sshKeysPath, file.replace('.pub', ''))) } catch (e) { log.error(e) return [] @@ -540,7 +539,7 @@ class TerminalSshBase extends TerminalBase { if (this.sshKeys.length > 0) { const p = this.sshKeys.shift() this.privateKeyPath = p - connectOptions.privateKey = require('fs').readFileSync(p, 'utf8') + connectOptions.privateKey = fs.readFileSync(p, 'utf8') } else if (this.sshKeys.length === 0) { this.connectOptions.passphrase = this.initOptions.passphrase delete this.connectOptions.privateKey @@ -552,7 +551,7 @@ class TerminalSshBase extends TerminalBase { if (list.length) { const p = list.shift() this.privateKeyPath = p - connectOptions.privateKey = require('fs').readFileSync(p, 'utf8') + connectOptions.privateKey = fs.readFileSync(p, 'utf8') this.sshKeys = list } } @@ -741,10 +740,7 @@ class TerminalSshBase extends TerminalBase { getHostVerificationTarget (connectOptions = this.connectOptions) { if (connectOptions === this.hoppingOptions && this.initHoppingOptions) { - return { - host: this.initHoppingOptions.host, - port: this.initHoppingOptions.port - } + return { host: this.initHoppingOptions.host, port: this.initHoppingOptions.port } } return { host: connectOptions.host || this.initOptions.host, @@ -823,7 +819,6 @@ class TerminalSshBase extends TerminalBase { async sshConnect () { const { initOptions } = this - const { Client } = require('@electerm/ssh2') this.conn = new Client() this.connectOptions = this.connectOptions || this.buildConnectOptions() const { @@ -1067,7 +1062,7 @@ class TerminalSshBase extends TerminalBase { const TerminalSsh = commonExtends(TerminalSshBase) -exports.session = function (initOptions, ws) { +export const terminalSsh = function (initOptions, ws) { return (new TerminalSsh(initOptions, ws)).init() } @@ -1075,12 +1070,15 @@ exports.session = function (initOptions, ws) { * test ssh connection * @param {object} options */ -exports.test = (options, ws) => { +export const testConnectionSsh = (options, ws) => { return (new TerminalSsh(options, ws, true)) .init() .then(() => true) .catch((err) => { - log.error('test ssh error', err) + console.log('test ssh error', err) return false }) } + +export const terminal = terminalSsh +export const testConnection = testConnectionSsh diff --git a/src/app/server/session-telnet.js b/src/app/server/session-telnet.js index ea3bb58..7e3a08a 100644 --- a/src/app/server/session-telnet.js +++ b/src/app/server/session-telnet.js @@ -1,12 +1,12 @@ /** * terminal/sftp/serial class */ -const _ = require('../lib/lodash.js') -const log = require('../common/log') -const { Telnet } = require('./telnet') -const { TerminalBase } = require('./session-base') -const globalState = require('./global-state') -const iconv = require('iconv-lite') +import _ from 'lodash' +import log from '../common/log.js' +import { Telnet } from './telnet.js' +import { TerminalBase } from './session-base.js' +import globalState from './global-state.js' +import iconv from 'iconv-lite' // Encodings that are equivalent to UTF-8 (no conversion needed) const utf8Aliases = new Set(['utf-8', 'utf8', 'utf-8-strict']) @@ -30,7 +30,7 @@ function stringToRegExp (regexString) { } class TerminalTelnet extends TerminalBase { - init = async () => { + async init () { const connection = new Telnet() const { initOptions } = this @@ -76,10 +76,9 @@ class TerminalTelnet extends TerminalBase { return true } globalState.setSession(this.pid, this) - return Promise.resolve(this) } - resize = (cols, rows) => { + resize (cols, rows) { Object.assign(this.channel.options, { terminalWidth: cols, terminalHeight: rows @@ -87,24 +86,29 @@ class TerminalTelnet extends TerminalBase { this.channel.sendWindowSize() } - on = (event, cb) => { + on (event, cb) { this.port.on(event, cb) } - write = (data) => { + write (data) { try { const encode = this.initOptions?.encode if (encode && !utf8Aliases.has(encode.toLowerCase()) && typeof data === 'string') { try { const buf = iconv.encode(data, encode) this.port.write(buf) + if (this.sessionLogger) { + this.sessionLogger.write(data) + } return } catch (e) { log.warn('iconv encode failed, falling back to raw write:', e.message) } } this.port.write(data) - // this.writeLog(data) + if (this.sessionLogger) { + this.sessionLogger.write(data) + } } catch (e) { log.error(e) } @@ -119,7 +123,7 @@ class TerminalTelnet extends TerminalBase { } } -exports.session = async function (initOptions, ws) { +export const terminalTelnet = async function (initOptions, ws) { const term = new TerminalTelnet(initOptions, ws) await term.init() return term @@ -129,7 +133,7 @@ exports.session = async function (initOptions, ws) { * test ssh connection * @param {object} options */ -exports.test = (options) => { +export const testConnectionTelnet = (options) => { return (new TerminalTelnet(options, undefined, true)) .init() .then(() => true) @@ -137,3 +141,6 @@ exports.test = (options) => { return false }) } + +export const terminal = terminalTelnet +export const testConnection = testConnectionTelnet diff --git a/src/app/server/session-vnc.js b/src/app/server/session-vnc.js index 7798919..126a270 100644 --- a/src/app/server/session-vnc.js +++ b/src/app/server/session-vnc.js @@ -2,12 +2,12 @@ * terminal/sftp/serial class */ -const log = require('../common/log') -const { TerminalBase } = require('./session-base') -const net = require('net') -const proxySock = require('./socks') -const { createHopProxy } = require('./session-hop') -const globalState = require('./global-state') +import log from '../common/log.js' +import { TerminalBase } from './session-base.js' +import net from 'net' +import proxySock from './socks.js' +import { createHopProxy } from './session-hop.js' +import globalState from './global-state.js' class TerminalVnc extends TerminalBase { init = async () => { @@ -119,7 +119,7 @@ class TerminalVnc extends TerminalBase { } } -exports.session = async function (initOptions, ws) { +export const terminalVnc = async function (initOptions, ws) { const term = new TerminalVnc(initOptions, ws) await term.init() return term @@ -129,7 +129,7 @@ exports.session = async function (initOptions, ws) { * test ssh connection * @param {object} options */ -exports.test = (options) => { +export const testConnectionVnc = (options) => { const inst = new TerminalVnc(options, undefined, true) return inst.test() .then(() => { @@ -141,3 +141,6 @@ exports.test = (options) => { return false }) } + +export const terminal = terminalVnc +export const testConnection = testConnectionVnc diff --git a/src/app/server/session.js b/src/app/server/session.js index 5799913..130787b 100644 --- a/src/app/server/session.js +++ b/src/app/server/session.js @@ -1,24 +1,27 @@ -/** - * terminal/sftp/serial class - */ +// Static imports so bundlers (esbuild) can discover and include all session +// modules. Dynamic import() with a computed path (e.g. `./session-${type}.js`) +// is opaque to bundlers — the files are never included in the bundle and the +// import fails at runtime. A plain dispatch table is the standard fix. +import * as sessionSsh from './session-ssh.js' +import * as sessionTelnet from './session-telnet.js' +import * as sessionSerial from './session-serial.js' +import * as sessionLocal from './session-local.js' +import * as sessionRdp from './session-rdp.js' +import * as sessionVnc from './session-vnc.js' +import * as sessionSpice from './session-spice.js' -/** - * Dynamically load a module based on terminal type - * @param {string} type - Terminal type - * @returns {Object} The loaded module - */ -function loadModule (type) { - return require(`./session-${type}`) +const sessionModules = { + ssh: sessionSsh, + telnet: sessionTelnet, + serial: sessionSerial, + local: sessionLocal, + rdp: sessionRdp, + vnc: sessionVnc, + spice: sessionSpice } -/** - * Create a terminal session - * @param {object} initOptions - Terminal initialization options - * @param {object} ws - WebSocket connection - * @returns {Promise} Terminal session - */ -exports.startSession = async function (initOptions, ws, func = 'session') { - const type = initOptions.termType || initOptions.type || 'ssh' +function getType (initOptions) { + const type = initOptions.termType || initOptions.type const tail = [ 'telnet', 'serial', @@ -29,6 +32,17 @@ exports.startSession = async function (initOptions, ws, func = 'session') { ].includes(type) ? type : 'ssh' - const module = loadModule(tail) - return module[func](initOptions, ws) + return tail +} + +export const terminal = async function (initOptions, ws) { + const type = getType(initOptions) + const { terminal } = sessionModules[type] + return terminal(initOptions, ws) +} + +export const testConnection = async (initOptions, ws) => { + const type = getType(initOptions) + const { testConnection } = sessionModules[type] + return testConnection(initOptions, ws) } diff --git a/src/app/server/sftp-file.js b/src/app/server/sftp-file.js index 52b41d9..f56d8a7 100644 --- a/src/app/server/sftp-file.js +++ b/src/app/server/sftp-file.js @@ -2,7 +2,7 @@ * sftp read/write file */ -const { Readable, Writable } = require('stream') +import { Readable, Writable } from 'stream' function createReadStreamFromString (str) { const s = new Readable() @@ -24,7 +24,7 @@ class FakeWrite extends Writable { } } -function writeRemoteFile (sftp, path, str, mode) { +export function writeRemoteFile (sftp, path, str, mode) { return new Promise((resolve, reject) => { const writeStream = sftp.createWriteStream(path, { highWaterMark: 64 * 1024 * 4 * 4, @@ -40,7 +40,7 @@ function writeRemoteFile (sftp, path, str, mode) { }) } -function readRemoteFile (sftp, path) { +export function readRemoteFile (sftp, path) { return new Promise((resolve, reject) => { let final = Buffer.alloc(0) const writeStream = new FakeWrite({ @@ -61,8 +61,3 @@ function readRemoteFile (sftp, path) { }).pipe(writeStream) }) } - -module.exports = { - readRemoteFile, - writeRemoteFile -} diff --git a/src/app/server/socks.js b/src/app/server/socks.js index fc02305..997140b 100644 --- a/src/app/server/socks.js +++ b/src/app/server/socks.js @@ -1,8 +1,8 @@ /** * socks proxy wrapper */ - -const { request } = require('http') +import { SocksClient } from 'socks' +import { request } from 'http' function isValidIP (input) { // Check IPv4 format @@ -29,7 +29,7 @@ function parseUrl (str) { } } -module.exports = (initOptions) => { +export default (initOptions) => { const { readyTimeout, host, @@ -102,6 +102,5 @@ module.exports = (initOptions) => { } // use socks proxy - const { SocksClient } = require('socks') return SocksClient.createConnection(options) } diff --git a/src/app/server/spice-proxy.js b/src/app/server/spice-proxy.js index 9c3c4a4..718a6ac 100644 --- a/src/app/server/spice-proxy.js +++ b/src/app/server/spice-proxy.js @@ -1,6 +1,6 @@ -const net = require('net') -const log = require('../common/log') -const proxySock = require('./socks') +import net from 'net' +import log from '../common/log.js' +import proxySock from './socks.js' const LOG_PREFIX = '[SPICE-PROXY]' @@ -211,7 +211,7 @@ function setupRelay (ws, tcpSocket, options = {}) { }) } -module.exports = { +export { handleConnection, createTcpConnection, setupRelay diff --git a/src/app/server/ssh-known-hosts.js b/src/app/server/ssh-known-hosts.js index 0bc14d2..ce362a3 100644 --- a/src/app/server/ssh-known-hosts.js +++ b/src/app/server/ssh-known-hosts.js @@ -1,8 +1,9 @@ -const crypto = require('crypto') -const fs = require('fs') -const os = require('os') -const { dirname, join } = require('path') -const { parseKey } = require('@electerm/ssh2/lib/protocol/keyParser.js') +import crypto from 'crypto' +import fs from 'fs' +import os from 'os' +import { dirname, join } from 'path' +import keyParserModule from '@electerm/ssh2/lib/protocol/keyParser.js' +const { parseKey } = keyParserModule function normalizeHost (host = '') { if (typeof host !== 'string') { @@ -15,9 +16,6 @@ function normalizeHost (host = '') { } function getKnownHostsPath () { - // os.homedir() is overridden by bootstrap.js to return the app's - // sandbox data directory (DATA_PATH), so this resolves to - // /.ssh/known_hosts. return join(os.homedir(), '.ssh', 'known_hosts') } @@ -435,7 +433,7 @@ function createHostVerifier (options) { } } -module.exports = { +export { appendKnownHost, buildHostMismatchError, buildHostMismatchPrompt, diff --git a/src/app/server/ssh-proxy-command.js b/src/app/server/ssh-proxy-command.js index d33bce2..df6b2b3 100644 --- a/src/app/server/ssh-proxy-command.js +++ b/src/app/server/ssh-proxy-command.js @@ -15,9 +15,9 @@ * loopback socketpair instead of patching a fake socket. */ -const { spawn } = require('child_process') -const net = require('net') -const log = require('../common/log') +import { spawn } from 'child_process' +import net from 'net' +import log from '../common/log.js' // resolved lazily so tests (and users) can override via env at any time function getNetbirdBin () { @@ -124,7 +124,7 @@ function bridgeChildStdio (child) { try { child.kill() } catch { - + // ignore } } child.stdout.once('end', cleanup) @@ -135,7 +135,7 @@ function bridgeChildStdio (child) { try { child.kill() } catch { - + // ignore } }) }) @@ -199,7 +199,7 @@ async function runProxyCommand (command, args, { onMessage } = {}) { try { child.stdin?.end() } catch { - + // ignore } child.kill() bridge.close() @@ -214,7 +214,7 @@ async function runProxyCommand (command, args, { onMessage } = {}) { try { child.stdin?.end() } catch { - + // ignore } child.kill() bridge.close() @@ -274,8 +274,10 @@ function clearDetectCache () { detectCache.clear() } -exports.maybeProxyCommand = maybeProxyCommand -exports.expandProxyCommand = expandProxyCommand -exports.detectNetbird = detectNetbird -exports.isNetbirdLikeHost = isNetbirdLikeHost -exports.clearDetectCache = clearDetectCache +export { + maybeProxyCommand, + expandProxyCommand, + detectNetbird, + isNetbirdLikeHost, + clearDetectCache +} diff --git a/src/app/server/ssh-tunnel.js b/src/app/server/ssh-tunnel.js index 349ace3..d2978c2 100644 --- a/src/app/server/ssh-tunnel.js +++ b/src/app/server/ssh-tunnel.js @@ -1,6 +1,8 @@ -const log = require('../common/log') +import log from '../common/log.js' +import * as socks from 'socksv5-server' +import net from 'net' -function forwardRemoteToLocal ({ +export function forwardRemoteToLocal ({ conn, sshTunnelRemotePort, sshTunnelLocalPort, @@ -30,7 +32,7 @@ function forwardRemoteToLocal ({ // Connect the local machine source stream to the local port // Create a NEW server connection for each forwarded connection - const server = require('net').connect(sshTunnelLocalPort, sshTunnelLocalHost) + const server = net.connect(sshTunnelLocalPort, sshTunnelLocalHost) // CRITICAL: Add error handling IMMEDIATELY before any async operations // This prevents unhandled errors from crashing the SSH session @@ -75,7 +77,7 @@ function forwardRemoteToLocal ({ }) } -function forwardLocalToRemote ({ +export function forwardLocalToRemote ({ conn, sshTunnelRemotePort, sshTunnelLocalPort, @@ -84,7 +86,7 @@ function forwardLocalToRemote ({ }) { return new Promise((resolve, reject) => { const activeSockets = new Set() - const localServer = require('net').createServer((socket) => { + const localServer = net.createServer((socket) => { // ⬇️ 2. Add new sockets to the set and remove them when they close activeSockets.add(socket) socket.on('close', () => { @@ -139,12 +141,11 @@ function forwardLocalToRemote ({ }) } -function dynamicForward ({ +export function dynamicForward ({ conn, sshTunnelLocalPort, sshTunnelLocalHost = '127.0.0.1' }) { - const socks = require('socksv5-server') return new Promise((resolve, reject) => { const dproxyServer = socks.createServer((info, accept, deny) => { conn.forwardOut( @@ -201,7 +202,3 @@ function dynamicForward ({ }) }) } - -exports.dynamicForward = dynamicForward -exports.forwardLocalToRemote = forwardLocalToRemote -exports.forwardRemoteToLocal = forwardRemoteToLocal diff --git a/src/app/server/ssh2-alg.js b/src/app/server/ssh2-alg.js index 8737571..8c2755b 100644 --- a/src/app/server/ssh2-alg.js +++ b/src/app/server/ssh2-alg.js @@ -1,13 +1,15 @@ /** * all supported ssh2 algorithms config */ -const nodeCrypto = require('crypto') -const browserDH = require('diffie-hellman/browser') + +import nodeCrypto from 'crypto' +import browserDH from 'diffie-hellman/browser.js' nodeCrypto.createDiffieHellmanGroup = browserDH.createDiffieHellmanGroup nodeCrypto.createDiffieHellman = browserDH.createDiffieHellman +nodeCrypto.ddd = 1 -exports.algDefault = () => ({ +export const algDefault = () => ({ kex: [ 'curve25519-sha256', // (node v13.9.0 or newer) 'curve25519-sha256@libssh.org', // (node v13.9.0 or newer) @@ -45,7 +47,7 @@ exports.algDefault = () => ({ ] }) -exports.algAlt = () => ({ +export const algAlt = () => ({ ...exports.algDefault(), cipher: [ // 'chacha20-poly1305@openssh.com', diff --git a/src/app/server/sync.js b/src/app/server/sync.js index 1829806..2289a99 100644 --- a/src/app/server/sync.js +++ b/src/app/server/sync.js @@ -2,13 +2,13 @@ * handle sync with github/gitee */ -const log = require('../common/log') -const rp = require('axios') -const { createProxyAgent } = require('../lib/proxy-agent') -const { +import { electermSync -} = require('electerm-sync') -const doWebdavSync = require('./webdav-sync') +} from 'electerm-sync' +import log from '../common/log.js' +import rp from 'axios' +import { createProxyAgent } from '../lib/proxy-agent.js' +import { doWebdavSync } from './webdav-sync.js' rp.defaults.proxy = false @@ -17,7 +17,6 @@ async function doSync (type, func, args, token, proxy) { if (type === 'webdav') { return doWebdavSync(func, args, token, proxy) } - const agent = createProxyAgent(proxy) const conf = agent ? { @@ -44,13 +43,13 @@ async function doSync (type, func, args, token, proxy) { }) } -async function wsSyncHandler (ws, msg) { +export default async function wsSyncHandler (ws, msg) { const { id, type, args, func, token, proxy } = msg const res = await doSync(type, func, args, token, proxy) if (res.error) { ws.s({ error: { - message: 'Sync data error: ' + res.error.message + message: 'sync error: ' + res.error.message }, id }) @@ -61,5 +60,3 @@ async function wsSyncHandler (ws, msg) { }) } } - -module.exports = wsSyncHandler diff --git a/src/app/server/telnet.js b/src/app/server/telnet.js index aae84ec..0bc3372 100644 --- a/src/app/server/telnet.js +++ b/src/app/server/telnet.js @@ -1,9 +1,9 @@ // used code from https://github.com/Eugeny/tabby/blob/master/tabby-telnet/src/session.ts and from https://github.com/mkozjak/node-telnet-client -const { EventEmitter } = require('events') -const { Socket } = require('net') -const { Duplex } = require('stream') -const proxySock = require('./socks') +import { EventEmitter } from 'events' +import { Socket } from 'net' +import { Duplex } from 'stream' +import proxySock from './socks.js' const TelnetCommands = { SUBOPTION_END: 240, @@ -46,7 +46,7 @@ class Stream extends Duplex { _read () {} } -class Telnet extends EventEmitter { +export class Telnet extends EventEmitter { constructor (options = {}) { super() this.options = { @@ -365,5 +365,3 @@ class Telnet extends EventEmitter { } } } - -exports.Telnet = Telnet diff --git a/src/app/server/terminal-api.js b/src/app/server/terminal-api.js index ece0474..56a5982 100644 --- a/src/app/server/terminal-api.js +++ b/src/app/server/terminal-api.js @@ -2,14 +2,16 @@ * run cmd with terminal */ -const { testConnection, terminal, terminals } = require('./session-process') +import { terminals } from './remote-common.js' +import { terminal, testConnection } from './session.js' +import { isDev } from '../common/runtime-constants.js' -async function runCmd (ws, msg) { +export async function runCmd (ws, msg) { const { id, pid, cmd } = msg const term = terminals(pid) let txt = '' if (term) { - txt = await term.runCmd(cmd, id) + txt = await term.runCmd(cmd) } ws.s({ id, @@ -17,7 +19,12 @@ async function runCmd (ws, msg) { }) } -async function execCmd (ws, msg) { +// Structured command execution: unlike runCmd, returns +// { stdout, stderr, exitCode, timedOut } from a dedicated exec channel. +// In electerm-web sessions live in-process (see remote-common.js), so the +// session's execCommand(cmd, options) is called directly instead of going +// through a child-process proxy. +export async function execCmd (ws, msg) { const { id, pid, cmd, timeoutMs } = msg const term = terminals(pid) if (!term || typeof term.execCommand !== 'function') { @@ -30,7 +37,7 @@ async function execCmd (ws, msg) { return } try { - const result = await term.execCommand(cmd, timeoutMs, id) + const result = await term.execCommand(cmd, { timeoutMs }) ws.s({ id, data: result @@ -46,11 +53,11 @@ async function execCmd (ws, msg) { } } -function resize (ws, msg) { +export function resize (ws, msg) { const { id, pid, cols, rows } = msg const term = terminals(pid) if (term) { - term.resize(cols, rows, id) + term.resize(cols, rows) } ws.s({ id, @@ -58,11 +65,11 @@ function resize (ws, msg) { }) } -function toggleTerminalLog (ws, msg) { +export function toggleTerminalLog (ws, msg) { const { id, pid } = msg const term = terminals(pid) if (term) { - term.toggleTerminalLog(id) + term.toggleTerminalLog() } ws.s({ id, @@ -70,11 +77,11 @@ function toggleTerminalLog (ws, msg) { }) } -function toggleTerminalLogTimestamp (ws, msg) { +export function toggleTerminalLogTimestamp (ws, msg) { const { id, pid } = msg const term = terminals(pid) if (term) { - term.toggleTerminalLogTimestamp(id) + term.toggleTerminalLogTimestamp() } ws.s({ id, @@ -82,10 +89,42 @@ function toggleTerminalLogTimestamp (ws, msg) { }) } -function createTerm (ws, msg) { +export function setTerminalLogPath (ws, msg) { + const { id, pid, logPath } = msg + const term = terminals(pid) + if (term) { + term.setTerminalLogPath(logPath) + } + ws.s({ + id, + data: 'ok' + }) +} + +export function startTerminalLogFile (ws, msg) { + const { id, pid, logFilePath, addTimeStampToTermLog } = msg + const term = terminals(pid) + if (term) { + term.startTerminalLogFile(logFilePath, addTimeStampToTermLog) + } + ws.s({ + id, + data: 'ok' + }) +} + +export function createTerm (ws, msg) { const { id, body } = msg - terminal(body, ws, id) - .then(data => { + terminal(body, ws) + .then(r => { + const data = isDev + ? { + pid: r.pid, + port: process.env.PORT + } + : { + pid: r.pid + } ws.s({ id, data @@ -102,9 +141,9 @@ function createTerm (ws, msg) { }) } -function testTerm (ws, msg) { +export function testTerm (ws, msg) { const { id, body } = msg - testConnection(body, ws, id) + testConnection(body, ws) .then(data => { if (data) { ws.s({ @@ -131,37 +170,3 @@ function testTerm (ws, msg) { }) }) } - -function setTerminalLogPath (ws, msg) { - const { id, pid, logPath } = msg - const term = terminals(pid) - if (term) { - term.setTerminalLogPath(id, logPath) - } - ws.s({ - id, - data: 'ok' - }) -} - -function startTerminalLogFile (ws, msg) { - const { id, pid, logFilePath, addTimeStampToTermLog } = msg - const term = terminals(pid) - if (term) { - term.startTerminalLogFile(id, logFilePath, addTimeStampToTermLog) - } - ws.s({ - id, - data: 'ok' - }) -} - -exports.createTerm = createTerm -exports.testTerm = testTerm -exports.resize = resize -exports.runCmd = runCmd -exports.execCmd = execCmd -exports.toggleTerminalLog = toggleTerminalLog -exports.toggleTerminalLogTimestamp = toggleTerminalLogTimestamp -exports.setTerminalLogPath = setTerminalLogPath -exports.startTerminalLogFile = startTerminalLogFile diff --git a/src/app/server/transfer.js b/src/app/server/transfer.js index e331377..463b599 100644 --- a/src/app/server/transfer.js +++ b/src/app/server/transfer.js @@ -2,14 +2,15 @@ * transfer class */ -const fs = require('original-fs') -const tar = require('tar') -const _ = require('../lib/lodash.js') -const log = require('../common/log') - -const { FolderTransfer } = require('ssh2-scp/folder-transfer') - -class Transfer { +import fs from 'fs' +import _ from 'lodash' +import log from '../common/log.js' +import * as tar from 'tar' +import { Transfer as Ssh2ScpTransfer } from 'ssh2-scp/transfer' +import { FolderTransfer } from 'ssh2-scp/folder-transfer' +import iconv from 'iconv-lite' + +export class Transfer { constructor ({ remotePath, localPath, @@ -32,20 +33,20 @@ class Transfer { this.sftpId = sftpId this.srcPath = isd ? remotePath : localPath this.dstPath = !isd ? remotePath : localPath - this.conn = conn this.pausing = false this.hadError = false this.isUpload = isd - this.isDirectory = isDirectory this.options = options + this.conn = conn + this.isDirectory = isDirectory this.concurrency = options.concurrency || 64 this.chunkSize = options.chunkSize || 32768 this.mode = options.mode this.encode = encode - this.onData = _.throttle((data) => { + this.onData = _.throttle((count) => { ws.s({ id: 'transfer:data:' + id, - data + data: count }) }, 3000) this.timers = {} @@ -121,7 +122,7 @@ class Transfer { } } if (this.encode !== 'utf8') { - folderOpts.iconv = require('iconv-lite') + folderOpts.iconv = iconv folderOpts.encoding = this.encode } this.scpTransfer = new FolderTransfer(this.conn, tar, folderOpts) @@ -143,7 +144,7 @@ class Transfer { const sshFs = type === 'download' ? this.src : this.dst const remotePath = type === 'download' ? this.srcPath : this.dstPath const localPath = type === 'download' ? this.dstPath : this.srcPath - const { Transfer: Ssh2ScpTransfer } = require('ssh2-scp/transfer') + this.scpTransfer = new Ssh2ScpTransfer(sshFs, { type, remotePath, @@ -467,11 +468,8 @@ class Transfer { // end } -module.exports = { - Transfer, - transferKeys: [ - 'pause', - 'resume', - 'destroy' - ] -} +export const transferKeys = [ + 'pause', + 'resume', + 'destroy' +] diff --git a/src/app/server/trzsz.js b/src/app/server/trzsz.js index b597e52..c597e81 100644 --- a/src/app/server/trzsz.js +++ b/src/app/server/trzsz.js @@ -1,12 +1,12 @@ /** * Optimized Trzsz protocol handler for server-side terminal sessions */ -const fs = require('fs') -const { open } = require('fs/promises') -const path = require('path') -const log = require('../common/log') -const sanitizeFilename = require('../common/sanitize-filename') -const { TrzszTransfer } = require('trzsz2') +import fs from 'fs' +import { open } from 'fs/promises' +import path from 'path' +import log from '../common/log.js' +import sanitizeFilename from '../common/sanitize-filename.js' +import { TrzszTransfer } from 'trzsz2' const TRZSZ_STATE = { IDLE: 'idle', @@ -263,24 +263,13 @@ class TrzszSession { } const detected = this.detectTrzszStart(data) if (detected) { - // Extract any data AFTER the magic key line to feed to the buffer. - // The magic key itself is terminal output, NOT a protocol message — - // feeding it to the buffer pollutes recvLine's junk handling and can - // cause deadlocks or parse errors depending on \r\n patterns. - // This aligns with how TrzszFilter (reference impl) works: it never - // feeds the magic-key-containing output to addReceivedData. - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - const newlineIdx = buf.indexOf(10, detected.offset) // find \n ending magic key line - const trailing = (newlineIdx >= 0 && newlineIdx + 1 < buf.length) - ? buf.slice(newlineIdx + 1) - : null if (detected.type === 'receive') { this.startReceiver() - if (trailing) this.transfer.addReceivedData(trailing) + this.transfer.addReceivedData(data) } else if (detected.type === 'send') { this.createTransfer() this.state = TRZSZ_STATE.SENDING - if (trailing) this.transfer.addReceivedData(trailing) + this.transfer.addReceivedData(data) this.startUploadProcess() } return true @@ -428,17 +417,19 @@ class TrzszSession { return } - // In binary mode, sendData passes Uint8Array directly. - // Convert to Buffer for reliable channel.write() compatibility. - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - if ( - buf.length < 200 && - (buf.indexOf(TRZSZ_SAVED_FILE_BUFFER) >= 0 || - buf.indexOf(TRZSZ_SAVED_DIR_BUFFER) >= 0) - ) { + if (Buffer.isBuffer(data)) { + if ( + data.length < 200 && + (data.indexOf(TRZSZ_SAVED_FILE_BUFFER) >= 0 || + data.indexOf(TRZSZ_SAVED_DIR_BUFFER) >= 0) + ) { + return + } + this.writeToTerminal(data) return } - this.writeToTerminal(buf) + + this.writeToTerminal(data) }, false) return this.transfer @@ -736,4 +727,4 @@ class TrzszManager { } } const trzszManager = new TrzszManager() -module.exports = { trzszManager } +export { trzszManager } diff --git a/src/app/server/webdav-sync.js b/src/app/server/webdav-sync.js index 6139fec..f50d595 100644 --- a/src/app/server/webdav-sync.js +++ b/src/app/server/webdav-sync.js @@ -2,9 +2,10 @@ * handle sync with WebDAV server */ -const log = require('../common/log') -const rp = require('axios') -const { createProxyAgent } = require('../lib/proxy-agent') +import log from '../common/log.js' +import rp from 'axios' +import https from 'https' +import { createProxyAgent } from '../lib/proxy-agent.js' rp.defaults.proxy = false @@ -12,26 +13,19 @@ rp.defaults.proxy = false * Create an axios client for WebDAV operations */ function createClient (serverUrl, username, password, proxy, skipVerify = false) { - const https = require('https') - const proxyAgent = createProxyAgent(proxy) let conf if (proxyAgent) { if (skipVerify) { - // apply skipVerify through the proxy - const Cls = proxy.startsWith('http') - ? require('https-proxy-agent').HttpsProxyAgent - : require('socks-proxy-agent').SocksProxyAgent - const agent = new Cls(proxy, { keepAlive: true, rejectUnauthorized: false }) + // Apply skipVerify through the proxy tunnel as well. + const agent = createProxyAgent(proxy, { rejectUnauthorized: false }) conf = { httpAgent: agent, httpsAgent: agent } } else { conf = { httpAgent: proxyAgent, httpsAgent: proxyAgent } } } else if (skipVerify) { - conf = { - httpAgent: new https.Agent({ rejectUnauthorized: false }), - httpsAgent: new https.Agent({ rejectUnauthorized: false }) - } + const agent = new https.Agent({ rejectUnauthorized: false }) + conf = { httpAgent: agent, httpsAgent: agent } } else { conf = { proxy: false } } @@ -229,7 +223,7 @@ async function download (serverUrl, username, password, proxy, skipVerify) { async function doWebdavSync (func, args, token, proxy) { log.info(`[WebDAV] doWebdavSync: func=${func}`) - // token format: serverUrl####username####password + // token format: serverUrl####username####password####skipVerify const parts = token ? token.split('####') : [] const serverUrl = parts[0] || '' const username = parts[1] || '' @@ -265,4 +259,4 @@ async function doWebdavSync (func, args, token, proxy) { } } -module.exports = doWebdavSync +export { doWebdavSync } diff --git a/src/app/server/ws-dec.js b/src/app/server/ws-dec.js deleted file mode 100644 index 4392641..0000000 --- a/src/app/server/ws-dec.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * add ws.s function - * @param {*} ws - */ - -const log = require('../common/log') - -const wsDec = (ws) => { - ws.s = msg => { - try { - ws.send(JSON.stringify(msg)) - } catch (e) { - log.error('ws send error') - log.error(e) - } - } - ws.on('error', log.error) - ws.once = (callack, id) => { - const func = (evt) => { - const arg = JSON.parse(evt.data) - if (id === arg.id) { - callack(arg) - ws.removeEventListener('message', func) - } - } - ws.addEventListener('message', func) - } - ws._socket.setKeepAlive(true, 30 * 1000) -} - -module.exports = wsDec diff --git a/src/app/server/xmodem.js b/src/app/server/xmodem.js index 2efd7f9..61c6807 100644 --- a/src/app/server/xmodem.js +++ b/src/app/server/xmodem.js @@ -3,11 +3,11 @@ * Supports XMODEM-CRC (128-byte) and XMODEM-1K (1024-byte) modes */ -const fs = require('fs') -const path = require('path') -const log = require('../common/log') -const generate = require('../common/uid') -const sanitizeFilename = require('../common/sanitize-filename') +import fs from 'fs' +import path from 'path' +import log from '../common/log.js' +import generate from '../common/uid.js' +import sanitizeFilename from '../common/sanitize-filename.js' // XMODEM control characters const SOH = 0x01 // Start of 128-byte block @@ -670,7 +670,7 @@ class XmodemSession { this.endSession() return } - // Re-read and resend the same block (keep sendBlock unchanged – same block#) + // Re-read and resend the same block (keep sendBlock unchanged – same block# must be retransmitted) this.sentBytes -= (this.use1K ? PACKET_SIZE_1K : PACKET_SIZE_128) if (this.sentBytes < 0) this.sentBytes = 0 this.sendNextPacket() @@ -932,7 +932,7 @@ class XmodemManager { const xmodemManager = new XmodemManager() -module.exports = { +export { XmodemSession, XmodemManager, xmodemManager, diff --git a/src/app/server/zmodem.js b/src/app/server/zmodem.js index a302152..f2d659a 100644 --- a/src/app/server/zmodem.js +++ b/src/app/server/zmodem.js @@ -22,14 +22,14 @@ * failure, which is what restores normal terminal display. */ -const fs = require('fs') -const path = require('path') -const log = require('../common/log') -const generate = require('../common/uid') -const sanitizeFilename = require('../common/sanitize-filename') +import fs from 'fs' +import path from 'path' +import log from '../common/log.js' +import generate from '../common/uid.js' +import sanitizeFilename from '../common/sanitize-filename.js' // Import zmodem2 (pure JS, no WASM) -const { Sender, Receiver, SenderEvent, ReceiverEvent } = require('zmodem2') +import { Sender, Receiver, SenderEvent, ReceiverEvent } from 'zmodem2' // Zmodem state constants const ZMODEM_STATE = { @@ -1248,7 +1248,7 @@ class ZmodemManager { // Export singleton manager const zmodemManager = new ZmodemManager() -module.exports = { +export { ZmodemSession, ZmodemManager, zmodemManager, diff --git a/src/app/upgrade/db-defaults.js b/src/app/upgrade/db-defaults.js index 29cd822..a18aef9 100644 --- a/src/app/upgrade/db-defaults.js +++ b/src/app/upgrade/db-defaults.js @@ -91,7 +91,7 @@ const defaultThemeTerminal = { brightWhite: '#E6E6E6' } -module.exports = exports.default = [ +export default [ { db: 'terminalThemes', data: [ diff --git a/src/app/upgrade/index.js b/src/app/upgrade/index.js index ae11666..df1d98b 100644 --- a/src/app/upgrade/index.js +++ b/src/app/upgrade/index.js @@ -4,16 +4,21 @@ * run every upgrade script one by one */ -const { packInfo } = require('../common/app-props') +import { packInfo } from '../common/runtime-constants.js' +import { resolve, dirname } from 'path' +import fs from 'fs' +import log from '../common/log.js' +import compare from '../common/version-compare.js' +import { dbAction } from '../lib/db.js' +import _ from 'lodash' +import initData from './init-nedb.js' +import { updateDBVersion } from './version-upgrade.js' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + const { version: packVersion } = packInfo -const { resolve } = require('path') -const fs = require('fs') -const log = require('../common/log') -const compare = require('../common/version-compare') -const { dbAction } = require('../lib/db') -const _ = require('../lib/lodash.js') -const initData = require('./init-db') -const { updateDBVersion } = require('./version-upgrade') const emptyVersion = '0.0.0' const versionQuery = { _id: 'version' @@ -44,13 +49,14 @@ async function getUpgradeVersionList () { return compare(a, b) }) } + async function versionShouldUpgrade () { const dbVersion = await getDBVersion() log.info('database version:', dbVersion) return compare(dbVersion, packVersion) < 0 } -async function shouldUpgrade () { +export async function checkDbUpgrade () { const shouldUpgradeVersion = await versionShouldUpgrade() if (!shouldUpgradeVersion) { return false @@ -73,16 +79,13 @@ async function shouldUpgrade () { } } -async function doUpgrade () { +export async function doUpgrade () { const list = await getUpgradeVersionList() log.info('Upgrading...') for (const v of list) { const p = resolve(__dirname, v) - const run = require(p) + const run = import(p).then(d => d.default) await run() } log.info('Upgrade end') } - -exports.checkDbUpgrade = shouldUpgrade -exports.doUpgrade = doUpgrade diff --git a/src/app/upgrade/init-db.js b/src/app/upgrade/init-nedb.js similarity index 60% rename from src/app/upgrade/init-db.js rename to src/app/upgrade/init-nedb.js index 83a10ee..d67cf63 100644 --- a/src/app/upgrade/init-db.js +++ b/src/app/upgrade/init-nedb.js @@ -3,11 +3,11 @@ * just need init db */ -const { dbAction } = require('../lib/db') -const log = require('../common/log') -const defaults = require('./db-defaults') +import { dbAction } from '../lib/db.js' +import log from '../common/log.js' +import defaults from './db-defaults.js' -async function initData () { +export default async function initData () { log.info('start: init db') for (const conf of defaults) { const { @@ -17,5 +17,3 @@ async function initData () { } log.info('end: init db') } - -module.exports = initData diff --git a/src/app/upgrade/version-upgrade.js b/src/app/upgrade/version-upgrade.js index 3ea7bfb..3af905f 100644 --- a/src/app/upgrade/version-upgrade.js +++ b/src/app/upgrade/version-upgrade.js @@ -8,10 +8,10 @@ * run every upgrade script one by one */ -const log = require('../common/log') -const { dbAction } = require('../lib/db') +import log from '../common/log.js' +import { dbAction } from '../lib/db.js' -async function updateDBVersion (toVersion) { +export async function updateDBVersion (toVersion) { const versionQuery = { _id: 'version' } @@ -35,5 +35,3 @@ async function updateDBVersion (toVersion) { log.error('insert dbUpgradeLog error', toVersion) }) } - -exports.updateDBVersion = updateDBVersion diff --git a/src/client/views/index.pug b/src/app/views/index.pug similarity index 89% rename from src/client/views/index.pug rename to src/app/views/index.pug index c677059..957104f 100644 --- a/src/client/views/index.pug +++ b/src/app/views/index.pug @@ -16,6 +16,7 @@ html top: 0; width: 100%; height: 100%; + background: #141314; display: flex; flex-direction: column; justify-content: center; @@ -31,7 +32,6 @@ html transition: all 1s ease-in-out; z-index: 5; } - - if (!isDev) link(rel='stylesheet', href='css/style-' + version + '.css') style(id='theme-css'). @@ -46,7 +46,10 @@ html img.iblock.logo-filter(src='images/electerm.png', alt='', height=80) script. window.et = !{JSON.stringify(_global)} - - var url = '/src/client/entry/basic.js' + - if (tokenElecterm) + script. + window.localStorage.setItem('tokenElecterm', window.et.tokenElecterm) + - var url = '/src/client/entry-web/basic.js' - if (isDev) //- script(src='/external/react.development.js?' + version) //- script(src='/external/react-dom.development.js?' + version) @@ -57,12 +60,10 @@ html window.$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true script(src='/@vite/client', type='module') - script(src=url1, type='module') script(src=url, type='module') - else //- script(src='/external/react.production.min.js?' + version) //- script(src='/external/react-dom.production.min.js?' + version) - - var url = src='/js/basic-' + version + '.js' - script(src=url1, type='module') + - var url = src=cdn + '/js/basic-' + version + '.js' script(src=url, type='module') diff --git a/src/app/widgets/load-widget.js b/src/app/widgets/load-widget.js index 20c40ec..8e22952 100644 --- a/src/app/widgets/load-widget.js +++ b/src/app/widgets/load-widget.js @@ -1,11 +1,12 @@ // load-widget.js -const fs = require('fs') -const path = require('path') -// const log = require('../common/log') +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +// import log from '../common/log.js' -// Store running widget instances -const runningInstances = new Map() +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const widgetIdPattern = /^[a-z0-9-]+$/ function resolveWidgetPath (widgetId, widgetDirectory = __dirname) { @@ -23,12 +24,16 @@ function resolveWidgetPath (widgetId, widgetDirectory = __dirname) { return widgetPath } -function listWidgetsFromFolder (widgetDirectory = __dirname) { +// Store running widget instances +const runningInstances = new Map() + +async function listWidgetsFromFolder (widgetDirectory = __dirname) { const widgetFiles = fs.readdirSync(widgetDirectory).filter(file => file.startsWith('widget-') && file.endsWith('.js')) const res = [] for (const file of widgetFiles) { try { - const widgetModule = require(path.join(widgetDirectory, file)) + const widgetPath = path.join(widgetDirectory, file) + const widgetModule = await import(`file://${widgetPath}`) res.push({ id: file.slice(7, -3), info: widgetModule.widgetInfo @@ -41,8 +46,8 @@ function listWidgetsFromFolder (widgetDirectory = __dirname) { return res } -function listWidgets () { - const widgets1 = listWidgetsFromFolder() +async function listWidgets () { + const widgets1 = await listWidgetsFromFolder() return widgets1 // if (process.versions.electron === undefined) { // return widgets1 @@ -79,8 +84,9 @@ function hasRunningInstance (widgetId) { return false } -function runWidget (widgetId, config) { - const widget = require(resolveWidgetPath(widgetId)) +async function runWidget (widgetId, config) { + const widgetPath = resolveWidgetPath(widgetId) + const widget = await import(`file://${widgetPath}`) const { type, singleInstance } = widget.widgetInfo if (type !== 'instance') { @@ -187,7 +193,7 @@ function registerCleanupHandlers () { // Initialize cleanup handlers registerCleanupHandlers() -module.exports = { +export { listWidgets, runWidget, stopWidget, diff --git a/src/app/widgets/widget-batch-op.js b/src/app/widgets/widget-batch-op.js index 85f28d0..b96fca9 100644 --- a/src/app/widgets/widget-batch-op.js +++ b/src/app/widgets/widget-batch-op.js @@ -4,7 +4,7 @@ * Runs entirely in the frontend, uses MCP tools for execution */ -const uid = require('../common/uid') +import uid from '../common/uid.js' const widgetInfo = { name: 'Batch Operation', @@ -35,7 +35,7 @@ async function widgetRun (config) { } } -module.exports = { +export { widgetInfo, getDefaultConfig, widgetRun diff --git a/src/app/widgets/widget-local-file-server.js b/src/app/widgets/widget-local-file-server.js index daf3348..c6cc75e 100644 --- a/src/app/widgets/widget-local-file-server.js +++ b/src/app/widgets/widget-local-file-server.js @@ -1,7 +1,7 @@ -const os = require('os') -// const path = require('path') -const express = require('express') -const uid = require('../common/uid') +import os from 'os' +// import path from 'path' +import express from 'express' +import uid from '../common/uid.js' const widgetInfo = { name: 'Static File Server', @@ -188,7 +188,7 @@ function widgetRun (instanceConfig) { } } -module.exports = { +export { widgetInfo, widgetRun } diff --git a/src/app/widgets/widget-local-ftp-server.js b/src/app/widgets/widget-local-ftp-server.js index 429b91a..c573fae 100644 --- a/src/app/widgets/widget-local-ftp-server.js +++ b/src/app/widgets/widget-local-ftp-server.js @@ -1,6 +1,6 @@ -const os = require('os') -const uid = require('../common/uid') -const FtpSrv = require('@electerm/ftp-srv') +import os from 'os' +import uid from '../common/uid.js' +import FtpSrv from '@electerm/ftp-srv' const widgetInfo = { name: 'Local FTP Server', @@ -137,7 +137,7 @@ function widgetRun (instanceConfig) { } } -module.exports = { +export { widgetInfo, widgetRun } diff --git a/src/app/widgets/widget-mcp-server.js b/src/app/widgets/widget-mcp-server.js index 43cb5f0..e9a359f 100644 --- a/src/app/widgets/widget-mcp-server.js +++ b/src/app/widgets/widget-mcp-server.js @@ -5,20 +5,18 @@ * Uses a simple local MCP implementation */ -const { ipcMain } = require('electron') -const { McpServer } = require('../mcp/server/mcp.js') -const { StreamableHTTPServerTransport } = require('../mcp/server/streamableHttp.js') -const { TaskManager } = require('../mcp/server/tasks.js') -const { z } = require('../lib/zod') -const express = require('express') -const uid = require('../common/uid') -const globalState = require('../lib/glob-state') -const { +import { McpServer } from '../mcp/server/mcp.js' +import { StreamableHTTPServerTransport } from '../mcp/server/streamableHttp.js' +import { TaskManager } from '../mcp/server/tasks.js' +import { z } from '../lib/zod.js' +import express from 'express' +import uid from '../common/uid.js' +import globalState from '../server/global-state.js' +import { sshBookmarkSchema, telnetBookmarkSchema, - serialBookmarkSchema, - localBookmarkSchema -} = require('../common/bookmark-zod-schemas') + serialBookmarkSchema +} from '../common/bookmark-zod-schemas.js' // Dangerous tab props that allow arbitrary command execution. // Must be stripped from any MCP tool args before forwarding to the renderer. @@ -87,7 +85,6 @@ const widgetInfo = { default: true, description: 'Enable bookmark group APIs' }, - { name: 'enableSftp', type: 'boolean', @@ -155,7 +152,7 @@ function getDefaultConfig () { class ElectermMCPServer { constructor (config) { this.config = config - // API key is optional - skip auth if not provided + // API key is optional; when empty, authentication is skipped. this.instanceId = uid() this.httpServer = null this.mcpServer = null @@ -165,71 +162,42 @@ class ElectermMCPServer { this.taskManager = null } - // Built-in blacklist: patterns that are always blocked regardless of user config. - // These cover the most common destructive / privilege-escalation shell idioms. static get BUILTIN_BLACKLIST () { return [ - /rm\s+-[^\s]*[rR][^\s]*\s+\//, // rm -rf / or rm -Rf / (recursive delete from root) - /rm\s+-[^\s]*[rR][^\s]*\s+~/, // rm -rf ~ or rm -Rf ~ (recursive delete home) - /rm\s+--recursive/, // rm --recursive (long-form flag) - /:\s*\(\s*\)\s*\{.*\|.*:.*&.*\}\s*;.*:/, // fork bomb :(){:|:&};: - /\bdd\b.*\bof\s*=\s*\/dev\//, // dd of=/dev/... - /\bmkfs\b/, // mkfs (format filesystem) - />\s*\/dev\/[sh]d[a-z]/, // redirect to raw disk - /\bsudo\s+rm\b/, // sudo rm - /curl\s+.*\|\s*sh/, // curl | sh (remote code execution) - /wget\s+.*\|\s*sh/, // wget | sh - /curl\s+.*\|\s*bash/, // curl | bash - /wget\s+.*\|\s*bash/ // wget | bash + /rm\s+-[^\s]*[rR][^\s]*\s+\//, + /rm\s+-[^\s]*[rR][^\s]*\s+~/, + /rm\s+--recursive/, + /:\s*\(\s*\)\s*\{.*\|.*:.*&.*\}\s*;.*:/, + /\bdd\b.*\bof\s*=\s*\/dev\//, + /\bmkfs\b/, + />\s*\/dev\/[sh]d[a-z]/, + /\bsudo\s+rm\b/, + /curl\s+.*\|\s*sh/, + /wget\s+.*\|\s*sh/, + /curl\s+.*\|\s*bash/, + /wget\s+.*\|\s*bash/ ] } - // Validate a command against whitelist/blacklist rules. - // Returns { allowed: true } or { allowed: false, reason: string } validateCommand (command) { - // 1. Always-on built-in blacklist for (const pattern of ElectermMCPServer.BUILTIN_BLACKLIST) { if (pattern.test(command)) { return { allowed: false, reason: `Command blocked by built-in safety rule: ${pattern}` } } } - - // 2. User-defined blacklist (newline-separated regex strings) - const userBlacklist = (this.config.commandBlacklist || '') - .split('\n') - .map(s => s.trim()) - .filter(Boolean) - + const userBlacklist = (this.config.commandBlacklist || '').split('\n').map(s => s.trim()).filter(Boolean) for (const raw of userBlacklist) { try { if (new RegExp(raw).test(command)) { return { allowed: false, reason: `Command blocked by blacklist pattern: ${raw}` } } - } catch (_) { - // ignore invalid regex in config - } + } catch (_) {} } - - // 3. User-defined whitelist (newline-separated regex strings) - // Only enforced when at least one pattern is configured. - const userWhitelist = (this.config.commandWhitelist || '') - .split('\n') - .map(s => s.trim()) - .filter(Boolean) - + const userWhitelist = (this.config.commandWhitelist || '').split('\n').map(s => s.trim()).filter(Boolean) if (userWhitelist.length > 0) { - const allowed = userWhitelist.some(raw => { - try { - return new RegExp(raw).test(command) - } catch (_) { - return false - } - }) - if (!allowed) { - return { allowed: false, reason: 'Command not in whitelist' } - } + const allowed = userWhitelist.some(raw => { try { return new RegExp(raw).test(command) } catch (_) { return false } }) + if (!allowed) { return { allowed: false, reason: 'Command not in whitelist' } } } - return { allowed: true } } @@ -237,10 +205,10 @@ class ElectermMCPServer { sendToRenderer (action, data, timeoutMs = 30000) { return new Promise((resolve, reject) => { const requestId = uid() - const win = globalState.get('win') + const commonWs = globalState.getCommonWs() - if (!win) { - reject(new Error('No active window')) + if (!commonWs) { + reject(new Error('No commonWs connection available')) return } @@ -253,7 +221,8 @@ class ElectermMCPServer { this.pendingRequests.set(requestId, { resolve, reject, timeout }) // Send to renderer - win.webContents.send('mcp-request', { + commonWs.s({ + type: 'mcp-request', requestId, action, data @@ -434,18 +403,6 @@ class ElectermMCPServer { } ) - server.registerTool( - 'open_electerm_local_terminal', - { - description: 'Open a new electerm local terminal tab', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'open_local_terminal', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - server.registerTool( 'send_electerm_terminal_command', { @@ -685,21 +642,6 @@ class ElectermMCPServer { } ) - server.registerTool( - 'open_electerm_tab_local', - { - description: 'Open a new Local terminal tab directly with connection parameters (no bookmark created)', - inputSchema: localBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'open_tab', - args: { ...stripDangerousTabProps(args), type: 'local' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - // ==================== Bookmark APIs ==================== if (this.config.enableBookmarks) { server.registerTool( @@ -778,21 +720,6 @@ class ElectermMCPServer { } ) - server.registerTool( - 'add_electerm_bookmark_local', - { - description: 'Add a new Local terminal bookmark to electerm', - inputSchema: localBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'add_bookmark', - args: { ...args, type: 'local' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - server.registerTool( 'edit_electerm_bookmark', { @@ -866,7 +793,6 @@ class ElectermMCPServer { } ) } - // ==================== SFTP APIs ==================== if (this.config.enableSftp) { server.registerTool( @@ -1020,7 +946,6 @@ class ElectermMCPServer { } ) } - // ==================== Settings APIs ==================== if (this.config.enableSettings) { server.registerTool( @@ -1042,8 +967,12 @@ class ElectermMCPServer { const { host, port } = this.config // Set up IPC response handler - this.ipcHandler = (event, response) => { - const { requestId, result, error } = response + this.ipcHandler = (message) => { + const msg = JSON.parse(message) + const { requestId, result, error, type } = msg + if (type !== 'mcp-response-back') { + return + } const pending = this.pendingRequests.get(requestId) if (pending) { clearTimeout(pending.timeout) @@ -1055,10 +984,13 @@ class ElectermMCPServer { } } } - ipcMain.on('mcp-response', this.ipcHandler) + const commonWs = globalState.getCommonWs() + commonWs.on('message', this.ipcHandler) - // Create MCP task manager (SEP-2663) when the tasks extension is enabled - if (this.config.enableTasks) { + // Create MCP task manager (SEP-2663) when the tasks extension is enabled. + // `enableTasks` defaults to true, so treat anything other than an + // explicit `false` (undefined/null from a stale pre-5.0.6 config) as on. + if (this.config.enableTasks !== false) { this.taskManager = new TaskManager({ ttl: this.config.taskTtlMs > 0 ? this.config.taskTtlMs : 3600000 }) @@ -1066,6 +998,10 @@ class ElectermMCPServer { this.taskManager.onCancel = (task) => this.cancelTaskRemote(task) this.taskManager.onSweep = (task) => this.sweepTaskRemote(task) } + console.log( + `[mcp-widget] tasks extension: ${this.taskManager ? 'enabled' : 'disabled'} ` + + `(config.enableTasks = ${JSON.stringify(this.config.enableTasks)})` + ) // Create MCP server this.mcpServer = new McpServer({ @@ -1081,14 +1017,12 @@ class ElectermMCPServer { const app = express() app.use(express.json()) - // Handle CORS — restrict to same-origin only (no wildcard) + // Handle CORS, defaulting to same-origin only. app.use((req, res, next) => { const allowedOrigin = this.config.allowedOrigin || '' if (allowedOrigin) { res.setHeader('Access-Control-Allow-Origin', allowedOrigin) } - // Do NOT set Access-Control-Allow-Origin when no origin is configured - // This blocks cross-origin browser requests by default res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization') if (req.method === 'OPTIONS') { @@ -1098,7 +1032,7 @@ class ElectermMCPServer { next() }) - // Authenticate requests with API key (only if apiKey is configured) + // Authenticate requests with API key when configured. if (this.config.apiKey) { app.use((req, res, next) => { const authHeader = req.headers.authorization || '' @@ -1220,7 +1154,8 @@ class ElectermMCPServer { async stop () { // Remove IPC handler if (this.ipcHandler) { - ipcMain.removeListener('mcp-response', this.ipcHandler) + const commonWs = globalState.getCommonWs() + commonWs.removeListener('message', this.ipcHandler) this.ipcHandler = null } @@ -1283,8 +1218,8 @@ function widgetRun (instanceConfig) { } } -module.exports = { +export { widgetInfo, widgetRun, - _ElectermMCPServer: ElectermMCPServer + ElectermMCPServer } diff --git a/src/app/widgets/widget-rename.js b/src/app/widgets/widget-rename.js index 1509e03..e740fa5 100644 --- a/src/app/widgets/widget-rename.js +++ b/src/app/widgets/widget-rename.js @@ -1,5 +1,26 @@ -const fs = require('fs').promises -const path = require('path') +import fs from 'fs' +import path from 'path' + +const fsPromises = fs.promises +const pathSeparatorPattern = /[\\/]/ +function resolveRenamePath (dir, newName) { + if (typeof newName !== 'string' || !newName.trim() || newName === '.' || newName === '..') { + throw new Error('Template produced an invalid file name') + } + + if (pathSeparatorPattern.test(newName)) { + throw new Error('Template must not include path separators') + } + + const newPath = path.resolve(dir, newName) + const relativePath = path.relative(dir, newPath) + + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + throw new Error('Template must keep files within the source directory') + } + + return newPath +} // Define defaults in one place const DEFAULTS = { @@ -10,7 +31,6 @@ const DEFAULTS = { startNumber: 1, preserveCase: true } -const pathSeparatorPattern = /[\\/]/ const widgetInfo = { name: 'File Renamer', @@ -60,7 +80,7 @@ const widgetInfo = { } async function getFiles (dir, fileTypes, includeSubfolders) { - const files = await fs.readdir(dir, { withFileTypes: true }) + const files = await fsPromises.readdir(dir, { withFileTypes: true }) let results = [] for (const file of files) { const fullPath = path.join(dir, file.name) @@ -77,7 +97,7 @@ async function getFiles (dir, fileTypes, includeSubfolders) { } async function processTemplate (template, filePath, index, startNumber, preserveCase) { - const stats = await fs.stat(filePath) + const stats = await fsPromises.stat(filePath) const parsedPath = path.parse(filePath) const date = new Date(stats.birthtime) const replacements = { @@ -101,25 +121,6 @@ async function processTemplate (template, filePath, index, startNumber, preserve return result } -function resolveRenamePath (dir, newName) { - if (typeof newName !== 'string' || !newName.trim() || newName === '.' || newName === '..') { - throw new Error('Template produced an invalid file name') - } - - if (pathSeparatorPattern.test(newName)) { - throw new Error('Template must not include path separators') - } - - const newPath = path.resolve(dir, newName) - const relativePath = path.relative(dir, newPath) - - if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { - throw new Error('Template must keep files within the source directory') - } - - return newPath -} - async function widgetRun (params = {}) { const config = { ...DEFAULTS, @@ -151,7 +152,7 @@ async function widgetRun (params = {}) { const dir = path.dirname(filePath) const newName = await processTemplate(template, filePath, i, startNumber, preserveCase) const newPath = resolveRenamePath(dir, newName) - await fs.rename(filePath, newPath) + await fsPromises.rename(filePath, newPath) results.push({ oldPath: filePath, @@ -175,7 +176,7 @@ async function widgetRun (params = {}) { } } -module.exports = { +export { widgetInfo, widgetRun } diff --git a/src/client/entry/basic.js b/src/client/entry-web/basic.js similarity index 60% rename from src/client/entry/basic.js rename to src/client/entry-web/basic.js index f3e8690..95b9576 100644 --- a/src/client/entry/basic.js +++ b/src/client/entry-web/basic.js @@ -2,16 +2,33 @@ * init app data then write main script to html body */ import '../electerm-react/css/basic.styl' +import '../web-components/style-overide.styl' import '../electerm-react/css/mobile.styl' +import '../web-components/web-api.js' +import '../web-components/web-pre.js' import { get as _get } from 'lodash-es' -import '../electerm-react/common/pre' -const { isDev } = window.et -const { version } = window.pre.packInfo +const { isDev, version, cdn } = window.et + +window.et.buildWsUrl = ( + host, + port, + tokenElecterm, + id, + type = 'terminals', + extra = '' +) => { + const ss = isDev ? window.et.server : window.location.href + const s = ss + ? ss.replace(/https?:\/\//, '').replace(/\/$/, '') + : `${host}:${port}` + const pre = ss.startsWith('https') ? 'wss' : 'ws' + return `${pre}://${s}/${type}/${id}?token=${tokenElecterm}${extra}` +} async function loadWorker () { return new Promise((resolve) => { - const url = !isDev ? `js/worker-${version}.js` : 'js/worker.js' + const url = !isDev ? cdn + `/js/worker-${version}.js` : cdn + '/js/worker.js' window.worker = new window.Worker(url) function onInit (e) { if (!e || !e.data) { @@ -35,7 +52,7 @@ async function load () { } function loadScript () { const rcs = document.createElement('script') - const url = !isDev ? `js/electerm-${version}.js` : 'js/electerm.js' + const url = !isDev ? cdn + `/js/electerm-${version}.js` : cdn + '/js/electerm.js' rcs.src = url rcs.type = 'module' rcs.onload = () => { @@ -46,10 +63,7 @@ async function load () { } document.body.appendChild(rcs) } - const initLocale = window.pre.runSync('getInitLocale') || {} - window.langMap = initLocale.langMap - window.initLanguage = initLocale.language - window.getLang = (lang = window.store?.config.language || window.initLanguage || 'en_us') => { + window.getLang = (lang = window.store?.config.language || 'en_us') => { return _get(window.langMap, `[${lang}].lang`) } window.translate = txt => { @@ -58,6 +72,12 @@ async function load () { return window.capitalizeFirstLetter(str) } await loadWorker() + if (!window.et.isDev) { + window.worker.postMessage({ + action: 'init-url', + url: window.location.href + }) + } loadScript() } diff --git a/src/client/entry-web/electerm.jsx b/src/client/entry-web/electerm.jsx new file mode 100644 index 0000000..2e6fdd8 --- /dev/null +++ b/src/client/entry-web/electerm.jsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client' +import '../../../node_modules/antd/dist/reset.css' +// import '../electerm-react/common/trzsz' +import '@fontsource/maple-mono/index.css' +import Main from '../web-components/web-main' + +const rootElement = document.getElementById('container') +const root = createRoot(rootElement) + +root.render(
) diff --git a/src/client/entry-web/worker.js b/src/client/entry-web/worker.js new file mode 100644 index 0000000..e4feada --- /dev/null +++ b/src/client/entry-web/worker.js @@ -0,0 +1,200 @@ +/** + * web worker + */ + +self.insts = {} + +function createWs ( + type, + id, + sftpId = '', + config +) { + // init gloabl ws + const { host, port, tokenElecterm, server = '' } = config + const ss = self.currentUrl || server + const s = ss + ? ss.replace(/https?:\/\//, '').replace(/\/$/, '') + : `${host}:${port}` + const pre = ss.startsWith('https') ? 'wss' : 'ws' + const wsUrl = `${pre}://${s}/${type}/${id}?sftpId=${sftpId}&token=${tokenElecterm}` + const ws = new WebSocket(wsUrl) + ws.s = msg => { + ws.send(JSON.stringify(msg)) + } + ws.id = id + // Buffer incoming messages until at least one addEventListener is + // registered. Without this, messages that arrive before the client + // has called addEventListener (e.g. the "session-interactive" prompt + // sent during SSH host-key verification on first connect) are silently + // lost, causing the SSH connection to hang indefinitely. + ws._messageBuffer = [] + ws._bufferActive = true + ws._bufferHandler = (evt) => { + if (ws._bufferActive) { + ws._messageBuffer.push(evt.data) + } + } + ws.addEventListener('message', ws._bufferHandler) + ws.once = (callack, id) => { + const func = (evt) => { + const arg = JSON.parse(evt.data) + if (id === arg.id) { + callack(arg) + ws.removeEventListener('message', func) + } + } + ws.addEventListener('message', func) + } + ws.onclose = () => { + if (ws.dup) { + return + } + send({ + id: ws.id, + action: 'close' + }) + delete self.insts[ws.id] + } + return new Promise((resolve) => { + ws.onopen = () => { + if (self.insts[ws.id]) { + ws.dup = true + ws.close() + resolve(null) + } else { + resolve(ws) + } + } + }) +} + +function send (data) { + self.postMessage(data) +} + +async function onMsg (e) { + const { + id, + wsId, + args, + action, + type, + persist, + url + } = e.data + if (action === 'init-url') { + self.currentUrl = url + return false + } + if (action === 'create') { + const inst = self.insts[id] + if (inst instanceof WebSocket) { + return send({ + action, + id, + persist + }, '*') + } else if (inst) { + return false + } else { + const ws = await createWs(...args) + if (ws) { + self.insts[id] = ws + } + } + send({ + action, + persist, + id + }, '*') + } else if (action === 'once') { + const ws = self.insts[wsId] + if (ws) { + const cb = (data) => { + send({ + id, + wsId, + data + }) + } + ws.once(cb, id) + } + } else if (action === 'close') { + const ws = self.insts[wsId] + if (ws) { + ws.close() + } + } else if (action === 's') { + const ws = self.insts[wsId] + if (ws) { + ws.s(...args) + } + } else if (action === 'addEventListener') { + const ws = self.insts[wsId] + if (ws) { + // Support multiple listeners using a Map keyed by listener ID + if (!ws.listeners) { + ws.listeners = new Map() + } + // Check if this listener ID already exists (prevent duplicates for same ID) + if (ws.listeners.has(id)) { + ws.removeEventListener(type, ws.listeners.get(id).cb) + } + const cb = (e) => { + send({ + wsId, + id, + data: { + data: e.data + } + }) + } + ws.listeners.set(id, { type, cb }) + ws.addEventListener(type, cb) + // Flush any buffered messages to the newly registered listener so + // that messages received before addEventListener was called are + // not lost (fixes first-use SSH connection hang). + if (type === 'message' && ws._messageBuffer && ws._messageBuffer.length > 0) { + for (const bufData of ws._messageBuffer) { + send({ + wsId, + id, + data: { + data: bufData + } + }) + } + } + // Stop buffering once at least one listener is active – future + // messages will be delivered directly via the addEventListener + // callback. Keep the buffer array around briefly so that any + // subsequent addEventListener calls (e.g. MCP handler) can also + // receive the backlog. + if (type === 'message' && ws._bufferActive) { + ws._bufferActive = false + if (ws._bufferHandler) { + ws.removeEventListener('message', ws._bufferHandler) + ws._bufferHandler = null + } + setTimeout(() => { + ws._messageBuffer = null + }, 5000) + } + } + } else if (action === 'removeEventListener') { + const ws = self.insts[wsId] + if (ws && ws.listeners && ws.listeners.has(id)) { + const listener = ws.listeners.get(id) + ws.removeEventListener(listener.type, listener.cb) + ws.listeners.delete(id) + } + } +} + +self.addEventListener('message', onMsg) +setTimeout(() => { + send({ + action: 'worker-init' + }) +}, 10) diff --git a/src/client/entry/electerm.jsx b/src/client/entry/electerm.jsx deleted file mode 100644 index 6154609..0000000 --- a/src/client/entry/electerm.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createRoot } from 'react-dom/client' -import 'antd/dist/reset.css' -import '@fontsource/maple-mono/index.css' -import Main from '../harmony/main.jsx' - -const rootElement = createRoot(document.getElementById('container')) -rootElement.render( -
-) diff --git a/src/client/entry/worker.js b/src/client/entry/worker.js deleted file mode 100644 index 54d77a9..0000000 --- a/src/client/entry/worker.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * web worker - */ - -self.insts = {} - -function createWs ( - type, - id, - sftpId = '', - config -) { - // init gloabl ws - const { host, port, tokenElecterm } = config - const wsUrl = `ws://${host}:${port}/${type}/${id}?&sftpId=${sftpId}&token=${tokenElecterm}` - const ws = new WebSocket(wsUrl) - ws.s = msg => { - try { - ws.send(JSON.stringify(msg)) - } catch (e) { - console.error('ws send error', e) - } - } - ws.id = id - ws.once = (callack, id) => { - const func = (evt) => { - const arg = JSON.parse(evt.data) - if (id === arg.id) { - callack(arg) - ws.removeEventListener('message', func) - } - } - ws.addEventListener('message', func) - } - ws.onclose = () => { - if (ws.dup) { - return - } - send({ - id: ws.id, - action: 'close' - }) - delete self.insts[ws.id] - } - return new Promise((resolve) => { - ws.onopen = () => { - if (self.insts[ws.id]) { - ws.dup = true - ws.close() - resolve(null) - } else { - resolve(ws) - } - } - }) -} - -function send (data) { - self.postMessage(data) -} - -async function onMsg (e) { - const { - id, - wsId, - args, - action, - type, - persist - } = e.data - if (action === 'create') { - const inst = self.insts[id] - if (inst instanceof WebSocket) { - return send({ - action, - id, - persist - }, '*') - } else if (inst) { - return false - } else { - const ws = await createWs(...args) - if (ws) { - self.insts[id] = ws - } - } - send({ - action, - persist, - id - }, '*') - } else if (action === 'once') { - const ws = self.insts[wsId] - if (ws) { - const cb = (data) => { - send({ - id, - wsId, - data - }) - } - ws.once(cb, id) - } - } else if (action === 'close') { - const ws = self.insts[wsId] - if (ws) { - ws.close() - } - } else if (action === 's') { - const ws = self.insts[wsId] - if (ws) { - ws.s(...args) - } - } else if (action === 'addEventListener') { - const ws = self.insts[wsId] - if (ws) { - if (!ws.cbs) { - ws.cbs = {} - } - const cb = (e) => { - send({ - wsId, - id, - data: { - data: e.data - } - }) - } - ws.cbs[id] = cb - ws.addEventListener(type, cb) - } - } else if (action === 'removeEventListener') { - const ws = self.insts[wsId] - if (ws && ws.cbs && ws.cbs[id]) { - ws.removeEventListener(type, ws.cbs[id]) - delete ws.cbs[id] - } - } -} - -self.addEventListener('message', onMsg) -setTimeout(() => { - send({ - action: 'worker-init' - }) -}, 10) diff --git a/src/client/file-select-dialog/file-item.jsx b/src/client/file-select-dialog/file-item.jsx new file mode 100644 index 0000000..bd1dcf3 --- /dev/null +++ b/src/client/file-select-dialog/file-item.jsx @@ -0,0 +1,34 @@ +import FileIcon from '../electerm-react/components/sftp/file-icon' +import classNames from 'classnames' +export default function FileItem (props) { + const { + file, + selected, + onClick, + onDbClick + } = props + const handleClick = (e) => { + onClick(file, e) + } + const handleDbClick = () => { + onDbClick(file) + } + const cls = classNames( + 'dialog-file-item elli', + { + selected + } + ) + return ( +
+ + {file.name} +
+ ) +} diff --git a/src/client/file-select-dialog/file-select-dialog.jsx b/src/client/file-select-dialog/file-select-dialog.jsx new file mode 100644 index 0000000..9d7ed47 --- /dev/null +++ b/src/client/file-select-dialog/file-select-dialog.jsx @@ -0,0 +1,542 @@ +/** + * file/folder select dialog component + */ + +import { Component } from 'react' +import { + Spin, + Pagination, + Button, + Input, + ConfigProvider +} from 'antd' +import { SaveOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons' +import Modal from '../electerm-react/components/common/modal' +import { notification } from '../electerm-react/components/common/notification' +import FileItem from './file-item' +import AddressBar from '../electerm-react/components/sftp/address-bar' +import isValidPath from '../electerm-react/common/is-valid-path' +import { + typeMap +} from '../electerm-react/common/constants' +import { resolve } from '../web-components/path' +import './file-select-dialog.styl' + +const s = window.translate + +export default class FileSelectDialog extends Component { + constructor (props) { + super(props) + const p = window.localStorage.getItem(this.lsKey) || window.et.home + this.state = { + opts: null, + isSaveDialog: false, + saveFileName: '', + loading: false, + page: 1, + localShowHiddenFile: false, + localPathHistory: [], + fileSelected: null, + selectedFiles: [], + lastClickedIndex: null, + pageSize: 100, + localInputFocus: false, + list: [], + localPathTemp: p, + localPath: p + } + } + + componentDidMount () { + window.addEventListener('message', this.handleMsg) + } + + componentWillUnmount () { + window.removeEventListener('message', this.handleMsg) + } + + lsKey = 'dialog-start-path' + + fileInputRef = null + + handleBrowserUpload = () => { + if (this.fileInputRef) { + this.fileInputRef.click() + } + } + + handleBrowserFileChange = (e) => { + const file = e.target.files[0] + if (!file) return + const reader = new FileReader() + reader.onload = (evt) => { + const fileContent = evt.target.result + const fileName = file.name + this.setState({ opts: null }) + window.postMessage({ + type: 'handleDialog', + data: { fileContent, fileName } + }, '*') + } + reader.readAsText(file) + e.target.value = '' + } + + handleBrowserDownload = () => { + const { opts } = this.state + const { filename, content } = opts + const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + this.handleClose() + } + + handleMsg = (e) => { + if (e?.data?.type === 'openDialog') { + this.setState({ opts: e.data.data, isSaveDialog: false, saveFileName: '' }, this.localList) + } else if (e?.data?.type === 'saveDialog') { + const opts = e.data.data || {} + const defaultName = opts.defaultPath || '' + this.setState({ opts, isSaveDialog: true, saveFileName: defaultName }, this.localList) + } + } + + handlePageChange = (page, pageSize) => { + this.setState({ page, pageSize, lastClickedIndex: null }) + } + + handlePageSizeChange = (k, pageSize) => { + this.setState({ pageSize }) + } + + handleLocalPathChange = (e) => { + this.setState({ localPath: e.target.value }) + } + + handleClose = () => { + const { isSaveDialog } = this.state + if (isSaveDialog) { + window.postMessage({ + type: 'closeSaveDialog' + }, '*') + } else { + window.postMessage({ + type: 'closeDialog' + }, '*') + } + this.setState({ opts: null }) + } + + isMultiSelectMode = () => { + const { opts, isSaveDialog } = this.state + const properties = opts?.properties || [] + return !isSaveDialog && + properties.includes('openFile') && + properties.includes('multiSelections') + } + + handleSubmit = () => { + const { selectedFiles, fileSelected, localPath, isSaveDialog, saveFileName } = this.state + if (isSaveDialog) { + const name = saveFileName.trim() + if (!name) { + return notification.warning({ message: 'Please enter a file name' }) + } + const filePath = resolve(localPath, name) + this.setState({ opts: null }) + window.postMessage({ + type: 'handleSaveDialog', + data: { canceled: false, filePath } + }, '*') + return + } + if (selectedFiles.length) { + const paths = selectedFiles.map(f => resolve(localPath, f.name)) + this.setState({ opts: null }) + window.postMessage({ + type: 'handleDialog', + data: paths + }, '*') + return + } + const p = fileSelected + ? resolve(localPath, fileSelected.name) + : localPath + this.setState({ + opts: null + }) + window.postMessage({ + type: 'handleDialog', + data: [p] + }, '*') + } + + localList = async () => { + this.setState({ + loading: true, + fileSelected: null, + selectedFiles: [], + lastClickedIndex: null + }) + const { + localPath, + opts, + isSaveDialog + } = this.state + const properties = opts?.properties || [] + const func = !isSaveDialog && properties.includes('openDirectory') + ? window.fs.readdirOnly + : window.fs.readdirAndFiles + const list = await func(localPath) + .catch((err) => { + console.log(err) + return [] + }) + this.updateLs(localPath) + this.setState({ list, loading: false, page: 1 }) + } + + onChange = e => { + this.setState({ + localPathTemp: e.target.value + }) + } + + onInputBlur = (type) => { + this.inputFocus = false + this.timer4 = setTimeout(() => { + this.setState({ + [type + 'InputFocus']: false + }) + }, 200) + } + + onInputFocus = (type) => { + this.setState({ + [type + 'InputFocus']: true + }) + this.inputFocus = true + } + + onGoto = (type, e) => { + e && e.preventDefault() + const n = `${type}Path` + const nt = n + 'Temp' + const np = this.state[nt] + if (!isValidPath(np)) { + return notification.warning({ + message: 'path not valid' + }) + } + this.updateLs(np) + this.setState({ + [n]: np + }, this[`${type}List`]) + } + + updateLs = (np = this.state.localPath) => { + window.localStorage.setItem(this.lsKey, np) + } + + toggleShowHiddenFile = type => { + const prop = `${type}ShowHiddenFile` + const b = this.state[prop] + this.setState({ + [prop]: !b + }) + } + + onClickHistory = (type, path) => { + const n = `${type}Path` + this.setState({ + [n]: path, + [`${n}Temp`]: path + }, this[`${type}List`]) + } + + goParent = (type) => { + const n = `${type}Path` + const p = this.state[n] + const np = resolve(p, '..') + if (np !== p) { + this.updateLs(np) + this.setState({ + [n]: np, + [n + 'Temp']: np + }, this[`${type}List`]) + } + } + + handleClickFile = (item, index, event) => { + const { isSaveDialog } = this.state + if (isSaveDialog) { + if (!item.isDirectory) { + this.setState({ + fileSelected: item, + saveFileName: item.name, + selectedFiles: [item] + }) + } else { + this.setState({ + fileSelected: item, + selectedFiles: [item] + }) + } + return + } + if (!this.isMultiSelectMode()) { + this.setState({ + fileSelected: item, + selectedFiles: [item], + lastClickedIndex: index + }) + return + } + // multi-select file mode + const { selectedFiles, lastClickedIndex, list } = this.state + const shift = event?.shiftKey + const meta = event?.metaKey || event?.ctrlKey + if (shift && lastClickedIndex !== null) { + const start = Math.min(lastClickedIndex, index) + const end = Math.max(lastClickedIndex, index) + const range = list.slice(start, end + 1) + this.setState({ + selectedFiles: range, + fileSelected: item + }) + event?.preventDefault?.() + } else if (meta) { + const exists = selectedFiles.some(f => f.name === item.name) + const next = exists + ? selectedFiles.filter(f => f.name !== item.name) + : [...selectedFiles, item] + this.setState({ + selectedFiles: next, + fileSelected: next.length ? item : null, + lastClickedIndex: index + }) + event?.preventDefault?.() + } else { + this.setState({ + selectedFiles: [item], + fileSelected: item, + lastClickedIndex: index + }) + } + } + + handleDbClickFile = (item) => { + if (!item.isDirectory) { + return false + } + const { localPath } = this.state + const np = resolve(localPath, item.name) + this.setState({ + localPath: np, + localPathTemp: np + }, this.localList) + } + + renderSaveInput () { + const { + isSaveDialog, + saveFileName + } = this.state + if (!isSaveDialog) { + return null + } + return ( +
+ } + onChange={e => this.setState({ saveFileName: e.target.value })} + /> +
+ ) + } + + renderHeader () { + const { + localPath, + localPathTemp, + loading, + localPathHistory, + localInputFocus, + localShowHiddenFile + } = this.state + const props = { + type: typeMap.local, + onChange: this.onChange, + onInputBlur: this.onInputBlur, + onInputFocus: this.onInputFocus, + onGoto: this.onGoto, + localInputFocus, + localPath, + localShowHiddenFile, + toggleShowHiddenFile: this.toggleShowHiddenFile, + localPathTemp, + onClickHistory: this.onClickHistory, + goParent: this.goParent, + localPathHistory, + loadingSftp: loading + } + return ( +
+ +
+ ) + } + + renderFooter () { + const e = window.translate + const { + isSaveDialog, + selectedFiles + } = this.state + const opts = this.state.opts + const properties = opts?.properties || [] + const disabled = !isSaveDialog && + properties.includes('openFile') && + selectedFiles.length === 0 + const noBrowserTransfer = opts?.noBrowserTransfer + const showBrowserUpload = !noBrowserTransfer && !isSaveDialog && properties.includes('openFile') + const showBrowserDownload = !noBrowserTransfer && !isSaveDialog && opts?.content + return ( +
+
+ {this.renderPager()} + {showBrowserUpload && ( + + )} + {showBrowserDownload && ( + + )} +
+
+ + +
+
+ ) + } + + renderList () { + const { list, selectedFiles, page, pageSize } = this.state + const all = list.slice((page - 1) * pageSize, page * pageSize) + const offset = (page - 1) * pageSize + const selectedNames = new Set(selectedFiles.map(f => f.name)) + return ( +
+ { + all.map((item, i) => { + const index = offset + i + return ( + this.handleClickFile(file, index, ev)} + /> + ) + }) + } +
+ ) + } + + renderPager () { + const { + page, + pageSize, + list + } = this.state + const len = list.length + if (len <= pageSize) { + return null + } + return ( + + ) + } + + renderContent = () => { + const { + opts, + loading, + isSaveDialog + } = this.state + const props = { + maskClosable: false, + open: true, + width: 'min(800px, 90vw)', + title: opts.title || (isSaveDialog ? 'Save As' : 'Open'), + footer: this.renderFooter(), + onCancel: this.handleClose, + wrapClassName: 'file-select-modal' + } + return ( + + { this.fileInputRef = r }} + className='hide' + onChange={this.handleBrowserFileChange} + /> + + + {this.renderSaveInput()} + {this.renderHeader()} + {this.renderList()} + + + + ) + } + + render () { + const { + opts + } = this.state + if (!opts) { + return null + } + return this.renderContent() + } +} diff --git a/src/client/file-select-dialog/file-select-dialog.styl b/src/client/file-select-dialog/file-select-dialog.styl new file mode 100644 index 0000000..0318515 --- /dev/null +++ b/src/client/file-select-dialog/file-select-dialog.styl @@ -0,0 +1,35 @@ + +.dialog-file-item + user-select none + padding 6px 10px + &:hover + background-color var(--primary) + color var(--primary-contrast) + &.selected + background-color var(--primary) + color var(--primary-contrast) +.file-dialog-list-wrap + height calc(min(600px, 90vh) - 200px) + overflow-y auto +.file-dialog-header + .sftp-title + .anticon-eye-invisible + .anticon-home + .anticon-plus + display none +.file-select-dialog-footer + display flex + justify-content space-between + align-items center + flex-wrap wrap + gap 8px +.file-select-dialog-footer-actions + display flex + align-items center + flex-wrap wrap + gap 8px +.file-select-dialog-footer-submit + display flex + align-items center + gap 8px + margin-left auto \ No newline at end of file diff --git a/src/client/harmony/language-select.jsx b/src/client/harmony/language-select.jsx deleted file mode 100644 index 6ebf938..0000000 --- a/src/client/harmony/language-select.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useMemo } from 'react' -import { GlobalOutlined } from '@ant-design/icons' -import './language-select.styl' - -// window.localStorage key that records the one-time language pick. -// When absent, we prompt the user to choose a language once. -const STORAGE_KEY = 'locale' - -function getLangs () { - // window.et.langs is the canonical list, but it is only populated - // after the store finishes initApp(). At first mount window.langMap - // (from getInitLocale) is already available, so derive from it as a - // fallback — both expose { id, name }. - if (Array.isArray(window.et?.langs) && window.et.langs.length) { - return window.et.langs - } - return Object.values(window.langMap || {}) -} - -export default function LanguageSelect ({ children }) { - const langs = useMemo(getLangs, []) - // locale is set once after the first pick. Also skip the prompt when - // no language data is available yet, so the user is never trapped - // behind an empty picker. - const selected = !!window.localStorage.getItem(STORAGE_KEY) || !langs.length - - const choose = async langId => { - // 1. mark the choice so we never prompt again - window.localStorage.setItem(STORAGE_KEY, langId) - // 2. persist into user config so the app actually boots in this - // language (saveUserConfig merges, so other settings are kept) - try { - await window.pre.runGlobalAsync('saveUserConfig', { language: langId }) - } catch (err) { - console.error('[language-select] saveUserConfig failed', err) - } - // 3. reboot — language/translate are resolved at load time - window.location.reload() - } - - if (selected) { - return children - } - - return ( -
-
- -
- Select language / 选择语言 -
-
- { - langs.map(l => ( - - )) - } -
-
-
- ) -} diff --git a/src/client/harmony/language-select.styl b/src/client/harmony/language-select.styl deleted file mode 100644 index 3d1a1f3..0000000 --- a/src/client/harmony/language-select.styl +++ /dev/null @@ -1,50 +0,0 @@ -.language-select-wrap - position fixed - left 0 - top 0 - width 100% - height 100% - z-index 9999 - background #fff - display flex - align-items center - justify-content center - -.language-select-card - width 420px - max-width 90vw - padding 36px 28px - text-align center - -.language-select-icon - font-size 48px - color #08c - -.language-select-title - margin 16px 0 24px - font-size 18px - font-weight 600 - color #333 - -.language-select-list - display flex - flex-wrap wrap - gap 10px - justify-content center - -.language-select-item - min-width 120px - padding 10px 16px - font-size 14px - color #333 - background #f5f5f5 - border 1px solid #e0e0e0 - border-radius 8px - cursor pointer - transition all 0.15s ease - outline none - - &:hover - color #fff - background #08c - border-color #08c diff --git a/src/client/harmony/main.jsx b/src/client/harmony/main.jsx deleted file mode 100644 index e9684a9..0000000 --- a/src/client/harmony/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import Entry from '../electerm-react/components/main/index.jsx' -import LanguageSelect from './language-select.jsx' - -export default function Main () { - return ( - - - - ) -} diff --git a/src/client/simple-auth/logout.jsx b/src/client/simple-auth/logout.jsx new file mode 100644 index 0000000..ef2b16b --- /dev/null +++ b/src/client/simple-auth/logout.jsx @@ -0,0 +1,25 @@ +import { auto } from 'manate/react' +import { + LogoutOutlined +} from '@ant-design/icons' +import './logout.styl' + +export default auto(function Logout (props) { + const handleLogout = () => { + window.localStorage.removeItem('tokenElecterm') + props.store.logined = false + } + + if (window.et.tokenElecterm) { + return null + } + + return ( +
+ +
+ ) +}) diff --git a/src/client/simple-auth/logout.styl b/src/client/simple-auth/logout.styl new file mode 100644 index 0000000..eaa9b0d --- /dev/null +++ b/src/client/simple-auth/logout.styl @@ -0,0 +1,8 @@ +.logout-icon + position fixed + left 0 + bottom 0 + z-index 100 + width 43px + height 48px + text-align center \ No newline at end of file diff --git a/src/client/simple-auth/web-login.jsx b/src/client/simple-auth/web-login.jsx new file mode 100644 index 0000000..7cd0a58 --- /dev/null +++ b/src/client/simple-auth/web-login.jsx @@ -0,0 +1,104 @@ +import { auto } from 'manate/react' +import { useState, useEffect, useRef } from 'react' +import LogoElem from '../electerm-react/components/common/logo-elem.jsx' +import { + Input, + Spin +} from 'antd' +import message from '../electerm-react/components/common/message' +import { + ArrowRightOutlined, + Loading3QuartersOutlined +} from '@ant-design/icons' +import Main from '../electerm-react/components/main/main.jsx' + +const f = window.translate + +export default auto(function Login ({ store }) { + const [pass, setPass] = useState('') + const submitting = useRef(false) + + useEffect(() => { + store.getConstants() + }, []) + + const handlePassChange = e => { + setPass(e.target.value) + } + + const handleSubmit = async () => { + if (!pass) { + return message.warning('password required') + } else if (submitting.current) { + return + } + submitting.current = true + await store.login(pass) + submitting.current = false + } + + const renderUnchecked = () => { + return ( + +
+ +
+ +
+ +
+ ) + } + + const renderAfter = () => { + return ( + + ) + } + + const renderLogin = () => { + const { + logining, + fetchingUser + } = store + + return ( + +
+ +
+ +
+ +
+ +
+ +
+ ) + } + + if (!store.authChecked) { + return renderUnchecked() + } else if (!store.logined) { + return renderLogin() + } + + return ( +
+ ) +}) diff --git a/src/client/statics/favicon.ico b/src/client/statics/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f808f3a29e79df183a7e18a1c8e0a7547c2f94f2 GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYHS0yw${w*&EeApWALqGd;n z8-Qy6q92KfByjvJCJ@50GWvmboNexyP>0JGFS=Beuz#42@}Uh zgA4%4BXnW1@aZR{7u_^qeu)9%dLTAIm!k&%KhP)t`xzMiFf%ZGU}Ru805r4#ih&dy UG=SuQ_yJJe4|bp)kUk&=0H7L+MgRZ+ literal 0 HcmV?d00001 diff --git a/src/client/web-components/path.js b/src/client/web-components/path.js new file mode 100644 index 0000000..b25a876 --- /dev/null +++ b/src/client/web-components/path.js @@ -0,0 +1,57 @@ +export function join (...parts) { + const { isWin } = window.et + const separator = isWin ? '\\' : '/' + const joined = parts.join(separator) + const regex = new RegExp(`${separator}{2,}`, 'g') + return joined.replace(regex, separator) +} + +export function resolve (...paths) { + const { isWin } = window.et + const separator = isWin ? '\\' : '/' + const resolved = [] + + let root = '' + if (paths[0].startsWith(separator)) { + root = separator + paths[0] = paths[0].slice(1) + } else if (paths[0].match(/^[a-zA-Z]+:/)) { + root = paths.shift() + separator + } + const len = paths.length + if (paths[len - 1].endsWith(separator)) { + paths[len - 1] = paths[len - 1].slice(0, -1) + } + + for (const path of paths) { + if (typeof path !== 'string') { + throw new TypeError(`Invalid argument type: ${typeof path}`) + } + + const parts = path.split(separator).filter(d => d) + + for (const part of parts) { + if (part === '') { + resolved.length = 0 + break + } else if (part === '.') { + continue + } else if (part === '..') { + resolved.pop() + } else { + resolved.push(part) + } + } + } + + return `${root}${resolved.join(separator)}` +} + +export function basename (path, ext) { + const { isWin } = window.et + const separator = isWin ? '\\' : '/' + const parts = path.split(separator).filter(d => d) + const lastPart = parts[parts.length - 1] + const basename = ext ? lastPart.slice(0, -ext.length) : lastPart + return basename +} diff --git a/src/client/web-components/store-login.js b/src/client/web-components/store-login.js new file mode 100644 index 0000000..3d97d5d --- /dev/null +++ b/src/client/web-components/store-login.js @@ -0,0 +1,51 @@ +import Fetch from '../electerm-react/common/fetch.jsx' +import { initWsCommon } from '../electerm-react/common/fetch-from-server.js' + +export default Store => { + Store.prototype.getConstants = async function () { + const { store } = window + store.fetchingUser = true + const res = await Fetch.get('/api/get-constants', null, { + handleErr: console.log + }) + if (res) { + Object.assign(window.pre, res) + window.reqs.fs.constants = window.pre.fsConstants + store.updateConfig(res.config) + await initWsCommon() + Object.assign(store, { + logined: true, + authChecked: true, + fetchingUser: false, + logining: false + }) + return true + } else { + console.log('getConstants err') + store.authChecked = true + Object.assign(store, { + authChecked: true, + logined: false, + fetchingUser: false + }) + return false + } + } + Store.prototype.login = async function (password) { + const { store } = window + store.logining = true + const res = await Fetch.post('/api/login', { + password + }) + if (res) { + store.updateConfig({ + tokenElecterm: res + }) + window.localStorage.setItem('tokenElecterm', res) + store.getConstants() + } + Object.assign(store, { + logining: false + }) + } +} diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl new file mode 100644 index 0000000..f2d432e --- /dev/null +++ b/src/client/web-components/style-overide.styl @@ -0,0 +1,36 @@ +@-moz-document url-prefix() + .tabs-inner + overflow-x hidden !important + +// Fix custom modal not rendering content on Android WebView. +// +// Root cause: the original .custom-modal-wrap is position:fixed WITH +// overflow:auto. On Android WebView, overflow on a position:fixed element +// does not render its children — the mask shows but the content card is +// invisible. Changing overflow to hidden (previous attempt) did not help +// because even overflow:hidden on position:fixed can trigger the same bug. +// +// Fix: switch .custom-modal-wrap and .custom-modal-mask from position:fixed +// to position:absolute. The original overflow:auto / position:relative / +// min-height:100% from modal.styl all work correctly on position:absolute +// elements in Android WebView. Since the modal covers the full viewport +// (top/left/right/bottom:0) and the app body does not scroll, absolute +// behaves identically to fixed here. +// +// Use `body` prefix for specificity: this override is imported early (in +// basic.js, before the app bundle), while modal.styl is imported by the Modal +// component (loaded later). With equal specificity the later rule wins, so we +// need the extra `body` ancestor to raise specificity above modal.styl. +// `body` covers both modals inside #container AND Modal.info()/Modal.confirm() +// instances that are appended directly to document.body. +body .custom-modal-wrap + position absolute + +body .custom-modal-mask + position absolute + +body .custom-modal-container + position relative + +body .custom-modal-content + position relative diff --git a/src/client/web-components/web-api.js b/src/client/web-components/web-api.js new file mode 100644 index 0000000..b9ec332 --- /dev/null +++ b/src/client/web-components/web-api.js @@ -0,0 +1,135 @@ +// window preload +// import { message } from 'antd' + +window.api = { + fetch: (url, options = {}) => { + const headers = { + token: window.store?.config.tokenElecterm, + ...options.headers + } + return window.fetch(url, { ...options, headers }) + .then(res => { + if (res.status > 304) { + return res.json() + .catch(() => ({})) + .then(data => { + throw new Error(data.error || `Request failed (${res.status})`) + }) + } + return res + }) + }, + getZoomFactor: () => 1, + setZoomFactor: (nl) => { + // message.info('Set ZoomFactor not supported') + }, + openDialog: (opts) => { + return new Promise((resolve, reject) => { + window.et.handleDialogEvent = (e) => { + if (e?.data?.type === 'handleDialog') { + window.removeEventListener('message', window.et.handleDialogEvent) + delete window.et.handleDialogEvent + resolve(e.data.data) + } else if (e?.data?.type === 'closeDialog') { + window.removeEventListener('message', window.et.handleDialogEvent) + delete window.et.handleDialogEvent + resolve(false) + } + } + window.addEventListener('message', window.et.handleDialogEvent) + window.postMessage({ + type: 'openDialog', + data: opts + }, '*') + }) + }, + saveDialog: (opts) => { + return new Promise((resolve, reject) => { + window.et.handleSaveDialogEvent = (e) => { + if (e?.data?.type === 'handleSaveDialog') { + window.removeEventListener('message', window.et.handleSaveDialogEvent) + delete window.et.handleSaveDialogEvent + resolve(e.data.data) + } else if (e?.data?.type === 'closeSaveDialog') { + window.removeEventListener('message', window.et.handleSaveDialogEvent) + delete window.et.handleSaveDialogEvent + resolve({ canceled: true, filePath: '' }) + } + } + window.addEventListener('message', window.et.handleSaveDialogEvent) + window.postMessage({ + type: 'saveDialog', + data: opts + }, '*') + }) + }, + ipcOnEvent: (event, cb) => { + + }, + ipcOffEvent: (event, cb) => { + + }, + runGlobalAsync: async (func, ...args) => { + if (func === 'initCommandLine') { + try { + const { init } = window.et.query + return init ? JSON.parse(window.et.query.init) : null + } catch (err) { + console.log('initCommandLine error:', err) + } + } else if (func === 'setTitle') { + document.title = args[0] + return + } else if (func === 'openNewInstance') { + return window.open(args[0], '_blank') + } else if (func === 'closeApp') { + return window.close() + } else if (func === 'restart') { + return window.location.reload() + } else if (func === 'init') { + const d = await window.wsFetch({ + action: 'runSync', + args, + func + }) + d.config.tokenElecterm = window.localStorage.getItem('tokenElecterm') || '' + return d + } + return window.wsFetch({ + action: 'runSync', + args, + func + }) + }, + sendMcpResponse: data => { + window.et.commonWs.s({ + type: 'mcp-response-back', + ...data + }) + }, + runSync: (func, ...args) => { + if (func === 'isMaximized') { + return false + } else if (func === 'isSecondInstance') { + return false + } else if (func === 'windowMove') { + return false + } else if (func === 'getLoadTime' || func === 'setLoadTime') { + return 0 + } else if (func === 'getInitTime') { + if (window.et.initTime !== undefined) { + return window.et.initTime + } else { + window.et.initTime = Date.now() + return window.et.initTime + } + } else if (func === 'nodePtyCheck') { + return window.et.hasNodePty + } + return window.wsFetch({ + action: 'runSync', + args, + func + }) + } +} diff --git a/src/client/web-components/web-main.jsx b/src/client/web-components/web-main.jsx new file mode 100644 index 0000000..df4370c --- /dev/null +++ b/src/client/web-components/web-main.jsx @@ -0,0 +1,14 @@ +import ErrorBoundary from '../electerm-react/components/main/error-wrapper' +import Login from '../simple-auth/web-login' +import store from './web-store' +import FileSelectDialog from '../file-select-dialog/file-select-dialog' +import Logout from '../simple-auth/logout' +export default function MainEntry () { + return ( + + + + + + ) +} diff --git a/src/client/web-components/web-pre.js b/src/client/web-components/web-pre.js new file mode 100644 index 0000000..683c7f2 --- /dev/null +++ b/src/client/web-components/web-pre.js @@ -0,0 +1,257 @@ +import * as path from './path.js' +import message from '../electerm-react/components/common/message' + +const { + ipcOnEvent, + ipcOffEvent, + runGlobalAsync, + getZoomFactor, + setZoomFactor, + runSync +} = window.api + +// Encoding function +function encodeUint8Array (uint8Array) { + let str = '' + const len = uint8Array.byteLength + + for (let i = 0; i < len; i++) { + str += String.fromCharCode(uint8Array[i]) + } + + return btoa(str) +} + +// Decoding function +function decodeBase64String (base64String) { + const str = atob(base64String) + const len = str.length + + const uint8Array = new Uint8Array(len) + + for (let i = 0; i < len; i++) { + uint8Array[i] = str.charCodeAt(i) + } + + return uint8Array +} + +window.log = window.console + +// Fallback clipboard copy using execCommand, for Android WebView where +// navigator.clipboard.writeText() may fail silently (non-secure http +// scheme, missing user-gesture context from antd Dropdown menu clicks, +// or Promise rejection that the try/catch does not catch). +// execCommand('copy') uses the WebView's internal clipboard mechanism +// which is connected to the Android system ClipboardManager. +function execCommandCopy (str) { + const textarea = document.createElement('textarea') + textarea.value = str + textarea.setAttribute('readonly', '') + textarea.style.position = 'fixed' + textarea.style.left = '-9999px' + textarea.style.top = '0' + textarea.style.opacity = '0' + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + // For iOS Safari compatibility + textarea.setSelectionRange(0, str.length) + let ok = false + try { + ok = document.execCommand('copy') + } catch (e) { + // ignore + } + document.body.removeChild(textarea) + return ok +} + +window.pre = { + resolve: (...args) => { + return path.resolve(...args.map(d => d || '')) + }, + transferKeys: [ + 'pause', + 'resume', + 'destroy' + ], + // Safe defaults for API-dependent data to prevent render crashes + // before /api/get-constants response arrives (fixes Android info-modal + // showing only background with no content) + osInfoData: [], + osInfo: () => { return window.pre.osInfoData || [] }, + extIconPath: window.et.extIconPath, + readClipboard: () => { + return window.et.clipboard || '' + }, + + writeClipboard: str => { + window.et.clipboard = str + if (!navigator.clipboard) { + // navigator.clipboard not available — use execCommand fallback + // (works in Android WebView via the system ClipboardManager) + if (!execCommandCopy(str)) { + message.error('Clipboard API not available') + } + return + } + try { + const promise = navigator.clipboard.writeText(str) + // Handle Promise rejection — the try/catch above only catches + // synchronous errors, not async rejections. On Android WebView, + // writeText() may reject because the page is served over http:// + // (not a secure context) or the user-gesture requirement is not + // satisfied from a Dropdown menu click. + if (promise && typeof promise.catch === 'function') { + promise.catch(() => { + execCommandCopy(str) + }) + } + return promise + } catch (err) { + // Synchronous error — try execCommand fallback + if (!execCommandCopy(str)) { + message.error('Failed to copy text: ' + err) + } + } + }, + readClipboardSync: function readClipboard () { + if (!navigator.clipboard) { + // Fallback: return in-memory clipboard value (may be stale if the + // user copied via the WebView's native text selection, but there + // is no synchronous clipboard read API available in this case). + return window.et.clipboard || '' + } + try { + return navigator.clipboard.readText() + } catch (err) { + // Fallback: return in-memory clipboard value + return window.et.clipboard || '' + } + }, + + // writeClipboard: function writeClipboard (str) { + // if (!navigator.clipboard) { + // message.error('Clipboard API not available') + // return + // } + // try { + // return navigator.clipboard.writeText(str) + // } catch (err) { + // message.error('Failed to copy text: ' + err) + // } + // }, + showItemInFolder: (href) => runSync('showItemInFolder', href), + ipcOnEvent, + ipcOffEvent, + getZoomFactor, + setZoomFactor, + openExternal: (url) => { + window.open(url, '_blank') + }, + runSync, + runGlobalAsync, + versions: {} +} + +// Ensure window.et.packInfo has all fields required by info-modal.jsx +// On Android/Capacitor the packInfo is minimal and missing author/bugs/releases/etc. +const _packInfoDefaults = { + author: { + name: 'ZHAO Xudong', + email: 'zxdong@gmail.com', + url: 'https://github.com/zxdong262' + }, + homepage: 'https://electerm.org', + bugs: { + url: 'https://github.com/electerm/electerm/issues' + }, + releases: 'https://github.com/electerm/electerm/releases', + sponsorLink: 'https://electerm.org/sponsor-electerm/', + knownIssuesLink: 'https://github.com/electerm/electerm/wiki/Known-issues', + langugeRepo: 'https://github.com/electerm/electerm-languages' +} +if (window.et.packInfo) { + window.et.packInfo = { + ...window.et.packInfo, + ..._packInfoDefaults + } +} + +const fs = { + stat: (path, cb) => { + window.fs.statCustom(path) + .catch(err => cb(err)) + .then(obj => { + obj.isDirectory = () => obj.isD + obj.isFile = () => obj.isF + cb(undefined, obj) + }) + }, + access: (...args) => { + const cb = args.pop() + window.fs.access(...args) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + open: (...args) => { + const cb = args.pop() + window.fs.openCustom(...args) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + read: (p1, arr, ...args) => { + const cb = args.pop() + window.fs.readCustom( + p1, + encodeUint8Array(arr), + ...args + ) + .then((data) => { + const { n, newArr } = data + const newArr1 = decodeBase64String(newArr) + cb(undefined, n, newArr1) + }) + .catch(err => cb(err)) + }, + close: (fd, cb) => { + window.fs.closeCustom(fd) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + readdir: (p, cb) => { + window.fs.readdir(p) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + mkdir: (...args) => { + const cb = args.pop() + window.fs.mkdir(...args) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + write: (p1, buf, cb) => { + window.fs.writeCustom(p1, encodeUint8Array(buf)) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + }, + realpath: (p, cb) => { + window.fs.realpath(p) + .then((data) => cb(undefined, data)) + .catch((err) => cb(err)) + } +} + +window.reqs = { + path, + fs +} + +function require (name) { + return window.reqs[name] +} + +require.resolve = name => name + +window.require = require diff --git a/src/client/web-components/web-store.js b/src/client/web-components/web-store.js new file mode 100644 index 0000000..65ab728 --- /dev/null +++ b/src/client/web-components/web-store.js @@ -0,0 +1,53 @@ +/** + * central state store powered by manate - https://github.com/tylerlong/manate + */ + +import { manage } from 'manate' +import initState from '../electerm-react/store/init-state' +import { StateStore } from '../electerm-react/store/store' +import loginExtend from './store-login' + +class Store extends StateStore { + constructor () { + super() + Object.assign( + this, + initState, + { + logined: false, + authChecked: false, + fetchingUser: false, + logining: false, + height: window.innerHeight, + _config: { + tokenElecterm: window.localStorage.getItem('tokenElecterm') || '' + } + } + ) + } +} + +Store.prototype.initMcpHandler = function () { + // Listen for MCP requests from main process + window.et.commonWs.addEventListener('message', (e) => { + if (e && + e.data && + typeof e.data === 'string' && + e.data.startsWith('{') && + e.data.endsWith('}') && + JSON.parse(e.data).type === 'mcp-request' + ) { + const { requestId, action, data } = JSON.parse(e.data) + if (action === 'tool-call') { + window.store.handleMcpToolCall(requestId, data.toolName, data.args) + } + } + }) +} + +loginExtend(Store) + +const store = manage(new Store()) + +window.store = store +export default store From 42284339b2e70aad1eba0b41ba439c60ff13ba68 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 12:43:07 +0800 Subject: [PATCH 02/52] docs: describe dev2 web variant (ArkWeb + ohos-node) Co-Authored-By: Claude Fable 5 --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index ab04bd1..af858bb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,23 @@ This project brings electerm to **HarmonyOS** using the [Electron Harmony OS runtime](https://gitcode.com/openharmony-sig/electron) (Chromium + Node.js). +> **`dev2` branch — web variant (experimental).** A second build path with no +> electron runtime at all, modelled on [electerm-android](https://github.com/electerm/electerm-android): +> the UI is an **ArkWeb** `Web` component and the electerm-web backend runs as +> an on-device **Node.js** process ([hqzing/ohos-node](https://github.com/hqzing/ohos-node) +> binary, started via `childProcessManager.startNativeChildProcess`). +> CI: `.github/workflows/build-web.yml` (push to `dev2`). +> +> ``` +> ArkWeb (frontend) ── http://127.0.0.1:5577 ──► Node.js backend (native child process) +> loads loading page serves UI + SSH/SFTP/telnet/ftp/RDP/VNC/Spice +> ``` +> +> The electerm app (frontend + backend bundle) is packaged in the HAP `resfile` +> and read directly by the node process; the node binary is packaged as +> `libs/arm64-v8a/libnode.so`. On-device boot diagnostics land in +> `/electerm-data/node-boot.log`. + --- [Huawei AppGallery](https://appgallery.huawei.com/app/detail?id=org.electerm.electerm) · [Apple App Store](https://apps.apple.com/cn/app/electerm/id6792971552) From 47e8462cd2e5db95722dd410dad52af1b624c5e1 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 12:51:14 +0800 Subject: [PATCH 03/52] fix: restore build/ dir, commit build/web sources, untrack local build artifacts - cdc36aa accidentally committed the whole build/ directory deletion and never included build/web/{build,vite.web}.mjs (CI failed: MODULE_NOT_FOUND); restored build/{bin,vite,web} + logos from electerm-android / transcript - untrack entry/.cxx (CMake/Ninja), entry/oh_modules, local.properties and ignore them going forward Co-Authored-By: Claude Fable 5 --- .gitignore | 5 + build/bin/.yarnclean | 45 + build/bin/build-common.js | 18 + build/bin/build.js | 19 + build/bin/clean.js | 7 + build/bin/copy.js | 56 + build/bin/gen-logo.py | 425 +++++ build/bin/install.js | 9 + build/bin/pre-push | 4 + build/bin/pug.js | 60 + build/bin/release | 10 + build/bin/run-prod.sh | 4 + build/electerm-logo-square.png | Bin 0 -> 44759 bytes build/electerm.png | Bin 0 -> 6080 bytes build/vite/common.js | 33 + build/vite/conf.js | 69 + build/vite/def.js | 5 + build/vite/dev-server.js | 200 +++ build/vite/diagnostics-channel-stub.js | 55 + build/web/build.mjs | 226 +++ build/web/vite.web.mjs | 68 + .../reply/cache-v2-49f5662a4b05781cba4a.json | 1407 ----------------- .../cmakeFiles-v1-e37c68776f2415d7fef8.json | 173 -- .../codemodel-v2-1ac40039f16ae038c893.json | 69 - ...ectory-.-Release-f5ebdc15457944623624.json | 14 - .../reply/index-2026-08-28T04-39-21-0293.json | 89 -- ...node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json | 155 -- ...launcher-Release-968f49e15038ddbf7844.json | 155 -- .../default/release/arm64-v8a/.ninja_deps | Bin 6144 -> 0 bytes .../default/release/arm64-v8a/.ninja_log | 6 - .../default/release/arm64-v8a/CMakeCache.txt | 421 ----- .../CMakeFiles/3.28.2/CMakeCCompiler.cmake | 74 - .../CMakeFiles/3.28.2/CMakeCXXCompiler.cmake | 85 - .../3.28.2/CMakeDetermineCompilerABI_C.bin | Bin 14168 -> 0 bytes .../3.28.2/CMakeDetermineCompilerABI_CXX.bin | Bin 14216 -> 0 bytes .../CMakeFiles/3.28.2/CMakeSystem.cmake | 15 - .../3.28.2/CompilerIdC/CMakeCCompilerId.c | 880 ----------- .../3.28.2/CompilerIdC/CMakeCCompilerId.o | Bin 3296 -> 0 bytes .../CompilerIdCXX/CMakeCXXCompilerId.cpp | 869 ---------- .../3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o | Bin 3320 -> 0 bytes .../CMakeFiles/CMakeConfigureLog.yaml | 386 ----- .../CMakeFiles/TargetDirectories.txt | 4 - .../arm64-v8a/CMakeFiles/cmake.check_cache | 1 - .../CMakeFiles/node_ctl.dir/node_ctl.c.o | Bin 4000 -> 0 bytes .../node_launcher.dir/node_launcher.c.o | Bin 11312 -> 0 bytes .../release/arm64-v8a/CMakeFiles/rules.ninja | 83 - .../arm64-v8a/additional_project_files.txt | 0 .../default/release/arm64-v8a/build.ninja | 194 --- .../release/arm64-v8a/build_file_index.txt | 1 - .../release/arm64-v8a/cmake_install.cmake | 54 - .../release/arm64-v8a/compile_commands.json | 14 - .../arm64-v8a/configure_fingerprint.json | 1 - .../arm64-v8a/hvigor_native_config.json | 1 - .../arm64-v8a/metadata_generation_command.txt | 17 - .../release/arm64-v8a/native_work_dir.txt | 1 - .../default/release/arm64-v8a/output.log | 2 - .../release/hvigor/arm64-v8a/summary.cmake | 0 entry/oh_modules/libnode_ctl | 1 - local.properties | 2 - 59 files changed, 1318 insertions(+), 5174 deletions(-) create mode 100644 build/bin/.yarnclean create mode 100644 build/bin/build-common.js create mode 100644 build/bin/build.js create mode 100644 build/bin/clean.js create mode 100644 build/bin/copy.js create mode 100644 build/bin/gen-logo.py create mode 100644 build/bin/install.js create mode 100755 build/bin/pre-push create mode 100644 build/bin/pug.js create mode 100755 build/bin/release create mode 100755 build/bin/run-prod.sh create mode 100644 build/electerm-logo-square.png create mode 100644 build/electerm.png create mode 100644 build/vite/common.js create mode 100644 build/vite/conf.js create mode 100644 build/vite/def.js create mode 100644 build/vite/dev-server.js create mode 100644 build/vite/diagnostics-channel-stub.js create mode 100644 build/web/build.mjs create mode 100644 build/web/vite.web.mjs delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.ninja_deps delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/.ninja_log delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt delete mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake delete mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake delete mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin delete mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin delete mode 100755 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_launcher.dir/node_launcher.c.o delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/additional_project_files.txt delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/build.ninja delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/build_file_index.txt delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/cmake_install.cmake delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/compile_commands.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/native_work_dir.txt delete mode 100644 entry/.cxx/default/default/release/arm64-v8a/output.log delete mode 100644 entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake delete mode 120000 entry/oh_modules/libnode_ctl delete mode 100644 local.properties diff --git a/.gitignore b/.gitignore index b538d3f..b0a0183 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,8 @@ entry/src/main/resources/rawfile /entry/src/main/resources/resfile/ # node runtime download cache /.cache/ +# hvigor native (CMake/Ninja) build dir & ohpm installs +entry/.cxx/ +entry/oh_modules/ +# generated local SDK paths (written by scripts/build-web-app.sh) +local.properties diff --git a/build/bin/.yarnclean b/build/bin/.yarnclean new file mode 100644 index 0000000..f7b5e72 --- /dev/null +++ b/build/bin/.yarnclean @@ -0,0 +1,45 @@ +# files +Makefile +Gulpfile.js +Gruntfile.js +.tern-project +.gitattributes +.editorconfig +.eslintrc +.jshintrc +.flowconfig +.documentup.json +.yarn-metadata.json +.travis.yml +appveyor.yml +LICENSE.txt +LICENSE +AUTHORS +CONTRIBUTORS +.yarn-integrity +*.md +*.ts +*.jst +*.js.map +*.ts.map +*.coffee + +# folders +__tests__ +test +tests +powered-test +docs +doc +website +images +assets +example +examples +coverage +.nyc_output + +# ignores +!*.d.ts +zmodem2/dist/cjs +zmodem2/dist/browser \ No newline at end of file diff --git a/build/bin/build-common.js b/build/bin/build-common.js new file mode 100644 index 0000000..e5b24bb --- /dev/null +++ b/build/bin/build-common.js @@ -0,0 +1,18 @@ +/** + * common functions for build + */ + +import { exec } from 'child_process' + +export const run = function (cmd) { + return new Promise((resolve, reject) => { + exec(cmd, (err, stdout, stderr) => { + if (err || stderr) { + return reject(err || stderr) + } + resolve(stdout) + }) + }).then(console.log).catch(console.error) +} + +export const cwd = process.cwd() diff --git a/build/bin/build.js b/build/bin/build.js new file mode 100644 index 0000000..eefd28b --- /dev/null +++ b/build/bin/build.js @@ -0,0 +1,19 @@ +/** + * build + */ +import pkg from 'shelljs' +const { exec, echo } = pkg + +echo('start build') + +const timeStart = Date.now() + +// echo('clean') +// exec('npm run clean') +echo('js/css file') +exec('npm run vite-build') +echo('copy file') +exec('node ./build/bin/copy.js') + +const endTime = Date.now() +echo(`done build in ${(endTime - timeStart) / 1000} s`) diff --git a/build/bin/clean.js b/build/bin/clean.js new file mode 100644 index 0000000..7824396 --- /dev/null +++ b/build/bin/clean.js @@ -0,0 +1,7 @@ +import pkg from 'shelljs' + +const { rm } = pkg + +rm('-rf', [ + 'dist' +]) diff --git a/build/bin/copy.js b/build/bin/copy.js new file mode 100644 index 0000000..b75b6b6 --- /dev/null +++ b/build/bin/copy.js @@ -0,0 +1,56 @@ +import { resolve } from 'path' +import pkg from 'shelljs' +import { cwd } from './build-common.js' + +const { cp } = pkg + +const f1 = resolve( + cwd, + 'src/client/statics/*' +) +const from0 = resolve( + cwd, + 'node_modules/electerm-icons/icons' +) +const from1 = resolve( + cwd, + 'src/app/views' +) + +const t1 = resolve( + cwd, + 'dist/assets/' +) +const to1 = resolve( + cwd, + 'dist' +) +const to2 = resolve( + cwd, + 'dist/assets/icons' +) +const arr = [ + { + from: f1, + to: t1 + }, + { + from: from1, + to: to1 + }, + { + from: from0, + to: to2 + } +] + +for (const obj of arr) { + const { + file, from, to + } = obj + if (file) { + cp(from, to) + } else { + cp('-r', from, to) + } +} diff --git a/build/bin/gen-logo.py b/build/bin/gen-logo.py new file mode 100644 index 0000000..35ef99e --- /dev/null +++ b/build/bin/gen-logo.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +""" +Generate ALL Android launcher icons, splash assets, and related XML from +two source images: + + build/electerm-logo-square.png (2160x2160 square logo -> all icons) + build/electerm.png (766x266 wordmark -> splash screen) + +Usage: + npm run logo + +The square logo may have a solid background — it is auto-removed by +detecting the corner colour (with anti-aliased edge handling). +Already-transparent PNGs are used as-is. + +After updating either source image, just run `npm run logo` to regenerate +everything in build/android/res-overlay. +""" +import os +import sys +from PIL import Image, ImageChops, ImageDraw + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +LOGO_SRC = os.path.join(ROOT, "build", "electerm-logo-square.png") +WORDMARK_SRC = os.path.join(ROOT, "build", "electerm.png") +RES = os.path.join(ROOT, "build", "android", "res-overlay") + +# --------------------------------------------------------------------------- +# Brand colours +# --------------------------------------------------------------------------- +BG = (21, 23, 26, 255) # #15171a — electerm dark slate (splash background) +BG_HEX = "#15171a" + +# Launcher icon background — matches the solid brown background of +# build/electerm-logo-square.png (#534741), so the rendered icon +# reproduces the source square logo instead of showing a black/ +# dark-slate background. +ICON_BG = (83, 71, 65, 255) # #534741 +ICON_BG_HEX = "#534741" + +# --------------------------------------------------------------------------- +# Density maps (108dp canvas for adaptive, standard sizes for legacy) +# --------------------------------------------------------------------------- +FOREGROUND_DENSITIES = { + "drawable-mdpi": 108, # 108dp @ 1x + "drawable-hdpi": 162, # 108dp @ 1.5x + "drawable-xhdpi": 216, # 108dp @ 2x + "drawable-xxhdpi": 324, # 108dp @ 3x + "drawable-xxxhdpi": 432, # 108dp @ 4x +} + +LEGACY_DENSITIES = { + "mipmap-mdpi": 48, + "mipmap-hdpi": 72, + "mipmap-xhdpi": 96, + "mipmap-xxhdpi": 144, + "mipmap-xxxhdpi": 192, +} + +# Logo size as a fraction of the icon canvas. +# Adaptive icon safe zone = 66dp / 108dp ~ 61%. +# 60% keeps the logo comfortably inside the safe zone on all launchers. +LOGO_FRACTION = 0.60 + +# Splash wordmark height (pixels). +SPLASH_LOGO_HEIGHT = 200 + +# Background-removal parameters. +# BG_TOL: pixels within this Chebyshev distance of the corner +# colour are fully transparent. +# BG_GRADIENT: distance at which alpha reaches 255. Between BG_TOL +# and BG_GRADIENT alpha is linearly interpolated, which +# preserves smooth anti-aliased edges. +BG_TOL = 30 +BG_GRADIENT = 100 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +_logo_cache = None + + +def ensure_dir(p): + os.makedirs(p, exist_ok=True) + + +def paste_centered(canvas, img): + """Paste *img* onto *canvas* centred, respecting alpha.""" + cw, ch = canvas.size + iw, ih = img.size + left = (cw - iw) // 2 + top = (ch - ih) // 2 + canvas.paste(img, (left, top), img) + + +def make_circular_bg(size, color): + """Circular background with anti-aliased edges (4x supersampled).""" + scale = 4 + big = size * scale + canvas = Image.new("RGBA", (big, big), (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + draw.ellipse((0, 0, big - 1, big - 1), fill=color) + return canvas.resize((size, size), Image.LANCZOS) + + +# --------------------------------------------------------------------------- +# Source loading +# --------------------------------------------------------------------------- +def remove_background(im): + """ + Detect the solid background colour from the four corners and make it + transparent, with a smooth gradient at anti-aliased edges. + + Uses PIL ImageChops (C-level operations) for speed — no per-pixel + Python loops over the 4.7M-pixel source. + """ + w, h = im.size + + # --- detect background colour from corners --- + corners = [ + im.getpixel((0, 0)), + im.getpixel((w - 1, 0)), + im.getpixel((0, h - 1)), + im.getpixel((w - 1, h - 1)), + ] + bg = tuple(sum(c[i] for c in corners) // len(corners) for i in range(3)) + + # --- compute Chebyshev distance from background --- + # ImageChops.difference gives |im - bg| per channel. + # ImageChops.lighter gives pixel-wise max => max(r, g, b) distance. + bg_img = Image.new("RGBA", (w, h), bg + (255,)) + diff = ImageChops.difference(im, bg_img) + r_d, g_d, b_d = diff.split()[:3] + max_diff = ImageChops.lighter(ImageChops.lighter(r_d, g_d), b_d) + + # --- map distance -> alpha via LUT (fast C-level point op) --- + table = [] + for d in range(256): + if d <= BG_TOL: + table.append(0) + elif d < BG_GRADIENT: + table.append(int(255 * (d - BG_TOL) / (BG_GRADIENT - BG_TOL))) + else: + table.append(255) + alpha = max_diff.point(table, mode="L") + + # --- replace alpha channel --- + r, g, b = im.split()[:3] + im = Image.merge("RGBA", (r, g, b, alpha)) + + # --- crop to content --- + bbox = im.getbbox() + if bbox: + im = im.crop(bbox) + return im + + +def load_square_logo(): + """ + Load build/electerm-logo-square.png. + + If the image already has transparency it is used as-is (just cropped + to its bounding box). Otherwise the solid background is auto- + detected and removed. + """ + global _logo_cache + if _logo_cache is not None: + return _logo_cache + + if not os.path.exists(LOGO_SRC): + sys.exit("ERROR: square logo not found: " + LOGO_SRC) + + im = Image.open(LOGO_SRC).convert("RGBA") + print(" Loaded square logo:", im.size, im.mode) + + # Detect whether the image already has meaningful transparency. + extrema = im.getextrema() # [(r_min,r_max), …, (a_min,a_max)] + has_alpha = len(extrema) > 3 and extrema[3][0] < 255 + + if has_alpha: + print(" Image already transparent — using as-is") + bbox = im.getbbox() + if bbox: + im = im.crop(bbox) + else: + print(" Removing solid background…") + im = remove_background(im) + + print(" Final logo size:", im.size) + _logo_cache = im + return im + + +def get_logo(max_size=None): + """Return a (optionally scaled) copy of the processed square logo.""" + im = load_square_logo().copy() + if max_size: + im.thumbnail((max_size, max_size), Image.LANCZOS) + return im + + +def load_wordmark(height): + """Load build/electerm.png and scale to *height* pixels.""" + if not os.path.exists(WORDMARK_SRC): + sys.exit("ERROR: wordmark not found: " + WORDMARK_SRC) + im = Image.open(WORDMARK_SRC).convert("RGBA") + w, h = im.size + new_w = int(round(w * height / h)) + return im.resize((new_w, height), Image.LANCZOS) + + +# --------------------------------------------------------------------------- +# Generators +# --------------------------------------------------------------------------- + +def gen_foreground(): + """ + Adaptive icon foreground (108dp canvas, logo inside the 66dp safe + zone). Generated at every standard density so Android never upscales. + + Also removes the old single-density foreground that used to live in + drawable/ (432px in drawable/ was treated as mdpi = 432dp, 4x too + large for the 108dp adaptive-icon canvas). + """ + old_fg = os.path.join(RES, "drawable", "ic_launcher_foreground.png") + if os.path.exists(old_fg): + os.remove(old_fg) + print(" Removed old single-density foreground:", old_fg) + + for folder, size in FOREGROUND_DENSITIES.items(): + out = os.path.join(RES, folder) + ensure_dir(out) + canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + logo = get_logo(max_size=int(size * LOGO_FRACTION)) + paste_centered(canvas, logo) + canvas.save(os.path.join(out, "ic_launcher_foreground.png")) + + +def gen_legacy(): + """ + Legacy (pre-26) launcher icons. + + Both ic_launcher.png and ic_launcher_round.png use a CIRCULAR brand + background with transparent corners, so the icon looks round even on + launchers that don't mask adaptive icons. + """ + for folder, size in LEGACY_DENSITIES.items(): + out = os.path.join(RES, folder) + ensure_dir(out) + bg = make_circular_bg(size, ICON_BG) + canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + canvas.paste(bg, (0, 0), bg) + logo = get_logo(max_size=int(size * LOGO_FRACTION)) + paste_centered(canvas, logo) + canvas.save(os.path.join(out, "ic_launcher.png")) + canvas.save(os.path.join(out, "ic_launcher_round.png")) + + +# Adaptive icon XML — both square and round reference the same +# foreground/background; the launcher's own mask is applied on top. +ADAPTIVE_XML = """ + + + + +""" + + +def gen_adaptive_xml(): + out = os.path.join(RES, "mipmap-anydpi-v26") + ensure_dir(out) + with open(os.path.join(out, "ic_launcher.xml"), "w") as f: + f.write(ADAPTIVE_XML) + with open(os.path.join(out, "ic_launcher_round.xml"), "w") as f: + f.write(ADAPTIVE_XML) + + +def gen_splash(): + """Splash: brand background + centred wordmark.""" + drawable = os.path.join(RES, "drawable") + ensure_dir(drawable) + logo = load_wordmark(height=SPLASH_LOGO_HEIGHT) + logo.save(os.path.join(drawable, "splash_logo.png")) + + with open(os.path.join(drawable, "splash.xml"), "w") as f: + f.write( + """ + + + + + + + + +""" + ) + + +def gen_values(): + """ + Colours + styles + network security config. + + Written as SEPARATE files (colors-electerm.xml / splash-styles.xml) + so they merge with Capacitor's generated resources instead of + overwriting them. + """ + v = os.path.join(RES, "values") + ensure_dir(v) + with open(os.path.join(v, "colors-electerm.xml"), "w") as f: + f.write( + """ + + """ + BG_HEX + """ + """ + ICON_BG_HEX + """ + +""" + ) + with open(os.path.join(v, "splash-styles.xml"), "w") as f: + f.write( + """ + + + +""" + ) + xml = os.path.join(RES, "xml") + ensure_dir(xml) + with open(os.path.join(xml, "network_security_config.xml"), "w") as f: + f.write( + """ + + + 127.0.0.1 + localhost + + + +""" + ) + + +def gen_manifest(): + """ + AndroidManifest.xml overlay (full file; copied over the generated + one). Includes android:roundIcon so tablet launchers that look for + a round icon get the electerm round icon. + """ + with open(os.path.join(RES, "AndroidManifest.xml"), "w") as f: + f.write( + """ + + + + + + + + + + + + +""" + ) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +if __name__ == "__main__": + print("=" * 60) + print(" electerm Android — logo & splash asset generator") + print("=" * 60) + print(" Square logo source:", LOGO_SRC) + print(" Wordmark source: ", WORDMARK_SRC) + print(" Output directory: ", RES) + print() + + load_square_logo() + print() + + print("[1/6] Adaptive icon foregrounds …") + gen_foreground() + print("[2/6] Legacy launcher icons …") + gen_legacy() + print("[3/6] Adaptive icon XML …") + gen_adaptive_xml() + print("[4/6] Splash screen …") + gen_splash() + print("[5/6] Colours, styles & security config …") + gen_values() + print("[6/6] AndroidManifest.xml …") + gen_manifest() + + print() + print("Done! All assets generated in:") + print(" " + RES) + print() + print("To apply them to the native project, run:") + print(" cd build/android && npx cap sync android && npm run overlay") diff --git a/build/bin/install.js b/build/bin/install.js new file mode 100644 index 0000000..c832a83 --- /dev/null +++ b/build/bin/install.js @@ -0,0 +1,9 @@ +import pkg from 'shelljs' + +const { echo, rm, cp } = pkg + +echo('install required modules') + +rm('-rf', 'src/client/electerm-react') +cp('-r', 'node_modules/@electerm/electerm-react/client', 'src/client/electerm-react') +echo('done install required modules') diff --git a/build/bin/pre-push b/build/bin/pre-push new file mode 100755 index 0000000..eddf067 --- /dev/null +++ b/build/bin/pre-push @@ -0,0 +1,4 @@ +#!/bin/bash +cd `dirname $0` +cd ../../ +npm run lint \ No newline at end of file diff --git a/build/bin/pug.js b/build/bin/pug.js new file mode 100644 index 0000000..b58b756 --- /dev/null +++ b/build/bin/pug.js @@ -0,0 +1,60 @@ +// build html +/** + * build common files with react module in it + * + * Generates a static dist/index.html from the pug template, injecting the + * same data the runtime server (src/app/lib/view.js) provides to the client. + * Mirrors upstream electerm's build/bin/pug.js, ported to ESM for this project. + */ +import fs from 'fs' +import pug from 'pug' +import { resolve } from 'path' +import deepCopy from 'json-deep-copy' + +const pack = JSON.parse( + fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8') +) + +const entryPug = resolve( + __dirname, + '../../src/app/views/index.pug' +) +const targetFilePath = resolve( + __dirname, + '../../dist/index.html' +) +const pugContent = fs.readFileSync(entryPug, 'utf-8') +const defaultAIPreset = { + baseURLAI: 'https://ai.electerm.org/api/ai', + apiPathAI: '/chat/completions', + modelAI: 'mistral-small-latest', + authHeaderNameAI: 'Authorization: Bearer', + id: 'ai.electerm.org', + nameAI: 'ai.electerm.org(default free)' +} +const supportSessionTypes = [ + 'ssh', + 'telnet', + 'web', + 'rdp', + 'vnc', + 'ftp', + 'spice' +] +const data = { + version: pack.version, + siteName: pack.name, + isDev: false, + cdn: '', + tokenElecterm: '', + defaultAIPreset, + downloadUpgradeFromBrowser: true, + versionFile: 'version-android.html', + supportSessionTypes +} +const htmlContent = pug.render(pugContent, { + filename: entryPug, + ...data, + _global: deepCopy(data) +}) +fs.writeFileSync(targetFilePath, htmlContent, 'utf8') diff --git a/build/bin/release b/build/bin/release new file mode 100755 index 0000000..f24d0c2 --- /dev/null +++ b/build/bin/release @@ -0,0 +1,10 @@ +#!/bin/bash +cd `dirname $0` +cd ../.. +git co main +git pull +git pull +git delete-branch build +git create-branch build +git push origin build -u +git co - diff --git a/build/bin/run-prod.sh b/build/bin/run-prod.sh new file mode 100755 index 0000000..6ad994b --- /dev/null +++ b/build/bin/run-prod.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd `dirname $0` +cd ../.. +NODE_ENV=production node ./src/app/app.js \ No newline at end of file diff --git a/build/electerm-logo-square.png b/build/electerm-logo-square.png new file mode 100644 index 0000000000000000000000000000000000000000..21d3124b9c2cc75fb036df85a5575639ae98275c GIT binary patch literal 44759 zcmeEuWmweP_wS|zQE{*(WCT2jN{OVzAR&r`fP$n!clW?xq8!pKLGj_b?U~cDRZfk?bcD!-Z*4ast9Vp$pg0-Ep^1mC~IR0T2u#C^`h8>>( zFF&8PHFhuZY)2<`Gi__Lj4?v5nIOIZ1Z-gxAR2NL=B}X#p`=ArS#NVHpJh0Rb@)k<)U5atKAt$<{&B*4FAD4Nx<)b+UCdv$ey^Y6#ugxr?!pf`hF!p3Gly^Z(&|0Wnd9bHV~Zx#$_}-ZJv?qVlI@&z+N1;6HOtMvVQ> zTBH9Lp78-Oe3)|lAIkF25NIIw>%XcGfBb7a#x`Iv4qzG{0asKIN}#L1iolnV#4K7s-_b<#k88&B}Z)V<}Zy$vvjR9 z#s`BcSM?U1f4qv?u(v6Cc*$#}ysQYbdW5=*!p>~{MzZCKBM%{J85;60c)Al?12Sq9 zk~R5c&@s{09yoF#ZRi{{sgYK>80H{09y|4gO2G{{si02LFMB{~y4?-9Sc=_0&R? z@cB3K!y&D$t?|RdH+HGi)Ge)Lf@gi^PM;N# z7LoQz7xR!j+fjXljlp}pn09h1%VW>2ZLy}NMVrYB780H9?Vg2;R@$Dm+f<|2x)@e( z2#2uwY;ao#T*xgTtxNt zSf1W*DK9td@nIoaoDt7T;ZL;POx5b0?k&sncXe@bb&cF9eS|pQ*XJ@=*Zb?2scQcL zM2cFXHQ|7*m-QnK?GadzOzFa^rr#Ojx%sIbbe&FTCd>vI)!(E%d{YzE``dS;fbhB(a3*CGBJWNLy5~hDf zBjk-`ueV&m;S{%zh; zB-yY%m#Ab&f!rRGPiHHitE&21t;8J6Yip3}<)B}XA|)VjxIh+0Zf}lZCTkA?;-MigBf{E3YYz7_q`S!#0GZpT)e(SpzX1T79yhw;zUmr2` z$#dyXD=Cr17DkaP*S5udeSK~B<|~WVe@VCI?VUQlgKD$)(qD@dzIW{mkKSeeLhM?6 z;^&&seD!2=T4ci2aSL6~Bh+ge z`<@grS(v7}7m+wlV>NOX|NP8xkAnvnM;g{#%;bja@?1S~SHJBIHgKD0J?|p9G$flB z+O<%y{t}N6TRB!|x6`D5{Aguiv@7+^%=TR&+2=;*H`9vhTD_RaEYiMcy?YxqanzDvNLDhSO09Lj{kxSX{b94oq1k(tlBo@+O z6mbaejuA-WeAbd_ePNjNY~#U!=g;altlRr7Da!_bxh`0@J9ITyhq>6a>*(s9q(cEy zJE0XpU{_L7<>MXJ62Jaq)%3N~NWJ`Cy5yDIT>afPU8(jZ>tQE$k9G0E^f>bL*TZUR z{MV;`)KlEqw)04RtnCGUp(}gMXr4TI;xCo%=-8jTDsr_j%{B^;2)6~Wd)xCJ+PN^S zt1fP5w<+_Wlm6@hbIQy6=snZ9a#gNUD7H2g$luxROn_H z)(`TluT#XUbtF4YrO0wrL?10lO-&Q>eVZ{kp4IYVb6RzAZJ{aHP@M|-)yW>hPWa(* z9`8#?mij7tKbV#2@_mNN^6CcaCBpH2Z5g*#3yRjGtQT!;@Mr>R-x}9udAC|{IiganOuXcUhShxLkg9iDCQI?q`jDE%tHf06WT;}Ic5YQBM8?`gE@ zq?GZSZ+{;Wbmc6~grDnlw=SbF2XwpMfY2+=HfA0kv@GQ&^zXIL+8_?`CZ8yX8|ks-ZJ z1y>TaIqDxt9J#=aNG~yWMfft(_Hp&q_MmT5kSD>=>pQv#r*i@ynk~e?W`ej z`517t_ZD_`sfQmM-E#&OC$sa1rR(Gi(1y+}*%CJIRq4&;*(lbZMTWaj_bdNQ%!34= z`Ub)K{!EfCU)~1ZD$A;@JxJZ%tH~-lWmuFw8o9ZAMOO9>6Z%{yh*7~(Y1`%9_jCHz zUzbiWi;43{>f=an(ifYwCRWN>!%nu`J9iYQc!SB6wA2u<6l}jY_U{PMoXzNBo2Qg^ z9Y;KuV%U5(^Cq-*BbHs536(yHEO=a|5`M*>AvIBLUfGj9^i zndLsFR5w?Tdg6a1Z+`x?m+MK`nRiNA6StAvxhCIri~*ZVd7IW&R+?03Y3Pk0dFN97qmVWP%WcQ53) zyTi;>wd3h4i$$x;5w0v-97`sPYo)X6@?RvrQKp0Y6k zz^he^u{a@@eI)IL?@6>iQ{%P;;G;l2t+7vjtN_aup0bMEs7N2MO-mg+jy@$6CQV3B ztPF2%wDA2(Jc7tFKe>GU{9#F(a|$N!@o2gyBzD1TGt*Y8U~oG(!iSKlNv*H1S9p3@ z#J+9kS$e{ng@wh+ywAp}Pv?BY%8G{&!i$lovEPo5FR4&ai4?FLqh35up@`#?SR5Pg zS{u<&mEfg8oJTP}+|{^vv7-Bi@!nXKFi~d@6`M#r+CK0j{zTE*SD%jg1_+?AV?5?Q z@GQF%seCu?Y*P&tcKq@@9wIdzWx&r!;!1pbk-g^AUjRFP!Ibc`fLT|}_4@Q-4U?(~-8RtW&?}&>C6k?*_P>~bGoP?iyT7M8I($zDJmxM=N-`k{rt-hHg#=g(hp+}0Em`na#c0oyrS2NtdUdecX^K9%wt4rh{f*HN)a!XuVtGweRQohDl?+Y)m$Osjd1kHu|6WrG{a%!P{|leJiX@&VWP1g}vdlJNM< zESGAOkacr-kM}zZ^y6IbUnVkbpxa)v?Zaj;{_VNe{-qb!Q&2z8vRVamd2$L5%?m4hYke>ynLBCl6n$g zf(uw`$hveeX1)=y`A_n_-44J+ixPf+3KC$pHvxA0}S zi;9x!L2A-*`g`Z33wl?J)|-BTEW;hj`4HNp)?)K3wl+eVxX=L>YJqS&D1AS-4(hAv z)lbr95i5DFAp8+bpo%QlT3h$V*1QsP_1=`Z(}eKblq1Aqbr&NI*L&t|3zpwAA)KM! z%ntB8K5B4053|^vI%dzs&eis=wV|k@Uq63pircA;6_o%v zAXwt`@}PmoZ+A8+M>NYf$0Enl!a+Tk~U5 zUwgGISOcz`UZvemO&KLR*pc)rceB#9aDR&r4k_;027>P)K!|*PIL)+17Z|9zi`1uS zXt>#(`H}fV1>`%LT3TWJK1l<~HmvkJitHOtVx*rM0W3Z(WY%BaWUmEd1S8#zriS1&oKs zT$Vsg%B{L8_Z=qHhT3XBFYh98&H)6p(aU= zMMOx%fG|$E__|$V_V%;^oFGsE>I(`ab$<*ejF z4m7bGWf)I^K9yi=6X@T+e|lKjzUIrZ5{18=F47ZH&8^ISX58{LmR|;VZ)s^kE%vwL zRsB~jnd3fdMLsVm(apPO2b#UAbfHlNKL;VHfga$3$}D03G%HoooWO)Aa&-|Dw4gBZHomon2k9*3&viT6I^OCc;0$RQ9gSFiB?iF9(4M_v~edau4*6;9Nj{k$GFIyyrA z@Jtvzk|VpN-r&GM0rQnA8l~XH;dZbN&N|3or)pS1L0k25l2*E9WORa*Y0&HMAgRyO zLme6IYhNo(sv?4e!eX}(p8ZT){j|%n#R_f$IYFb}0c`8Xm~3w0=%^uj@Y|NB{^mF@ z;$w&-8BNvisMSikrk|6!mZ4>End3lyQM}inrJ+k16EaiAc>MHYyUTLg0N<-TGjm9t zAmH5*G3d&(*Ov!fNK=)JS4a#wee3Fc@g3I=?=T=t=LvQg$8kWG#r6Gr->ax9&kr%; zr@;yPG^}KRBDhM=q;QSiYVD9$viOkTv+#yZkpg`zIVu1tHV+Svb^Tn(j^K~SdF@Nw zNu^pE`UUfa%HSN>Ix;gpeYzZwo#NDAE+o`cls_5^PWmK6NbZ{sKeJ_BOCwR`eIqIR13dK=^yG!HFJy$9CTCku6_%cOx+Mq`Ma(aIhk-@uI> z;5DpEQ2oS&9*#pC)V%uKR!?_&fH^rKke@eutO8ceekO*&ao;hE=F9;9J;-xS*s}9W zN4|s*+VeAkuPFV-cqrtxMQc4fEI1P#X-C&fASgxz64hbBOUgk|e zf4&65E|T0l=0$XhTuRzzxY)WrD%?~8N3g(Fm`HtW(6tImigN+klZ@&LWsCEVRSxj# zb!6%nGNJA9Nl7m4>xaYCW_mtT~y8xsWN}qa~GB^&ys42lNp1CfJ=h>Fzf+VpLi-p~#tNIShAl`h4r+|B2V)|CP z+?&(bc%i|R_nN)>WKSWsE_7sMRm3_@FKs1;HcLcUnEs1B-TU_j&LszvbCxKz9#E0^ zSx8FYePU9GmkC$lpvzA#AzrxRzLw`N(m^lp?l zFD`c_nhE{D!Y-sb0HEZ#62k>tBbnbJ&c7jrnW~``(R|*OWlwDKrMit%kn~*4nF}2) zT=~j(Mo0)WudEuJ3{Dj)0lKL@Z!hgNX=+@9;11DfY0!nwk8OZ(hV>X!}#(2{i@PA=XdF} z(nJ-Fd>xGPgg^o2)r-M%zss!j^amsCdctWQr#U(_B0pE!g~=|x92+A~c)V-+b2DTX zGJd(QYxH>w26B@->^?F#Qhom9;KzV)viF}H$^fSX6#_*HTniI3lgq@@@H`Xek0d2(7vwnn z+HmdKfA>{JPorR{v{sgFfg!B(z}A_GJJl{P?>Lw&8lUpUvxchllWCNEfb!lNBEp2SH@b@cf%c0IZC;Kz3aCu^q|U4yKN3E7V{X&H28yC{Pn2dopj zZTj6Z@SS~k13B2X=-V)>^tVi$N8g&=tC zsrsOl>}FBhn_MvSXdkk3BNIFIvt+kW(J&!AcXhX=9DciykdS2htyN&nM0<827CjX; z0Tl-u5ZC9m^y-I9P9QsNSP5+mX4NroCt&0IdCSFu^-LztJKj{=B3+urT-TQ#02^;$ zYKgi74o3MT!PQIJ99pE)BO2 zE_AU_OR@T{%lznRNoApuNN`d z$sG;}3D$EScaAFh&aH>0j&eg1iUK}k6_f;@&9j#DECtUts8l4z_ll#>56FB0{+(3a zH0r9~fFVQ7)j@C-0i?{Ezc!Mdeg^EC&yejHj55@+=2JXV^EW6^74N;oin^&VrghVe zjE=}7#dDRaQq}j0{)|HT{PqqXiqBnM#?P@GN&bCb4AB$?nQ+8rxm+H*KX|Tz0%86i zA+JQJr_Aydupioak%ExwqrA1K|VivGyOB923>Ev~U+h%~9kG`%Ex4i;J#PFx+ZbuEVjUCy? zWM(Sd^|NT_-JDC4{s{Y!8wM;g&hs*i$gKb=2lboRKJG)D z%v*OVi_SJ2&i`8Jgz&9fj1?7*jc``UTk`^1PyC@(8FDCd)10%Cs2oH4Sj8bs$mfRr z1!gI@WpI8Q|!%cT$ zH~q|brm^KQ`g|VpCATl-vBQXP`p*_?FOwv4WMyLD@)CbY<^#^9r`6U`knWNd5Jk(s zuZ?WU%T9(#!dsJg_RvuS+g|)1BSPs`ecuIo;ikS=_6%>4?x=bXqyf62MSEUg_JK(J zLv;y@bFH&5=`uEn#1Z#F98ZbP_dZvRj0g?=K!vuGxr_*I`wr}{*3G{Ji?@tRZr~}y zj|$MzzpD~Y!Hp%zeA#o&Mdc~okI=TvO2gD1&VR?z7xUqVT!R_bv9RSgGE)hn#>S?6 zy6&k!G`W}#nM3CF;q5Bw6!_vT;n1c1U3G~o9tB+AQmngCL7GUBe@g;i_&5X|`-h`` z0At8?WPISeSW`b#ciQ#$vjtXqxRd~S8$~1YVRLrE+y>=)K4-4|Xb*-k;IfJ#d3F!a z!tRH*c9*-TU^4J}uSz2Yi5iP{VC%OJ@Jb#%%XOZ1t5AYOZ$sUc5|#0sff;44s1L1S7Nrs((9F~ zPtBFhd$~2cH?tv{5IaWqiiQ{s_C3O#AK<&jONplEKC4qgjs+!EJA*#uCfA(^M$xKAaC##2~=qM zi=-x41N0d@;nvEpPJcXBbm8J7(`b^g`S371t|fni^=2#hMQ^C&feY1}I<*~fM%VRn ztu+*Nw0;qK_rx;}O_qPRleIuV_?mTz_UQ5poW5x5tu`$);Tr;Sc-dO(Cai2-l zsVZP3j)FuNT4dD9k#__PN>o(z85H}zLH8yqZ|%dEr}8(}Bn3~Owy?C!xMwtAB-Eqt zp>UtBoP(p}3VmFvG1t++ZDUA-h-iyjpk{CLx?a2A(GHB+M2hN(6zqtY(E#YgHkpJCL&OFj0g%8 zcAW5HM1DjAM|xy`3P*QJ>IOKHl}mjFo}-=H3HWYfZ6tTKw(~1dX;pf@v=@k|l#mC> ze2|s-Ew5!Dm~Lexw|Vk#NR67W5tIf_Qo^2=o#()a1Zpg+5uR5$TzoxhqrWWBK%-4m zmc~rV#P4%IHAT1VU#d|dk&!V}=u?#jC&HORhf#VqN9ldFLzhZ)#zt#-Iiqj(RSgXe z%F@EI@bKxN*d)VQE^g#^4OEQe23^}9B1NtbglN#yPH4R{;(4pWvk27aW(ah!@KV&2 zzI#=~)*A(-Up%Y-ko*8cGi=ZFJUnvgN;5>d-1P7ET_&dJ++7kfa=RAGt8d`hzaKVN z#x@uI5fk10O}EH>r8UZ?U+7(RjOj6kk=+D6&+!+*@`^{EJlVxwnlYR<2u4r^L6(rn zEM)UVq-e7j^2OIPhCYK4L@DZkzLAj;Iee02#^83wXJzfSh1}=#!o(XgtSS8Td6#L> zr;|{fs;Uhyx;{K(RzL;7bwo0d{zC0Km*1jl_h6*=#Rq-UGfI4ZxE68eVRh((@6BU- zK}kB)2WIYf89Mg4^X*-adqV5Sz8;L6^+lZ?v%A310gO3Gr@Wo%>-B$d?kHYho0U~Rz)~nup{1_k+v(r29be*jPtGoza!3i$&o%#(JmCOJYpz)laVk2gN4H&bAIBM@@(X2lY?c}7Y#Kc>Ge!QEQJV~^M&h9<=h`0C!cq~yD&7 zYHfa6xqijpfCxHE1A#L>9=zMwj#^1bO9YPD_2ff~g0OFXOyBG4yvVnjhuo8^P~J;T zT2qx0|4i#KH$ohBEo9q~<=XRVj5z-So>{Q16vJzE?MoyijaJ3W2Q z#Alu3TB{g0x9?(6RA9)9mCl4+AO`i$RTUQ(75voTW>#kA`1kSLLbjF4+&D@^~Hq%U7-Sk`GFJ{_#Tnz=Z7Kk;;$U zpU-;?6|-s?rdtdyScCI2he~%xDL23s4fSJ?bhbd!`8X*z)6+ll3hl-K`yNQzxVbO8 z<;YqqSKn#Ryn6L{SJ9`_PI?D}(X-M@zCyd%46Osfb72yh=5F z+)tmT*C*M!j=c?KVh-~O@?R^W#fY{#;MU<3dMY0|q3KnfK$-Qh6qX!i)lzUGeE~!c!YizuJ)ukpT;jSr5u9fp_ zZB)rE-=Go$Y+`Ox*EMeSLaOWmjX|uHsl+GI)!5kAOUd#M+Bywu%>xC&yIsc` z#fpfl`T3dF8^&v#q(eEsF9 zw+OF*2?l;XnXNrHXGgfRTiCMnb!y-n!L(E-n6ZUz{p2C^!XpB(&_iVS3ZFn^z5=cs zEA|6EJtTB$Ds-9rtOwznY3>xKUcLonlB@JU z1Vz?mihtMp!e`JHW7sO4!%anhWa@iI6&`~lF+Rgj&z<%-gp>dCN6FOXH{i;3{+$1S z;?8{|Ba;zC(EP*8-NlWKU_Nl*i9d6m+~Clg-$5GM%Byf96$*KLlBn(vr)`|Zn+u@R z$%@Q~f6geMx}3B^L)iCcM=rp^`zc7xf0z=v^RNL_TAWbyi>++Rk4r4az2MO*5OMMk zA58D>WCr;5aVx(igddhjyhM$3LbjwSDrh#u4_x?n13{ZNl!VbgYke?=UM0VBLHLdA zC-nbrvu4k{|KClPdWBD*^wV2OpMANt=AS+SFswd025zF`FGEA3Zh?9-BoF}skdU$i zMUZe2*qH^rZ*KbDaRPqdZ3KP?+x&7y(avyRMQx~*W%sOvwNX<77AL$Okv2=AL+K1~ z&O>tm=)ne#M0i#PimiBkot6nsJr-#~iKnQ(mc5k!n0JS$2q^5HU!RZ4%EOL;cSv%A z(EX`3SaY|8MVxnRBs~WD1~6WMJOb1Pcnmxy?gfv4pf9lFu$L-tsnEyIKxI$LdmvyB z6Tg`s5J0)8MuC^n)*$)OZ@U8t^!ZuA(Xgf_?rrGf`y#mLRS+khQCmYa|GI_6Fv%C- znk&?s^}W6M4^`EaFaM}Dx1hhmmbkKN*#7gUwuZ*~qO^gbmSIZ|S2Z)!$f$*-|5Ahc z?c%Dc#N*pe9X}P$Z>*EPF`AK)v1@+5Y|)a+_j8v|qHY=HXKqb)Y8iMB|Bl#g)2f*R zg{5_6Y7*VB$o1>9RQUrBFgJS{1ke*Hbg$%ld9}X%$lzA9+pa;O;Rb4_%PuPv~K zc3oWA{p$3s=2*V$DG}Gr8EKylLDe}#xO;?W?!?6ETqGCxkk=i+!6g&r-~1j66SLKm zn`W&Q8j0L;TF?}!XpnYab{2t(9;3Q?n;|t~hisC-5|(H7Q0Sh2P*||*FP^LeE~lQ| zJ|y7M(+4TbFl2ga>f(SA$y4(~PdjD;jiSSU3qn{NAzZp6biY zlF`O@33R3J*BDskuz9*8Uqz z&?Q%zmnvFj+cJZ0zkxTAa-i|T$KJgIM=l1!y{VtKwl-R{mH{sn z)Y0~6ctrva`vMBrF;_zeXbsl@&m#(_-X%M`+_8UpnMjNPn!kNhU_|(H9l=LPe%PEm zc(RjJI6VNbo!)k9H#HsB^t%iy#|~OB$LlhN2-M(W{F?YKb8~~?t;2g9bvu?3i(My5 z-3J(b^-Ltj&s#TIrtHkTf^WJ7YA$vuTY_EC)Nx_HwF zLCXzxR#6-03?X)0SRxU$mAC?y?6(g~T1fplFgfw$53z=!zw1HL~s4Q(N1pd0BgoI`SdgB%W>hF7SMlXHdhi%dZjDHq~C z#ayPoi9H8o|DZJ~#myZ#Nr`9o@}!7ha zz1wij;qoA_I*zxAcK0KKmBSr2CZN^H=#c>9Zm0+@0k@P(Hc&pRr_x{*P6`kxcYF4^Dwd0(WpUT^4;WcMW zG@eUDiF3MYWv?aQlqiNJj>Wu@Jq+Ko8~4LsALlaUvcUa9eRI zTnH+Eqn&BNbm%TT#33Cw3y#{{y}9%bN^0DGIISz}CQVIEkHCNjFhRik5Q3$Wl2YEv zH(jXFJiGlJN)ZC^a?Aj<@C>ja5KusL#>d>TNANbs3ICemh!Z5}gqe}Hf(L(&xa$0c zI~Lx}oTp{~2t9l1oAW_wmtlE2q~w4CAp#p~Yl%2)6hh70JkW0WZUDX)GWq+5qDv-2b1?WV7^c*q~B+edGX z(Js!ciRexOcNy*L51T$^I%`1tdg%I<7Z6~}6D?i&rYP~@AlkcKQ-N<($TfFFI_~gI zg3JwkmH57WdJg|G>XEps5U869b}Q~fJH0pBn4we}d^;{7KAsWo6ykjR`1kKsP%*KC zjy6sOiqkcuVp(VWfiuFwen3uNZZ_bU-|);6Z>8`gEM={)0U|nJMhq`0VB5Y)i5u!U znOx`lv+l=_#74fbNc9jab&(P12!t_24~o6qJb6*BI}3ka4T4%X3B8q6(@i5L!@a=+g*sPz!9C zS)j}E@Fg% z(;JWv)MZJ%S$3_D0&jgIZ=J}lwN8n@1l(nbg<_2K^zf_`9!#S7A%Ga{!n|V+f>w&x6WNGxs<8jANjR2_O0hR7H%a~NN1H3bYu`Bo~CL}QB&viP^t^47vsW^@g z-c88kw;7ul!^J)(%F6fx_z4GYyg6;T@fza>fyLayYM-yR*ILA$eW29WWNw@XVU$or zP$QPhmt$*8eO#dW56{O=48hYwJl1Dm0nOKTZhdj>8dhON({a#O__Hs^D*@MyAE}Ju zFm7KFO-axO>4}B|La7mN`C5p+EH0frh6mF)4P{uo1g+B^WOr!L!0iG&cVs}npS_3x zb52inSsuR=A1@9M7U51(&Djrw%CIm5wdod0U&87FE8yjOK#5m+ct?jLU#=!?j}{ga z;rlhovN0gQAL`h`wvfTds7WEc(zIM1P!i;GRc#2vxfl%a=TEmPO?2Pk43zGX^@Spr znE=dTd-*bQqxG?LG8I8?e6i+?pTbaWf)r?z9l|bVBfmh#?6km-0&2k zfdcQ4ng&%RNTD+^hy4=b`IISmx~P@0K_}!FNBGQPhx~4X8ITc`&X?aT$j7CAIO^1? z6M&kaP2x+4w$5__o?$H_hT+b%mS=landRBBfOZ8lVST)&@_i-OXC*Fg2tecxH&w17v9Mt&oUDLZ?X_Jla^e9sq?CGiRM} z6yyiwoO$w>l*4(_BHO9B+5Bjjuj!Z!*}Qjr$dE3Z5AU7fYo1mC!YgW#O?PbY(?Ownc}{7XeNKdW z_9Iw^LT>=P$Hr=K(bBKv%>*dTY5TalsMI|0<~(-n*vXTt96k=xpN+VbB^Wa9WMbMbEq?ZwO1pK?8RbmQW4n&|5=eH`Uxrg>4849>mPQOiA1KGwg7O%;n{K!-I;w3-pq7w*}?uBG_eA%blAC6z}6 z(qG@ok)??~>Lzh-+ZicuHEPOq+erh)=mRQ|B9m-B!zZ&g z#wL=zdcy=f8t>~YjYSd5L%;lXa&mzf`xPiFADrID)G!gT&W?*N>^#C4?V=vvs8u*! zU9{Z3`Lm-VQQ?+_x%GOzR#6hw1IKLLXw+>6Uiw*WFZY@ERJU$9h$4>iIh$S5^6q=%=r1ip%Me@e2Ey72s46IC2dPB5-YL^C(>(dIkeyPdH8sc5 ztTviKjW)|`7yRA`x+fo+Sw+#fJd-<*Cucr{r& ziK?;sN{ilE&l){ers{kRmHoaPL*bITf9OlqEu8xLviT~jnUTMg+vZ#tLsfP#lyuK` zZ6-0OsaD4~?q}QZV|eoZV}U;P(<)gIY}HYI^suS3ZhBaHJ)dFfal22dP4!5!_beZ8 z{?Z><7n#Mu(72DUYl zL#q{UFtfDV_1ez_pUI~p28Ma2*R#6r@N3jaR5~;5ip^;~DDJkKm6i3)38cmtWr{fG zdOc6fw@R-*W3Y>pq{<_jGe^gSdk=)LBin|~VPO7=K8F1yquhxgk=%g2$jq)GiXkI2 za~QEyuwr}ykAyuBG+dg|f+#BbyL7L-+3|^Q-ICQ(Yca{l&S=|y67|Z|`DFL5R%o6f zyL_2<#m_GuV-!uz6yBXgp$0LNKuO%F^nvQu=ky#$|AQ2wnkSl|vh)H#DrdFWV8Q0I z%A_9@=(>*Q`3H2LW5mUY+9(=O0qJc{|EwV(mToBQbz7TM&ouF)6YH{alqAj&LDvZ` zo9xT9h?MnKotu#)>s;dkK6F0>RnCeiua&fqACt7H(bC7vOc(gH+QQ}l2y7D<2))eE z&`_I=jzJ6-h=wz8QBbOJ1C(ILVUP|BMd_Fm12%(lXD@L(h? zbvT`lCFHCoihBhoc1n583voqAsKPd+H+7wLN4G>#8a7)9yP#+WxU+z;=|qRW-nsyU zXc%h7h8Cu!SBxA_yaR~#`Lczz^z-H2$miqcXaxHBFr)lUlOpEat zk;K6zls0imxod#&W9&t0v~bDn+>QHT*d;~(VvUGk-@4DKg2^$*8mUDI?C7TtY1b*p zWWP)*s&k$rFO`O++PM?{HUQ1wX9Q{tIO)N{&D%7w5s=WqPb0i^#(O6%m(D{P0hmP+ zva_DS5zAzNdhjy>?^1Aru@UwI*9OBAO6n}Ft@mRekWRq7n**W#KIiV3)-gJIA)N5Q zV-JL?=*Tn7)yPC1hPsX41GuZp%3)Ki4663TOA2Vj?5VWm1>15Q9SmMMacMt5F-(`- zmVR7m@xwMkg778@M-{ILU*(a83O)K2 z{aSxkgC^ssf(`87VJ9H|J8-oarFZHF^&R3dsWw%&-Cj9mK@~cqjM_^iYySbxd%~y#J9*`mY zo@Fl;@bv)h=-S*5>iul)Od^Z9>J;$h2j;P{e=*|qvkS3*3?(|kr|v5zdbxMnryyiv zW-OBjC=RlVh`GMEe{P&4<~YbJu?|fVSgn5Qz@Ho2>D$%OVruE9CL9KJe2kti^BX(( z_F%-ETh@alkxWbr%|Xv|P`$=q)X>1JAJPE2VCG?%_4>uXv=Ac1eTt{K!s9S@-~He0 zj&rOnB4B1?=6;Tb^avzQ>D=5ph;bVC@!XAzf6i#UHW&%QMJz$_+=eFyCHox2{%}#v zG{P|n1Yvh+#(nFPYkxkGgSYyzQOi=~d%dXC`Gw*zj4x*z?!1tAO-c?ZkvkO>32mH; zB@0%9UyX||WKM)#M)l_Z>|h#&eHj$_b@sjG0>`nnJYrM<^Mpgjf{{?G!8|lBC-4Z| z-(lG+N&D2u7Nd*JRo>4jJEE)k(y;3U zjMX_p;|#+c)MXI05XuO+U<$F}hMh=@exUWZ3X zdwc)Ts67zj@BwI13%S%bOUqJTx&+Sy$?oSASCqpe@+a!?rzp>NZ{JUbEp(OTvjdo6 ztL^Bem5hNafu`LMbB_e3frEggh9HEEgt(5tqY)S>#-b{Gl?Qj=JbWV#HTq$08NQ*4 zKgA^m*#nB+_I{?pL41p4+A3o6np=0CnBIpzU*;A}8MbvD5Ze_9c+^G{9li3f zeH`}H^{tVGa52mP8ow;bU}NALsQZ@os$ecN`hWyb`U2>p2ARo#e@+u}Vx6gawI+#zvsm6f@30(wl!cy~n3OYdK zTLy?Z!ZAoK$4Z!k@IQ7(FIzXn?v0SG3n*#Nx5}HPfwHM7zIJpq&AZ0 zT%S(6jyz(AebqS%PqWr>Fd4r)x#RNJ)IwP{0vQr4Yuj5qwl`XsPPH@p=xT9*M>qS#j zQ;eUwVW;;)5ZKW-ovf*(6c%+6br-N4Vg}o{F!a5Ma8JQ4GU9ar2#F9^Uh=I6*@gp7 zV1j=q-qNZOWSoKVaymHiXpgFsdVJBwqK|7+8TLvzx)=C|Lq&TmUm$mR>dKjWl zdR-kONCsX~;_e z7UKK?s1#JC+g0KIB2?ZmEr7{(Umk+gr(Ws_hed`%0TBy?Bx*<@ATE-!c20JI3u?mJ zjoTaBn*RcKWNQz)7YL>VKLJK>N=m?j$X|y>0k~Ex1?o=opHHB;52Y%Yz$y1hZJ)0z zlM7@Am;YJA`;Hb5^#~GaA3Y7(eGBz4>_@Mm3Qead6^sRHybcHW89Y5Usm}qN8yLUl zseKH8KN&`Zd5q=uupuI(D=6Hr?1!hInvg)_0<8Xm`VzR>h26WLXuyfh<38{hu7~8z zCc8;c4FR*n1zZxxf-B+f{e=loYeh`0)lYychI22jE=+*z$z#tV!vTSU+s8~?WSG2LJV-5_+tzzuMKun{BX1xm@jkI?~qA^C5kn;qnlz-h4Vtr^kSf)`LX`AuBK z8s2~I6N~vGxtn&2NsvZED;~VXU1KLBz|V*@GJg0ca0*+0_bU}BAPW2`$dCVL1RMa2 z9Qs+H0X;&@wk!Rxo;98xNXF~H{wboEU&HUA%5wp6+y#E!I6MmkAwrWwPRI8tZ)ojd z*FS~b5yeGf?11v+zkfre>^o@Vm(#-H>ZkTOkxncVsnL&aJzzfeWc&vAi;JuG7e>&P z3X?Q;0Botgzs_R)emJ^&=fr{$AS%t9C!!AeW&t5vTdn5%*b(4vafHt3FPkFS?SRtd zAl1Qz7u?xk2WLm<|BLHenZkMJKl`|0Y7xz}UQmEF^RQsHuoDypp%Yo2rbQqoDB@y^ zsrntRVX(SlY-AD?2&4P9Aeimxe8a8bvC%)n=h96-7tFy9``el(!JEFu!dW0QrfMgG z%HKX%oNU-ye=a@E7Wywxfu4a1$Jodyz-}EZgvAxLVA3qJviN|f%v*st*oc+nxTg=IhqinS3EpHcLAcGU0nsuWI&D3S;IF5Qxu%t8YiC31V@6U9h4bqMadWrcu{Ur zkI5ps@CYbp1^`yNMop=%$wt07&3H|KQPC9h3vP#L2p?h9myC~|X>dbyJ~_)Yj1FQV z?623JWrM|r?&IUkZCgHx3DZi;(E)hR54QBq0HC;<5=a^T>4eG3eu$Yc`lOJR7XZV3 zP>kOv4m>VskJtp|v(wBcQAWRqL7Dau0}R5v!Q!3;6t8jVz@M7RE4(zcj8`s;fp$8|j}0-g-jmnt2-RWP2D zh@W!f+{+(zNpPVl7u&Axs@*fM4r4kT3E<}Hu82bNooaYtpS+X7;bISZ$;<6FQ75KO z|M8k+pzX~

tMhZwKrGmiKCwY;6JSo#pH8OV!a!sd%f8?#^d62ho2LiD(Vkes2j& z)?_GI6px;ZgH6n8d@y37;a6Zt>{TbDsS0zbH!h5B3LV_tTSdT~!?365)kSQrjoMOXg|EwYt@TMx zmV)dSd?aOShS+u*@t6+3R5_jArAFosGplRK5Tm;C@f&ebzpcb6Ym5%K;TfHM$4gdX@OeMN8Raivj|26=~4txMu2gH_$ij zR!tMUo-SN>=T?aHW#Q@K)jHiZuv7zqt^x5Kav5L+n07Rw?)nzk+FgV~kBa*3903*A z$Ea+&_e1dDp}S*TU}lz6d(JpiH5-fMyId8;zT{PUL9+-|j{ALrzBVIcOM0r`0(3u~ zN~MDs(?E=QKSjl|1VJ)rGrNZPUKf#i8b|e=PRxUVV>2x61d+mye*q$Gx^$+ph+piV zhVAjsJh1-5Np{V5PrilQ$ntXILdCZ7u+^)@orcg7e48c}1<5o?CizwRNz9jpO_#gz zRwUK2ieNr&X5T*SmIb>*yMt=7lC%LkqOx)0u%jGyXz&dn`D&IZB~CQ7g{W^v53AYA zRA(%W{kI#O!MAdac8X0Bfzqvx>i)a`wfAJkch~{9E(a0lPD*k>n1SUs_e7tLm+?8= z4U7rbqhwatpdBCe^l#wh7lg)9eS1Rxf&)@!r;*4Jhq`?@&ppr%-WO>k@zMVpL2149 zRk(tL3nhqRur_ig41~w?k;qY6ytS=aA(2AcRe{Sz1A`pJaCkvgF0Zwi)vN!ep6E-UkrN014nhhn4cao~|PdzhJaT8c#{ap&$O znQllh&Rv7U2y?KboH1R0uL7 zRW`{Dbh{EFZR#HRDGE!18+a3TnC;vMHXu~Dc4TZ67BK|c4YHuBmb|S|2aDQ=6ZVk~{Aq9xq&->`vs||YwCf_E~UuB!0vm;Xs#I~PAI4I9E zKL-tA+N=h|LQW5szRJ9HYZrAp`1GcGP|NQwS0TW{j+k2TiDm8wq~Bu1#v19w6y!~i zc}MK%2W^G?P`QDP(Le0pPwi$-`l|u)8?X%_`%$-i`PZP7+ZXDpB~x{d;y z2yo?T*jC=XweT~5iTkN8pGQ8HDfg{f`-80E!#BgDAQ=)B|E#uU1tYuLh%ES@)1%*x z0=_tWN!zCl_-66RlXh{Fg0>hi7}m`CO4P4ltbW3o(9DGjZ(e=qs2oV*)pcFV3#2V- zgXbC}3R{j3Ypg3tGXJNUXdEWA)o8r1w^I@=EREHtFr{ zUDx2ZiJE6VQ!U9N0&ZR~RDF!aq2eKB`ImF!+G3e=soRWJ-au#})=IYZWe$0Mbz4NF z-s-Q038H>tCI9=1lYeFUSdlD&^QH5xgP~Q&aN>`((sYMWH~O$SJ!~#*>(!z_c#=!Z z)Wr}AJ|x(LCBnrSVzsALn=GV3*4N0ooc z8-LG!qb4)KdCJ!u9uoXLiqZHi-?{lGCoS>h+ZBIPms)auyaSNFNsuV56sW0uo z?T2Rd@6VE?9+gKnf}v@{uOYN=m$WRH+d=c-8ugo>L<{@q0*^P>0t09H$Rt zoBw1Jn%+!i1mmh{bC`%V&ujPqAxlgJcV?5HMz>4|#T` zpg2VlqW$S-8jY`bDme8G z>~*~LMa2GlxWm!HyAeFo1Ll0QKkjtKNH#NN?@Ph?AF8u0Is${}F>~4M+uZ1o(zq=E z&}}ZKr#BN}b>pG@Z2zsXdb>lZI=(k8GX^HBKZ(gfXZ<%=tx$Gaptv#3zi zdVjrpcvx8Zna@hhlhZ}?ndnDcSeoZa9!e!Hg>!bYC|cog==6=f?`=%!lbOt6f$Hj- zVp--*@#0G(XG$*of=cOK2MwWh3R!qM4ve)c=JrAY#;Mean>v^ z-TP)vYkZZ`n);_sI9-b~dKEe)T5=Af0nM}YmVoaUj)b0+^7(rymdr+=*=<>L{MZ4c%8;Bje%p6qmXVlb{$MJ)sTC}CV|c46<~TRT0$ zoQ;F1`mgDt+*nJqiAE+aG~?q29U{0xVRqIwp9_~0(c;nq!N>h9uH=yb(~HqXI^pOB zjsuoPv>pv|82p_>QzX}v|DKvqcFG{sL3#U;n~xOrCJ@`~6k{W~Z(!StU1P-g2ra##Yb4usiI54QUdLS+to7&_zr{S$>e# z8E1ea`)G9{t=0=UIGm@-n}2sCO%k^a-B%nN?|1w?TsVeX{Y5)ABhfC`{&$lXt7*Xf zt=x3ZqulksN9#j%V~K!$2Nnj|UA->M^DkUQ2;~F7`JQOqq$XvHOmk2HEIjw%IkbIK zW!kZ^g?g8MH{Bym?)%pj?;h=+oxZM0`f-bqS@v|sg?K4!kBNvVWkEY{J~sS0&Y`l1 zH_)z>g;7@1L2>5_kHZ4Ys^3inET4y~k7n1rH;-l^@1E7o#ngkh8N^zb4SnanO(|N zuex;cGgjAM|Dfv?sTl34DSZV47GTb~2G5~w@!x4(dYh|0g~ba0?gc9x%$)ef@DtNC z{oU&ZdU-5%%~e*IY(O!QW2~)(U~BhupDbU_<(1yuaAPlP<{$6L%vu94Umfa8jm(HD zWC2xT9l&)u1wh@HPZ!{H{S6y~zcIkvNR9K^!_SR+0UC)rrrD^-H}G!l3W}cH8eX*5hNMz9sf&@wZDQAFq<{${z*Kp~)?} zvp0LSvwo3hZ#YwR*VUf$(&W1?MGRQ<*zd^$0qId%<80aVI1X1U6Z?@0_Qdj=l(BqQ zC%gO-Ay?(0=5(8Ga|zZD$x62M*7(Re)&BbS?OWMI!#1DG8SEm-fEQ z@#?7v2~Q1|w=Y!+UmyIWQ($=qNSy1JA- zGDo?@>W`@jxI{k{t1$glWcP((waUL%%E{;~-Tl2-Q%{edvoc`PVDC$R)Fz3YDpB;= zU>KU;HD>GY>sygw*VfK3E{Sh7N)g}TA095iG?8B~DJd>0Y9C@Z)Mjj4GF2+>FRkz? z{K5Wp2NrVn)Fn88g(H2%YLe}I#5Om0guZ@#wW~H1H%n2@+ica(2j_C|6W@|V<1%|~K}pH(Jb{lvZpRxrLaatQes(t%#Ez#4 zL`S`0e=%4U^GH!<(r~nODsr=EWMpLab!TJw7u4(5)4e7llltoF?kj?3X!w&odmt## zX`17`=J3(?#={4-Ch5nQzuGvrq*&#Ip883hjBG&*6g(_hleH3PwD%q@mew(tPc_vs zXPued&Jz|)KfW&W;=TY|eP`>=j}u|kan*xEZM+_7Ez=xgt9MKX;Mi}FM>;L4D$}IB zP-JPi(zNi^)na~&sT>U++&NegcniZx)__{vTJ}V1yo^)Hy^_~hZk5g@DI5M*z zr1nh~7H%=2>3pe48H=0ht54Zj2@*FFG#k%xQiq<3(#nc58S8VLT(bQ@-AJ!OFm6;H zT~R1Lzu!7*q>eGuRPTECFNJV^eK8K18em@DzVN4e`*#u1tw{?gni<5SuxHOF_sLO# zAE?!=TCU5;n&IrB2PIrd7jY4r@(P;naYoVJPmvCLSlg zao@Qk)7Fe0U$xA-5?giOmRZZ0(>~Hy!AyrMFJ*?Cr_Sm)ml#x zQETmS7+ z3=(VgE{J_MSJ*|*aT0alej$>e4$k8#gR~#3vohJ^6e!a53RMM^o=YXowJh!USHof6 z3Z8vs7ruivjBq|k3&KP3^dU~aT8F8v{zqOMUv^Tp#@N#**9=FxT7Y~jaZ#LQzk{Uc`ZDq6rmk_x3>oG_Nk&Bxt z>vA=c0`h(yJcky)=}XO6&aWCT{+*`xwHCSNlF`O`jA0E4J|nn(l$n~&nE4d#ekpK>h0U|Bf?FqHs@Gv)lS+mN7(z@{l)+Lz7*f@ z>`Q*GtK2GKcXzSnNq66yi-jL63ZMJm?hiU#u}wcXV?20~!_wR64eh3gsHh}@5z)}7 z!f$V8IQoC<+$1`JZ*|nwIsR`mcE+2-g-Y|pPq{Rsu8;@imd7-jLj@-TQQlg6>a)_# zXAcuZWqBUbcJc8yJiij#R=fbLf9-3y@zKE|+mIfpqk4UPr3%||i0!w33!Xjo*l!Jx zO~$x*@@jw1`x^5iLKe^U}Gs3Ri^ighlZC?isjiY zO8-&Tv>G%wp~A3{?qdV1cyaeAPp)@BLyn^L0M`3{rb5uhH&hN& z+0d|6Xv{-5{!F0A@qT+jV2;Ws{SP3D{?=9Csl3^JkqmxzO8B*E zOVo?Y27EWqU#>C4h~M$ehGj=S(jC+cLlGoZlzaLAZv4ZFq8Bs$@&=;c3ZN;4UA`?g zlEm?c4OqXiX2Ru9nYPo@9glKZW|87vGF=P`2-I2^D07VQ3Kv_Xeq=f>$8-(z_ROa) z{o_opPj{Xzt4p`p@RgDBnFB!tT!+9TD74VkvttqaE)-xoE%%ue;0xkN<6JO8l zDbM*=F0c#-!NZ^v%MGX{kl^)zIeu%5rT-uP1suxk!{#3-WQ>2~$o8y%w*X_%&cQAS zKEAHo~FB1b2}qT)?EJb`#hFm;C2LPvwvq@_Bl?PDPzG1oHyn8 z#P7+JA0p$_kNx;^GG0M}8KdnKZ#e^TjAuH@`@V$Dc^|AYDYWbKD%Wi4JPIE0Cy zk>c?H-HeE!2I|;&u0Y1nf30q$bqkMM##FUv@$Om6P0QjJP~1M@qEe8Xk^oZC{fUGN zwYK@iUTe3(u;e!RcEsy{2$s>7vN-MBUhI@1ZJWZHrRfF6Gdck=p_?*&e&oI>%k;@( z4HOe!TI!nr;DubH(C=0;GJKk)%rvMWbG%kna+@lAFp4{fuz(;;9ed`l8c)av7MR|{ zlAz^Qy*KaWQhiJ0KLy>=3zLT~=RPXFEOY3fsW_yMEvyZ35ln5dOiBp76^~ev!9z1mJ%!gIKQ; zS=eElsF?iDStq|!gx2p73pVfmr=j-xi3EYx-Z}%;28q2&`)jA&;U}RS?ax^Y4#R@E zwZ9b;VdIOxDf(`TY9sosPj;ZK}ksbtB`g;gcLq%A z)RUDCWpoadK)EFB!TRPs$~<3UrZDOCpu6JtZtAg{hcfp$`QI}(tbqh$%2@$I}^dr%s(`!J!PAfmW)SbiFECVLIi&P#cSp8YIZlJiwZR<+)sP zGhc4yd-B6b0}chTI`2x@=@t_ovSZ+7vPfH%(Bj+W0-Nox)&7F_7@v_*$f5)NLof*Z z3j=lxR4}K~_79!-T(2RKny5B?T4ttQO#Z1C%$l6ZP*(q0!6L6FfW$V%A>i`@#Y3 zBAoC{bz|n}RC_NHl<>N!OVJVWm+K2Vr-p`{t__)?aO%H!@8&khW&$a;1MA@S(!3Sd z?0y%toN^IYx!RE9@pUOm$lMJSUnlvq``#Stu&~3|SK$J7+t+deCf@LSsen{VnGCBs ze~75j&dO}O;IV*>^8OIfW2h{5O<76lfe6qE*+kRjlfQhe$m1<)n+ixZ)p~gRbqjkT z1#-RU&bFukf{s> z0iQ?Q7#E|(v11B8 zrkqpli{-7YVZV)0YJ@TVsjD&;D)0iz+Jw07L2M}5v7l{XUs`5VV)-)MSH*A*rXun- zrn5cI@Q#tduIu~Zb78YhkHGN(!f)WfXw=usLm7E{`|rBe@vgl56t-w}e(3H9SDm9P zdJzfZvOq;uHNkx38-Z9)UHZ{Vb27-sNy*53x}$41Sa&06nl^W*eM^7ssF?PM$=$kiO+Pl*_Ng>tCDl`B!SFslgs; z5C^n={5YO5=U2r11S6Z?k`QgLK+JgV2vnf*?yLH5(yAe1+R2>FGB!OO(^cZOrZv0p zq>FE~)+*d#o?kNL1EjwT3;T?YyaY!E-{>i})p?5`if*}bTEHv?#12ZBHEl%jc|R+z z)Ne>8{sLDDvh0HL^V(O21*lC5=lB=ygkD3CrYKTuwt|z&sU%Q&s&e8)92VZQ-W}B$ z>fRpJ0PL1@Mr-KXGrw1Y>;rCs+VUQHH|z=lj2opqq=nMG^=u9;36IkK$pRw!&4%!UdcHOd|ZB%cdVx_MYWv}dRGB_EiR8t zvjUdVJnlo*nAWt<6&sE>vMtK_uutdoeCkd5%*4dlY*Twl0tal7aKgb&xv&mY6<%2O zbUb3a<$3`MY91vcE{g|xpfPy)WFRY8{+ysW>}I@;_WkL!@e!X5csUAmoUn*e5f$~_ z<^(BOT6z+LCc&wqtisYFF7-w{!46HTBGd#bBNtM~YlaU8$&KaV{j4v5&MIy|`~4|( z^)%CgkBA{&?W;zSWqxetcyF%;vmf>cOB7H>2gxdbHF`I}tTWZhj#18b>aDij&QA3k z;`)5{#Lo~cpR95l2vM+a`HGg}r*d(EVzGFYW~)}B1Uf#0p~25DZPOYb3c4miBZ?=l zPggeFao5YJ#H$St|M~83h<*P`**I3OKP_kPO~&gZ<5^+XTc1lE3zGOZ^stZ9+=b!c z;WhIp?+X7XqWsgXVn^%!q5ARg@bK*1g}Aa=jlSQ>&ZmBj zM|?X12#p}jN%5#2u$zczEoduzTK(!k!S0#Ka27>+?@?0vE-E(NmMJ~E(rtef8&!_b z7^Qd~9#EA#@3sK(Nb8#9%*o*=YIq5Sn9L;(3@r%@Ptb=wR3GKW=qM3FkfJ~@{*9xn zC+ex~ApQ?bpRxLZAv~1^i7B&&>`e3VG6AjaMipJHRl0~|)h0#mf#^CfrZ*0Eh0AXl z>3EcMOkIW(RYsr7QEGU!qbhx0{_zE7FIiK8Qwzj_=$FPkP}jyaZAx`%V-GRU0zNqvvPa(HAS29k1AI#9pL&?0 zjCocjUYdw^b((OWc65NhCLxEVHbj1fNhk{|+yPcN;;p(+086Og6IdV>KB%eQi3e8l zkH1)gRAlV9N{PW3UEb)k1W^5eP#U}X!xRfBl5X`_)5@%UDSh&Pq2mR$(fEbVcKLyI zJF%bDepsqy8s~}?MM0864g;@NY}Fp^DwAHn{?i7^aAWZ0uL60fB$ zg})yE;XQu{niC<7gF#L+IQC)&@5tQQX?9<8pU3PA)aMaANS0~T^s6MKz;9dNPQ#PK zHuTxKrDbKLt0B~gSM3#z#@RVoZ`?it)+^9RChFUQq^$ND8X96)RB|+(Ls_3t*P5&e z=Vu}s@4ELV0DKN14N3(9UTsZf^s~v%e1vd@yK2NlBkw3t4S(%t?sja5sgbrgZGmMh zAYdL5TDI7%F2nfSZLh^+yu%$o)784Aq}W`_PAy%WIg{__jPNfgos3Onm`V`jHX>8u z*9QkY&9rN5JAR}d)p!os+FSKP9Fg1g9Bt5N_lS(o(R`F`!3gM7MX0-6PCOFjIbmmF zY_WU$*9Bc4M+nvNT}J~Otg?TPlWn@+=i<{!vz*0GavLoXn)8=3yR?*&c`Il^4;h=e z43stcuSWMcvDP4q_n#Mo>;%QdyVIU$jf^qJi(l-rjaxt+HXpq0Reycr(>E4#4-gCI zq6!Li`swa$lVwipFZ_!i7P_|z?BDhv`|B_?fNzr5pnk~Q4ZPB4%Per_v##oZMQ8#G z3RL#mi#a_S>D&*uT97~VIU?C7OzYomv%ktH846>}mPuC9f5?fKZJ>nTA>tI|+~o$b zOu(QTh!c@N#&?J(#7wj;k#=f#sB;)jTtF?tz1p^rG5SP9uQV!JN8@!B;>ikLC@MU8 z@}S<;0VVoOJpzdXyUtL;c+D?my6>5OYftT2psg?4j2EYYZoRCiCv@VK3xODR%71I^=7;4{0Q*7S@{S>~?9Zl05StDLdiLTOuOwN^59OKN{#-rJeHlIpA0ZKf_U><|7g?Thn> zdmRs${dFeUcB%+v7tW#mh4*Wg?!LyzwXKheio#AGUX$C0jRi`WRhSM-$E3|_)A{X- zQY$Dyn<%|UGlN6fMf4N|d1GGhJ`oay>oQWr4b`nq*_{7<0N$Td$b|**>!Mbse&28XnMR z`<`g%*8oGSDG3*tS6KtDya?vN5mDqJ-I~4>7EXRns&pJ;WaHW~WkOl{!UvrF#&Alo zs?!1=9wsV6k%scVnT<8{Rq}2Auv1Ma6@5;>$H<-(h=(7$zkeOL#QOp0=;1qH{Gh(| zcWdc-iEAB}=*YWK%#S$PzJ7k1K+JL0wB0BNZ7qG>q|mxD#jLVuHHA1N&ognyF4@@3 z%Wpb}iF=;xSJ)lzrE!R54Og$pUhcRi(~O&%6AkEn#?z&lCgBL1Kr6VsE=$%;|F=@t zxmVhvt18_~k{Uk(tHE*Ti63+zNQY$@y0l1M>*((`&oD;r1AdC;To<3wwmzr!Bn{uK zS`FWZD2l}LDB*O#xFE~wCy4u6kzG5q;mUb}#(Z10TxUE`D0;DT>sn4kJ{mYMH8mRn z9YlXW%J{4^Wt%xQny`R&Qbc6;F!D@+5L&E=5Gx5ljlas8yTCKGjge*5YYnU6-2&P< zQS=08jy)eYR#OXwGkhGbGlV>;ui)_EjzP>sI=mO3l8n?nh(SGu6cPpaD7GE`(%CJ8 zqMvnaXTG>^vw4JTwSu8A2-(0&E@eLf4bX^9VN&IofW)uW)IbN~ri%nJqzwjzMdtsG zFT(dZ6kD&MWBr{_NT!jP36*yTF7^iB45${KJ(r& z9Pl?;EF$(P=4BOv)Dnb4&V7FD7Bo9?Ywn#X2ojZIde>sIV0j4J_5kU@K?@!0fz}Xt zqxlUzJ5}C059T{R``7TCrZGHKs%Qa$)q$zk2MKz`?KiI9cpgFU1b;hrsUMKM2&PjmL`~)FlOQJR#tWxn17CrffSF zBxTlg`{uM9Kqs++EcbLdvS^L%xZb$l&nvBG5RYT7o~O9?k>YtsemG_ErX@F3Hl63D zTs{*^nY-J5q*JylzTlAVE_=K?r^o%5;)3;`J|%=HK~*;)zm1Xe*vZK6OCqeh!IoFc z4MPs;UA00Pu01aEpz;$gS9xoeg&`uK)hq(;{Fm%eaQ1a*IG2DGDnje+hImc0VVvbg zSgEZiK;Eni`7lIBp}uIQL=fJyfTCEtv&(x(Y_%k0@{S!l{+g5bDCCf5VUYZlqG1u) z6pI9O_8{O;`Fd4PL4>kb0@&|_1&r`V^QG&y30)D#of{? zyOroBbm+k@#?2GaS}9WO+#FDIX+K5Vi~Up2H)agR?|_$;M*^cRw|CQ(4zJpNvEMB` z+GDj`PXOH4=hPAlm~R~IMX+>>UcOpKVA3u9lo0CSb&&_$uN>05RzIKjAl-&hRUnvC zm3(C?`M?z;v8S@#XU@I^;jyB;v>lh|FLUn=iOJcv%ojSS_X8!5Zc!r_QAHI7v$lk- z4Kg6;%yXAeQ+#)6bPYPMwVM5c@qZ=j%HBe5kjKrmCf@k#wB1qO@ARM`3m`HNVNfcE zKHO8+o>!*6ZLL-P#8$239qg8u8ZEEk@sxrU-}RwRxdVX8=-e0aNEj2gR4wCG` z$+%v>bxkFXR3u}X+JG1Ex)iktGy_uMY`?f({q5b~?RY^*X6x4P8y>E_&$T>SFEDe5 zL#O|ff`TJQu__mU2-cjCkOAIw-FJ71fW*~bc;~k!*~Dn+J+Ys}B8=5eSm)3iu{_mB6`bpNw@SFm9j7;rFn72V=*sq0zf}u zhePwEHAceX8bpx{!|JS2y@!?l|-b>s7s6x(9NBr2ozFt9%u6Chv( z_Ndibkrig}yrr)F>c6q??udZQ_Q@EfV76r~?R?IwtNbNlm2cU73j9c2B1+fgU`5=O zc2po87La*E&q0ktPMd+{Z^XdA$VI~g!rOD^&1sVBupDdRkJC(z=`WjI_S~A{TzM@c z{;$=wS^E|~&*_YeiKvb<>-JSzBNeTmZkIc0aL5b3po@Lut6?Bhn{zpz2Y=Pk6)&!* zn5Bcx;#Ci?PT4+f=sJ&*wVE0nGO&9cp1bpm)*kKsit_~**l;?0;vfeY zsphL6yEdY$Uv2T)B9f^Sm3gHp`Y212ezluFvrHw=Z*VX^fKS%Ov@YJHee^|5a$k+l z8cx^mz=+Tw>I+d18%j&da1aCU7c=-X?U9d7cZk)KcNqDK6CXSBJQamco>swGJkNj^ zvNngj0(225lv@bV@Dxc?@^c)@aCEpFlHblSt%=b^#cE_>M=F1mM%v;C2C|!l2XZ%w zY^#5sE-P(R3I7TNc(4A=CNXKOKMt8)m^r6T>a3N=D{?bc^6>mourLI9e%&-1_pDwN zjf9J~%ev3I?f#bXst+GOIy#K?eE(jRAiEJsEjZE$9ZO=Uflnz1!S_}OLZ|MOCCfRc zq}2N(uFNf~G!t~0f8}UO^QX`Q1->hqMUiMBZXmtBHY3wvXtZyqRnG$_Yt02Xrb^5o zN?&v>r@*FK0eOE3&5?;I?L<@SjQFda)kxrT!qMm{7qJg9XfHCWNs`v`hC}no_5Nax z!J(ni-s0k7I0O%>Pk%B9Zc=6T6Ssxu9Z<1cw+1V>=v+IQBD-2Z| zn)|h)^sTD9Z*Of!y4n*zzsmCR$4~etj|261BX9w_`?@}W!ErGzSh5IRanh~d#9C6G`O2%(7(LXsBYSsv@4?tpw51(%QsO zQBeRX6oSHWM7J;nT+oqU2uA224+1uTh{fY1Hxb?3@xerxG^pvHOYje{wEPPg7xdFm zpvjcN+yay!ipom<{+sLiK^;UqgZ_6l{+2q(K0E-ebOs%S4<>kk`{8xu7c*$Pe-^Y! z2xpH-SJPTAG?FMyf`J zCK@VQ$_6UxzcBue*FaTYQwyS@stEx%0%8KuS2k2JP**q5f*6|^niw1X;ysNEBD&!` z(7)tjLAgJ9RsN9|YDhr45%C0jJl^k@2iRcnM0^kiA0TOHry(he#Ns^hp+Rz++wi!C%3H#(@z-07C=1cP$10_AH(@(zg$z%nd~p zyC5R@7sa{5Z;0Sq_%89qJIqL$37e5^?z9>XNXnjq`indIn0oglrcvT5x=F1T)M!m(rGHa2H)=PX+83r)K}woRQ$@EdDo5GgUc@`3#va%s+M~3 zY+IdU4b09h!A7& z;xF|7Ev)~>(*6s*cJ>XM0Us?&z1myd;YW`i6-;Q?&U86MZ9SX6qUFv1sI;Z!biG8h z*VW=$WsUc<#iR+rjAO`rLA`Wd5|DF{KG`%ehY>Mm_F|Up`Scd~A0-sSwKd2{sy1`d z)G1SbQ|vch_HB>lXL+y?Qf~aFdGG9@C*?Op`IzQy1x=x+<;J-RogzQhDMAPX^|km{ z7TV1p4UHX-kI|qG8os|G??#g2-cFiDH1+XJGX zBUE5_B3ygZZyvL(#d4Vf^%Ca=_ZQ_-LU-AX!p)2&Q&uPVO4ba@UgdE~G7O6@y8m9z za;qCh@IEiInc~-5BqtYVH1U(QuTIaibLF`~PqDM=M;%J}7Tiy|UBmO|*xHb1*s)w< zi^V{A-VYL!{tTD$g()(GiBGCo7K=nblNCf6ltPeGrD7mtzEq|HL~zY#1zUbg%%5%uLm#%&TM3E~Y1O5k7SCr1K?-R4anY0c@tB6qt#qz za0l>>lF8fMR~8pxo^g9fq@>;{lQ+5}rHRD0Zoa0ac)8c79Jd;s$6`$TR0x-)qGr zHX6jMqp}VjAqNZRLDzCILL!Uxkr+R=WAeM_Nj0bg?*{L7(vh)4W|}>UbIab&C3ZGh zs~xXpdc%qe@4R7F?a;bMETzYSVV1zUhq>?3dL^nd@ab*kmf{QX|1zZR+a(dGdC6qmZeLj$TjRPg>^HyceP|DLFPT^L#c0L^iF5mG zetAfbwh9Dt^K3ws66Ip0(-9p}mY1=j_8!(`C)7S6i$61M*QAH}mdP)MC2Q|@`g~u) zz@OIM$Zm`Z(PW_uY+Xpv{le6z)2?Fu7~p7xh7kVG2wCqg}xzww`vdfOw2?S&BM=D9IQR z&x%^NQPkcYoRP51x;oksJ#;8?7GL#xK3iBQqm35!?CSV59a^at*`oBt^B%N`#&Ip&gOG~upu5Y@Zq_&;8#L)@PF=ZyW zjg&ZkO;->b^BwwfNYf0b8FJ6lk<1^wPi#)Be&mX1tlN@BtzLdFAx^o-_gi=CknAfy zsxzFUF)gp=0Lhn67TZXk%8K^g?%A+2w7vY)wpO>TtpRPwm?q_>43{M*Bj{)3HrYWx zlmQ<}WNf}IcnFga37!>i4rP!&%6e4CB690ViBSTJyM#Mv7F8j4arPCM!q`01&WeLAHd8W0groo0N4Ru`XGEH=M*-{xn_9eGS-Nh)2JLATo`_FYEZ^of-(>(usjxy9z- zF?Q@+kfZp)JqgY#y;BZ2St2F7r>srJ|8 z%*b4yks%I_hlh5X?+QCtNhM*8Z>+eDK564p2oZd6|NcpcVtlCgbGSED=0>?k(C3>k zL!P9#76qDl8HrgDg34wyhxTU(TOPiJq_m&%MQ&yHMvQSY9C{BYYqV-k#$ezQR3n!{ z+ZqwpBs`xUkmV{7Z5_&$Y_Yt6d+MrNCKnkk@zwl@=R(&@*Vif))`?;8F7@%?&P1Zw zmZJjY8;;`i+Wl@LAK&Y=Bf0BI*s}aI*L4N}vpP^W*{YUCEfYC1bmDcPAQN@1xc2Pn zfQwtAT~v!;mNYyfqKz#eOwX_v1XHAFLiX=rU1==e*#}iO(ED}(k9KjY4f{!YTJfVV z5@O(^?t+V?vk2<6X3_Ox5}5#IgfDfuzB%ln=Hx|&a_F(vwcB(ZxPTD1@G_?(bNI9I zCCcX8K;UybjYn&Z_cgWdmbqV3eFw~M!AU^WyYa-=qhOM&=Y4xKDYUes*taw>cCdkj zk`*&R{H{icIkt3N=QjpV9qmD>IObN_yTNar!~H(xiS$A+`E6bDTj7X27|=)%*|)(3 zoSX2!N?+}Kc8#7SOnuFl{@?WGsd)GGt9vszPpaYpDQU!+j9l=2Corh%k9F!`E2GRt`xR}5Fum^6t%R;L{Utzw~c zsyzv)u$8&5pJdE*@2x2%;e{fJA%4ToajfM`g~Oi`g;W`2{{14;$AP+?4-Z-{dVlP? zF?ukco}j~_P-!W`^lk$WvQgDZiguQ`PW&ZNuf>s^4ggTr+Y!4c_9DAZ!mW1p@mFC6 z3QRgW72`wRKD~~9Dg<0oGu;*~Me)u-Npf>aU1C{TGmkRn=Q$Qujq(r%dF_m5xCD^! zw#E!dNZHCX&!_K7SHp4scs1`WR}XJbUQf$|p=Oc^50&=Gj>Xgzhz+aNCpW6ksUY~@ zu}j@*`%_VIEL402a-KOz8_h`1PJd{CeWJLv?FJEoQpxUnNFD?}$FkN+*6u>*GF;UC z!aVj|B0W?J_6fW!p_zR<^-#*~I2r+I>hdYvSA+fL`+3wv%KEpORT`9QGZbzLcrGQsYu%5%%%1ol~ zqIdg=;qxiWPtxsv7d#9=KR@L?+!S|{pMx_XfxDFQBlUMB={L;!nMh+Q6wLJay_8y# zrbFF&>Pcd5aDf4>a-WX75AVTspFV=7ASJ{uL~cKrDhO4M*fx5p{CXW{ebcn%NWrP* z4e9Jsit&|jKmgkYVzjpsX(F@1IUYAeGk!vWmm4)iILlxZ}>%n9X%7<$Y z+k3W;1N_R7wkId_;nypD15 z4nuYMT&XS0M=yMzQkA$y4T@x^($2Zmmiio;c|T^Zi$cNItJ!n&NvMJ%zN!czrotHQ z$@E>)NkcgEGG-MW@_OM~h4gy=j4tUzPUVUR2lJk&f9`8Ne8W`zH7c1LcX8-(mJ@%^ zA4|6ksskxB58Ltv{C$9+-iBJezXqOoO`G+zUiV(rC#9Q(d)mIA9S^Xg+N`8Zyh_2W zjFQI-C~3A$>vX5C=d^iLvX41kIEAh+Ucb*z-+l!E zLi54AWVlv!VT}#>E&{H+W%4T}uX<)tRa`ff{`K1Kc$=b_a2q(>!`|Pvl$f!EH-gYeSIjNi0dGf3A9WN z&%8-m;J1QoWz$2;&Q9MVPHV}$i=VvTYP+8NME&P{o00yDbYx6H+nE^m6Y?7j*MNBa z#kj6E1bA4N-UW7TJpLVwd?5OSjHOWm$8S01p`I=A!w<841$zq8!nhDCq( zx8H34yvXzx7y6*&o#2He#2!dh0f4FDVS#~>W0|lsi);OAF?}1W7>Pq&zOa)q<4k)5 zeWto0J^bqgMyQGyLPX{;SE#zflUUXr9u=)baxBA9)1<0sv}T=g1GO+CMb9FOowxe5 zpAMDzY-7ewRoBv*CUOIMUp7ms|J!$>|1S?yde7L%Xf7Z^&Oh{E|-oWN;gE2C!y H=VJa1wU^l_ literal 0 HcmV?d00001 diff --git a/build/vite/common.js b/build/vite/common.js new file mode 100644 index 0000000..4bee2ab --- /dev/null +++ b/build/vite/common.js @@ -0,0 +1,33 @@ +import { config as conf } from 'dotenv' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +conf() + +export const cwd = process.cwd() +export const env = process.env +export const isProd = env.NODE_ENV === 'production' +export const isMac = env.PLATFORM === 'darwin' +export const isWin = env.PLATFORM === 'win32' +const packPath = resolve(cwd, 'package.json') +export const pack = JSON.parse(readFileSync(packPath).toString()) +export const version = pack.version +export const viewPath = resolve(cwd, 'src/app/views') +export const staticPaths = [ + { + dir: resolve(cwd, 'node_modules/electerm-icons/icons'), + path: '/icons' + }, + { + dir: resolve(cwd, 'node_modules/@electerm/electerm-resource/tray-icons'), + path: '/images' + }, + { + dir: resolve(cwd, 'node_modules/@electerm/electerm-resource/res/imgs'), + path: '/images' + }, + { + dir: resolve(cwd, 'src/client/statics'), + path: '/' + } +] diff --git a/build/vite/conf.js b/build/vite/conf.js new file mode 100644 index 0000000..a5154c4 --- /dev/null +++ b/build/vite/conf.js @@ -0,0 +1,69 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { cwd, version } from './common.js' +import { resolve } from 'path' +import def from './def.js' + +function buildInput () { + return { + electerm: resolve(cwd, 'src/client/entry-web/electerm.jsx'), + basic: resolve(cwd, 'src/client/entry-web/basic.js'), + worker: resolve(cwd, 'src/client/entry-web/worker.js') + } +} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + // commonjs(), + react({ include: /\.(mdx|js|jsx|ts|tsx|mjs)$/ }) + ], + define: def, + publicDir: false, + legacy: { + inconsistentCjsInterop: true + }, + resolve: { + alias: { + 'ironrdp-wasm': resolve(cwd, 'node_modules/ironrdp-wasm/pkg/rdp_client.js'), + '@novnc/novnc/core/rfb': resolve(cwd, 'node_modules/@novnc/novnc/core/rfb.js'), + // @xterm/addon-ligatures bundles lru-cache@11, which calls + // channel()/tracingChannel() from node:diagnostics_channel at import time. + // In the renderer (browser) context Vite stubs Node builtins and the call + // throws. lru-cache only uses it for optional metrics, so a no-op stub is + // safe. Covers both bare `diagnostics_channel` and the `node:` prefix. + 'node:diagnostics_channel': resolve(cwd, 'build/vite/diagnostics-channel-stub.js'), + diagnostics_channel: resolve(cwd, 'build/vite/diagnostics-channel-stub.js') + } + }, + optimizeDeps: { + exclude: ['ironrdp-wasm'] + }, + // assetsInclude: ['**/*.wasm'], + root: resolve(cwd), + build: { + target: 'esnext', + cssCodeSplit: false, + codeSplitting: false, + emptyOutDir: false, + outDir: resolve(cwd, 'dist/assets'), + rollupOptions: { + input: buildInput(), + output: { + format: 'esm', + entryFileNames: `js/[name]-${version}.js`, + chunkFileNames: `chunk/[name]-${version}-[hash].js`, + assetFileNames: chunkInfo => { + const { name } = chunkInfo + if (/\.(png|jpe?g|gif|svg|webp|ico|bmp)$/i.test(name)) { + return `images/${name}` + } else if (name && name.endsWith('.css')) { + return `css/style-${version}[extname]` + } else { + return 'assets/[name]-[hash][extname]' + } + } + } + } + } +}) diff --git a/build/vite/def.js b/build/vite/def.js new file mode 100644 index 0000000..7565975 --- /dev/null +++ b/build/vite/def.js @@ -0,0 +1,5 @@ +import { version } from './common.js' + +export default { + 'process.env.VER': JSON.stringify(version) +} diff --git a/build/vite/dev-server.js b/build/vite/dev-server.js new file mode 100644 index 0000000..bc85ee3 --- /dev/null +++ b/build/vite/dev-server.js @@ -0,0 +1,200 @@ +import logger from 'morgan' +import { + viewPath, + env, + staticPaths, + pack, + isProd, + cwd, + isWin, + isMac +} from './common.js' +import express from 'express' +import { createServer as createViteServer } from 'vite' +import conf from './conf.js' +import os from 'os' +import copy from 'json-deep-copy' +import proxy from 'express-http-proxy' +import fsFunctions from '../../src/app/common/fs-functions.js' +import { createToken } from '../../src/app/lib/jwt.js' +import { logDir } from '../../src/app/server/session-log.js' +import { resolve } from 'path' +import fs from 'fs' +import { defaultUserName } from '../../src/app/common/runtime-constants.js' +import { migrationNotice } from '../../src/app/lib/fancy-console.js' + +const devPort = env.DEV_PORT || 5570 +const devHost = env.DEV_HOST || '127.0.0.1' +const port = env.PORT || 5572 +const host = env.HOST || '127.0.0.1' +const h = '' +const tar = `http://${host}:${port}` +const defaultAIPreset = { + baseURLAI: 'https://ai.electerm.org/api/ai', + apiPathAI: '/chat/completions', + modelAI: 'mistral-small-latest', + authHeaderNameAI: 'Authorization: Bearer', + id: 'ai.electerm.org', + nameAI: 'ai.electerm.org(default free)' +} +const base = { + version: pack.version, + isDev: !isProd, + siteName: pack.name, + defaultAIPreset, + isWin, + isMac, + fsFunctions, + packInfo: pack, + home: os.homedir(), + versionFile: 'version-android.html', + downloadUpgradeFromBrowser: true, + server: h, + cdn: h, + isWebApp: true, + sessionLogPath: logDir, + tokenElecterm: process.env.ENABLE_AUTH ? '' : createToken() +} +let needMigrate +function checkNeedMigrate () { + if (needMigrate !== undefined) { + return needMigrate + } + + const nedbPath = process.env.DB_PATH || resolve(cwd, 'data/nedb-database') + const nedbUserPath = resolve(nedbPath, 'users', defaultUserName) + + // Check if nedb directory exists and has .nedb files + if (fs.existsSync(nedbUserPath)) { + const nedbFiles = fs.readdirSync(nedbUserPath).filter(file => file.endsWith('.nedb')) + + if (nedbFiles.length > 0) { + needMigrate = true + return needMigrate + } + } + + needMigrate = false + return needMigrate +} + +async function checkNodePty () { + return import('node-pty') + .then(() => true) + .catch(() => false) +} + +async function handleIndex (req, res) { + const hasNodePty = await checkNodePty() + const needMigrate = checkNeedMigrate() + if (needMigrate) { + migrationNotice( + 'electerm-web v3', + 'nedb', + 'sqlite', + 'electerm-data-tool --data-path "/path/to/data/nedb-database" export data.json' + ) + } + const data = { + ...base, + query: req.query, + hasNodePty, + needMigrate + } + const view = 'index' + res.render(view, { + ...data, + _global: copy(data) + }) +} + +function redirect (req, res) { + const { + name + } = req.params + const mapper = { + electerm: '/src/client/entry-web/electerm.jsx', + worker: '/src/client/entry-web/worker.js' + } + res.redirect(mapper[name]) +} + +async function createServer () { + const app = express() + + // Create Vite server in middleware mode and configure the app type as + // 'custom', disabling Vite's own HTML serving logic so parent server + // can take control + const vite = await createViteServer({ + ...conf, + server: { + middlewareMode: true, + allowedHosts: ['service.html5beta.com'], + hmr: { + overlay: true, + port: env.DEV_HMR_PORT || 23589 + } + }, + appType: 'custom' + }) + app.use( + logger('dev') + ) + app.use(express.json()) + app.use(express.urlencoded({ + extended: true + })) + staticPaths.forEach(({ path, dir }) => { + app.use( + path, + express.static(dir, { maxAge: '170d' }) + ) + }) + + app.set('views', viewPath) + app.set('view engine', 'pug') + + // Use vite's connect instance as middleware. If you use your own + // express router (express.Router()), you should use router.use + app.use(vite.middlewares) + app.get(['/', '/index.html'], handleIndex) + app.get('/:dir/:name.:ext', redirect) + app.listen(devPort, devHost, () => { + console.log('cwd:', cwd) + console.log(`server started at ${h || `http://${devHost}:${devPort}`}`) + }) + app.use( + '/api/login', + proxy(tar, { + proxyReqPathResolver: function (req) { + return req.originalUrl + } + }) + ) + app.use( + '/api/get-constants', + proxy(tar, { + proxyReqPathResolver: function (req) { + return req.originalUrl + } + }) + ) + app.use( + '/api/download', + proxy(tar, { + proxyReqPathResolver: function (req) { + return req.originalUrl + } + }) + ) + app.use( + '/api/upload', + proxy(tar, { + proxyReqPathResolver: function (req) { + return '/api/upload' + } + }) + ) +} + +createServer() diff --git a/build/vite/diagnostics-channel-stub.js b/build/vite/diagnostics-channel-stub.js new file mode 100644 index 0000000..c09ab70 --- /dev/null +++ b/build/vite/diagnostics-channel-stub.js @@ -0,0 +1,55 @@ +// Browser-safe stub for Node's `node:diagnostics_channel` module. +// +// Why this exists: +// @xterm/addon-ligatures (beta line) bundles lru-cache@11, which imports +// `channel`/`tracingChannel` from `node:diagnostics_channel` and calls them at +// module-load time (for optional metrics). In the Electron renderer / Vite dev +// server this is a browser context: Vite stubs Node builtins with a +// `browser-external` shim that throws on use, so `channel()` is undefined and +// the addon crashes on import. +// +// lru-cache only publishes metrics when `hasSubscribers` is true, which never +// happens here (we never subscribe). So all of these can be silent no-ops. + +function makeChannel () { + return { + publish () {}, + subscribe () { + return () => {} + }, + unsubscribe () {}, + bindStore (store) { + return store + }, + unbindStore () {}, + hasSubscribers: false + } +} + +export function channel () { + return makeChannel() +} + +export function tracingChannel () { + const ch = makeChannel() + return { + start: ch, + end: ch, + asyncStart: ch, + asyncEnd: ch, + error: ch, + trace (fn) { + return fn() + } + } +} + +export function hasSubscribers () { + return false +} + +export default { + channel, + tracingChannel, + hasSubscribers +} diff --git a/build/web/build.mjs b/build/web/build.mjs new file mode 100644 index 0000000..26b3d05 --- /dev/null +++ b/build/web/build.mjs @@ -0,0 +1,226 @@ +/** + * Build the electerm HarmonyOS (ArkWeb) web bundle. + * + * Modelled on build/android/build.mjs from electerm-android. Produces the + * Node.js project that runs on-device inside the HarmonyOS app: + * + * entry/src/main/resources/resfile/electerm/ + * ├── index.js entry started by the on-device node binary; sets + * env (HOST/PORT/SERVER_SECRET/data dirs) then + * imports app.bundle.mjs + * ├── app.bundle.mjs esbuild-bundled electerm backend (pure node, + * no electron APIs) + * ├── package.json read by runtime-constants.js via process.cwd() + * ├── views/index.pug server-rendered shell for the UI + * └── dist/assets/ vite-built frontend + static assets + * + * The resfile directory is packaged into the HAP as-is and is directly + * readable by the Node.js child process at + * /data/storage/el1/bundle/entry/resource/resfile/electerm — no runtime + * extraction needed. + * + * Differences vs the Android build: + * - No Capacitor www/ layout; output goes straight into the entry module. + * - No sql.js shim: the on-device runtime is hqzing/ohos-node v24 LTS + * (node:sqlite available without flags). + * - No path-to-regexp regex patch: that worked around nodejs-mobile's + * stripped ICU; ohos-node is a full build. + * - SERVER_SECRET is baked in at build time (from SERVER_SECRET / + * OHOS_SERVER_SECRET env; CI must provide it). + * - User data dir is passed at runtime via ELECTERM_DATA_DIR (the resfile + * install dir is read-only), so nothing about it is baked here. + */ +import { build as viteBuild } from 'vite' +import * as esbuild from 'esbuild' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const ROOT = path.resolve(__dirname, '..', '..') // build/web -> repo root + +process.chdir(ROOT) + +const OUT_DIR = path.resolve(ROOT, 'entry/src/main/resources/resfile/electerm') +const VERSION = JSON.parse( + fs.readFileSync(path.resolve(ROOT, 'package.json'), 'utf8') +).version + +// JWT secret for the on-device server. +// In CI this MUST come from the SERVER_SECRET / OHOS_SERVER_SECRET Action +// secret. Local development falls back to a fixed value. +const LOCAL_DEV_SECRET = 'electerm-harmony-local-dev-secret' +const SERVER_SECRET = process.env.SERVER_SECRET || process.env.OHOS_SERVER_SECRET || LOCAL_DEV_SECRET +if (process.env.CI && SERVER_SECRET === LOCAL_DEV_SECRET) { + console.error( + '[web] FATAL: SERVER_SECRET is not set. Add it to the repository GitHub Actions secrets (gh secret set SERVER_SECRET).' + ) + process.exit(1) +} + +function copyDir (from, to) { + if (!fs.existsSync(from)) { + console.warn('[web] skip missing source:', from) + return + } + fs.mkdirSync(to, { recursive: true }) + for (const entry of fs.readdirSync(from, { withFileTypes: true })) { + const s = path.join(from, entry.name) + const d = path.join(to, entry.name) + if (entry.isDirectory()) copyDir(s, d) + else fs.copyFileSync(s, d) + } +} + +// -------------------------------------------------------------------------- +// 1. Frontend (vite) +// -------------------------------------------------------------------------- +async function runVite () { + console.log('[web] building frontend (vite)…') + await viteBuild({ + configFile: path.resolve(__dirname, 'vite.web.mjs'), + root: ROOT, + logLevel: 'warn' + }) +} + +// -------------------------------------------------------------------------- +// 2. Static assets for the node project +// -------------------------------------------------------------------------- +function copyFrontendAssets () { + console.log('[web] copying static assets into node project…') + const assets = path.resolve(OUT_DIR, 'dist/assets') + + copyDir(path.resolve(ROOT, 'src/client/statics'), assets) + copyDir( + path.resolve(ROOT, 'node_modules/electerm-icons/icons'), + path.resolve(assets, 'icons') + ) + copyDir( + path.resolve(ROOT, 'node_modules/@electerm/electerm-resource/res/imgs'), + path.resolve(assets, 'images') + ) + copyDir( + path.resolve(ROOT, 'node_modules/@electerm/electerm-resource/tray-icons'), + path.resolve(assets, 'images') + ) + + fs.mkdirSync(path.resolve(OUT_DIR, 'views'), { recursive: true }) + fs.copyFileSync( + path.resolve(ROOT, 'src/app/views/index.pug'), + path.resolve(OUT_DIR, 'views/index.pug') + ) +} + +// -------------------------------------------------------------------------- +// 3. Backend (esbuild) +// -------------------------------------------------------------------------- + +// Mark all .node native-addon files external: the native binaries are not +// built for HarmonyOS and the libraries that use them have pure-JS fallbacks +// guarded by try/catch (see DISABLE_LOCAL_TERMINAL below). +const nativeNodePlugin = { + name: 'native-node-files', + setup (build) { + build.onResolve({ filter: /\.node$/ }, (args) => ({ + path: args.path, + external: true + })) + } +} + +async function bundleBackend () { + console.log('[web] bundling backend (esbuild)…') + await esbuild.build({ + entryPoints: [path.resolve(ROOT, 'src/app/app.js')], + bundle: true, + format: 'esm', + platform: 'node', + // hqzing/ohos-node v24 LTS runs on device + target: 'node22', + outfile: path.resolve(OUT_DIR, 'app.bundle.mjs'), + // Native modules that cannot be built for HarmonyOS. Kept external so + // esbuild never resolves them; guarded imports fall back at runtime. + external: [ + 'node-pty', + 'serialport', + 'node-bash', + 'font-list' + ], + banner: { + js: "import { createRequire } from 'module'; import { fileURLToPath as __etu } from 'url'; const require = createRequire(import.meta.url); const __filename = __etu(import.meta.url); const __dirname = __etu(new URL('.', import.meta.url));" + }, + plugins: [nativeNodePlugin], + // keep node built-ins external; everything else is bundled + logLevel: 'info' + }) +} + +// -------------------------------------------------------------------------- +// 4. Entry script + package.json +// -------------------------------------------------------------------------- + +function writeNodeEntry () { + const entry = `import { resolve } from 'node:path' +import { mkdirSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const __d = fileURLToPath(new URL('.', import.meta.url)) + +// The node binary is exec'd by the native launcher with cwd inherited from +// the app process; electerm's runtime-constants.js reads "package.json" via +// resolve(process.cwd(), 'package.json'), so switch cwd to this directory +// before loading the backend bundle. NOTE: this directory (resfile inside the +// HAP install tree) is READ-ONLY — all writes go to ELECTERM_DATA_DIR. +process.chdir(__d) + +process.env.NODE_ENV = 'production' +process.env.HOST = '127.0.0.1' +process.env.PORT = '5577' +// JWT secret baked in at build time. The web UI auto-logs-in because +// ENABLE_AUTH is not set. +process.env.SERVER_SECRET = ${JSON.stringify(SERVER_SECRET)} +// No pty on HarmonyOS -> disable the local terminal feature. +process.env.DISABLE_LOCAL_TERMINAL = '1' +// Where the pug views live (cwd is this directory, set above). +process.env.VIEW_FOLDER = resolve(__d, 'views') + +// Writable user-data directory, created by the ArkTS side and passed in via +// ELECTERM_DATA_DIR (the app sandbox filesDir). This is where the database, +// ssh keys and logs live. Falls back to a sibling of this (read-only) dir — +// which will fail on writes, but keeps local dev on desktop working. +const userDataDir = process.env.ELECTERM_DATA_DIR || resolve(__d, 'data') +mkdirSync(userDataDir, { recursive: true }) +process.env.DB_PATH = userDataDir +process.env.HOME = userDataDir + +// SSH keys live under /.ssh +mkdirSync(resolve(userDataDir, '.ssh'), { recursive: true }) + +await import('./app.bundle.mjs') +` + fs.writeFileSync(path.resolve(OUT_DIR, 'index.js'), entry) + + fs.writeFileSync( + path.resolve(OUT_DIR, 'package.json'), + JSON.stringify({ name: 'electerm-web', version: VERSION, private: true, type: 'module' }, null, 2) + ) + console.log('[web] wrote index.js + package.json') +} + +// -------------------------------------------------------------------------- +// main +// -------------------------------------------------------------------------- + +fs.rmSync(OUT_DIR, { recursive: true, force: true }) +fs.mkdirSync(OUT_DIR, { recursive: true }) + +await runVite() +copyFrontendAssets() +await bundleBackend() +writeNodeEntry() + +const outFiles = fs.readdirSync(OUT_DIR) +console.log('[web] done →', OUT_DIR) +console.log('[web] top-level:', outFiles.join(', ')) diff --git a/build/web/vite.web.mjs b/build/web/vite.web.mjs new file mode 100644 index 0000000..743053a --- /dev/null +++ b/build/web/vite.web.mjs @@ -0,0 +1,68 @@ +// Vite config used to build the electerm *frontend* for the HarmonyOS +// (ArkWeb) app. Identical to build/android/vite.android.mjs except the +// output goes into the entry module's resfile Node.js project. +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' +import { cwd, version } from '../vite/common.js' +import def from '../vite/def.js' + +function buildInput () { + return { + electerm: resolve(cwd, 'src/client/entry-web/electerm.jsx'), + basic: resolve(cwd, 'src/client/entry-web/basic.js'), + worker: resolve(cwd, 'src/client/entry-web/worker.js') + } +} + +export default defineConfig({ + plugins: [ + react({ include: /\.(mdx|js|jsx|ts|tsx|mjs)$/ }) + ], + define: def, + publicDir: false, + legacy: { + inconsistentCjsInterop: true + }, + resolve: { + alias: { + 'ironrdp-wasm': resolve(cwd, 'node_modules/ironrdp-wasm/pkg/rdp_client.js'), + '@novnc/novnc/core/rfb': resolve(cwd, 'node_modules/@novnc/novnc/core/rfb.js'), + // @xterm/addon-ligatures pulls in lru-cache which touches + // node:diagnostics_channel at import time; stub it for the browser. + 'node:diagnostics_channel': resolve(cwd, 'build/vite/diagnostics-channel-stub.js'), + diagnostics_channel: resolve(cwd, 'build/vite/diagnostics-channel-stub.js') + } + }, + optimizeDeps: { + exclude: ['ironrdp-wasm'] + }, + root: resolve(cwd), + build: { + target: 'esnext', + cssCodeSplit: false, + codeSplitting: false, + emptyOutDir: false, + // Output the built frontend *inside* the resfile Node.js project so the + // backend (which serves `dist/assets`) finds it at runtime on device. + outDir: resolve(cwd, 'entry/src/main/resources/resfile/electerm/dist/assets'), + rollupOptions: { + input: buildInput(), + output: { + format: 'esm', + entryFileNames: `js/[name]-${version}.js`, + chunkFileNames: `chunk/[name]-${version}-[hash].js`, + assetFileNames: chunkInfo => { + const { name } = chunkInfo + if (/\.(png|jpe?g|gif|svg|webp|ico|bmp)$/i.test(name)) { + return `images/${name}` + } else if (name && name.endsWith('.css')) { + return `css/style-${version}[extname]` + } else { + return 'assets/[name]-[hash][extname]' + } + } + } + } + } +}) diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json deleted file mode 100644 index 6bfce3d..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-49f5662a4b05781cba4a.json +++ /dev/null @@ -1,1407 +0,0 @@ -{ - "entries" : - [ - { - "name" : "CMAKE_ADDR2LINE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-addr2line" - }, - { - "name" : "CMAKE_AR", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Archiver" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" - }, - { - "name" : "CMAKE_ASM_FLAGS", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Flags for all build types." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_ASM_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Flags for debug variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_ASM_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Flags for release variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_BUILD_TYPE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel ..." - } - ], - "type" : "STRING", - "value" : "Release" - }, - { - "name" : "CMAKE_CACHEFILE_DIR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "This is the directory where this CMakeCache.txt was created" - } - ], - "type" : "INTERNAL", - "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a" - }, - { - "name" : "CMAKE_CACHE_MAJOR_VERSION", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Major version of cmake used to create the current loaded cache" - } - ], - "type" : "INTERNAL", - "value" : "3" - }, - { - "name" : "CMAKE_CACHE_MINOR_VERSION", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Minor version of cmake used to create the current loaded cache" - } - ], - "type" : "INTERNAL", - "value" : "28" - }, - { - "name" : "CMAKE_CACHE_PATCH_VERSION", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Patch version of cmake used to create the current loaded cache" - } - ], - "type" : "INTERNAL", - "value" : "2" - }, - { - "name" : "CMAKE_COMMAND", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Path to CMake executable." - } - ], - "type" : "INTERNAL", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake" - }, - { - "name" : "CMAKE_CPACK_COMMAND", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Path to cpack program executable." - } - ], - "type" : "INTERNAL", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack" - }, - { - "name" : "CMAKE_CTEST_COMMAND", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Path to ctest program executable." - } - ], - "type" : "INTERNAL", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest" - }, - { - "name" : "CMAKE_CXX_COMPILER_AR", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "LLVM archiver" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" - }, - { - "name" : "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "`clang-scan-deps` dependency scanner" - } - ], - "type" : "FILEPATH", - "value" : "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" - }, - { - "name" : "CMAKE_CXX_COMPILER_RANLIB", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Generate index for LLVM archive" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" - }, - { - "name" : "CMAKE_CXX_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for all build types." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_CXX_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for debug variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_CXX_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the CXX compiler during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "-Os -DNDEBUG" - }, - { - "name" : "CMAKE_CXX_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for release variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_CXX_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the CXX compiler during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "-O2 -g -DNDEBUG" - }, - { - "name" : "CMAKE_CXX_STANDARD_LIBRARIES", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Libraries linked by default with all C++ applications." - } - ], - "type" : "STRING", - "value" : "-lm" - }, - { - "name" : "CMAKE_C_COMPILER_AR", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "LLVM archiver" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar" - }, - { - "name" : "CMAKE_C_COMPILER_CLANG_SCAN_DEPS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "`clang-scan-deps` dependency scanner" - } - ], - "type" : "FILEPATH", - "value" : "CMAKE_C_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" - }, - { - "name" : "CMAKE_C_COMPILER_RANLIB", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Generate index for LLVM archive" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" - }, - { - "name" : "CMAKE_C_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for all build types." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_C_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for debug variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_C_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the C compiler during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "-Os -DNDEBUG" - }, - { - "name" : "CMAKE_C_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags for release variant builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_C_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the C compiler during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "-O2 -g -DNDEBUG" - }, - { - "name" : "CMAKE_C_STANDARD_LIBRARIES", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Libraries linked by default with all C applications." - } - ], - "type" : "STRING", - "value" : "-lm" - }, - { - "name" : "CMAKE_DLLTOOL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "CMAKE_DLLTOOL-NOTFOUND" - }, - { - "name" : "CMAKE_EDIT_COMMAND", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Path to cache edit program executable." - } - ], - "type" : "INTERNAL", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ccmake" - }, - { - "name" : "CMAKE_EXECUTABLE_FORMAT", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Executable file format" - } - ], - "type" : "INTERNAL", - "value" : "ELF" - }, - { - "name" : "CMAKE_EXE_LINKER_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Linker flags to be used to create executables." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_EXE_LINKER_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during DEBUG builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_EXE_LINKER_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_EXE_LINKER_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during RELEASE builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_EXPORT_COMPILE_COMMANDS", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "ON" - }, - { - "name" : "CMAKE_EXTRA_GENERATOR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Name of external makefile project generator." - } - ], - "type" : "INTERNAL", - "value" : "" - }, - { - "name" : "CMAKE_FIND_PACKAGE_REDIRECTS_DIR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake." - } - ], - "type" : "STATIC", - "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/pkgRedirects" - }, - { - "name" : "CMAKE_FIND_ROOT_PATH", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a" - }, - { - "name" : "CMAKE_GENERATOR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Name of generator." - } - ], - "type" : "INTERNAL", - "value" : "Ninja" - }, - { - "name" : "CMAKE_GENERATOR_INSTANCE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Generator instance identifier." - } - ], - "type" : "INTERNAL", - "value" : "" - }, - { - "name" : "CMAKE_GENERATOR_PLATFORM", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Name of generator platform." - } - ], - "type" : "INTERNAL", - "value" : "" - }, - { - "name" : "CMAKE_GENERATOR_TOOLSET", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Name of generator toolset." - } - ], - "type" : "INTERNAL", - "value" : "" - }, - { - "name" : "CMAKE_HOME_DIRECTORY", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Source directory with the top level CMakeLists.txt file for this project" - } - ], - "type" : "INTERNAL", - "value" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" - }, - { - "name" : "CMAKE_INSTALL_PREFIX", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Install path prefix, prepended onto install directories." - } - ], - "type" : "PATH", - "value" : "/usr/local" - }, - { - "name" : "CMAKE_INSTALL_SO_NO_EXE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Install .so files without execute permission." - } - ], - "type" : "INTERNAL", - "value" : "0" - }, - { - "name" : "CMAKE_LIBRARY_OUTPUT_DIRECTORY", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a" - }, - { - "name" : "CMAKE_LINKER", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" - }, - { - "name" : "CMAKE_MAKE_PROGRAM", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja" - }, - { - "name" : "CMAKE_MODULE_LINKER_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Linker flags to be used to create modules." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_MODULE_LINKER_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of modules during DEBUG builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of modules during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_MODULE_LINKER_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of modules during RELEASE builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of modules during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_NM", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-nm" - }, - { - "name" : "CMAKE_NUMBER_OF_MAKEFILES", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "number of local generators" - } - ], - "type" : "INTERNAL", - "value" : "1" - }, - { - "name" : "CMAKE_OBJCOPY", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objcopy" - }, - { - "name" : "CMAKE_OBJDUMP", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objdump" - }, - { - "name" : "CMAKE_OHOS_ARCH_ABI", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "arm64-v8a" - }, - { - "name" : "CMAKE_PLATFORM_INFO_INITIALIZED", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Platform information initialized" - } - ], - "type" : "INTERNAL", - "value" : "1" - }, - { - "name" : "CMAKE_PROJECT_DESCRIPTION", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "" - }, - { - "name" : "CMAKE_PROJECT_HOMEPAGE_URL", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "" - }, - { - "name" : "CMAKE_PROJECT_NAME", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "electerm_web_runtime" - }, - { - "name" : "CMAKE_RANLIB", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Ranlib" - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib" - }, - { - "name" : "CMAKE_READELF", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-readelf" - }, - { - "name" : "CMAKE_ROOT", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Path to CMake installation." - } - ], - "type" : "INTERNAL", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28" - }, - { - "name" : "CMAKE_SHARED_LINKER_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Linker flags to be used to create shared libraries." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_SHARED_LINKER_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of shared libraries during DEBUG builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of shared libraries during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_SHARED_LINKER_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of shared libraries during RELEASE builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of shared libraries during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_SKIP_INSTALL_RPATH", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "If set, runtime paths are not added when installing shared libraries, but are added when building." - } - ], - "type" : "BOOL", - "value" : "NO" - }, - { - "name" : "CMAKE_SKIP_RPATH", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "If set, runtime paths are not added when using shared libraries." - } - ], - "type" : "BOOL", - "value" : "NO" - }, - { - "name" : "CMAKE_STATIC_LINKER_FLAGS", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of static libraries during all build types." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_STATIC_LINKER_FLAGS_DEBUG", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of static libraries during DEBUG builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of static libraries during MINSIZEREL builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_STATIC_LINKER_FLAGS_RELEASE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of static libraries during RELEASE builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Flags used by the linker during the creation of static libraries during RELWITHDEBINFO builds." - } - ], - "type" : "STRING", - "value" : "" - }, - { - "name" : "CMAKE_STRIP", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-strip" - }, - { - "name" : "CMAKE_SYSTEM_NAME", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "OHOS" - }, - { - "name" : "CMAKE_TAPI", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "Path to a program." - } - ], - "type" : "FILEPATH", - "value" : "CMAKE_TAPI-NOTFOUND" - }, - { - "name" : "CMAKE_TOOLCHAIN_FILE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake" - }, - { - "name" : "CMAKE_UNAME", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "uname command" - } - ], - "type" : "INTERNAL", - "value" : "/usr/bin/uname" - }, - { - "name" : "CMAKE_VERBOSE_MAKEFILE", - "properties" : - [ - { - "name" : "ADVANCED", - "value" : "1" - }, - { - "name" : "HELPSTRING", - "value" : "If this value is on, makefiles will be generated without the .SILENT directive, and all commands will be echoed to the console during the make. This is useful for debugging only. With Visual Studio IDE projects all commands are done without /nologo." - } - ], - "type" : "BOOL", - "value" : "FALSE" - }, - { - "name" : "HMOS_SDK_NATIVE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native" - }, - { - "name" : "OHOS_ARCH", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "arm64-v8a" - }, - { - "name" : "OHOS_SDK_NATIVE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native" - }, - { - "name" : "PACKAGE_FIND_FILE", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "No help, variable specified on the command line." - } - ], - "type" : "UNINITIALIZED", - "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake" - }, - { - "name" : "UNIX", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "FROCE" - } - ], - "type" : "BOOL", - "value" : "TRUE" - }, - { - "name" : "_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "linker supports push/pop state" - } - ], - "type" : "INTERNAL", - "value" : "TRUE" - }, - { - "name" : "electerm_web_runtime_BINARY_DIR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a" - }, - { - "name" : "electerm_web_runtime_IS_TOP_LEVEL", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "ON" - }, - { - "name" : "electerm_web_runtime_SOURCE_DIR", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Value Computed by CMake" - } - ], - "type" : "STATIC", - "value" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" - }, - { - "name" : "node_ctl_LIB_DEPENDS", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Dependencies for the target" - } - ], - "type" : "STATIC", - "value" : "general;libace_napi.z.so;" - }, - { - "name" : "node_launcher_LIB_DEPENDS", - "properties" : - [ - { - "name" : "HELPSTRING", - "value" : "Dependencies for the target" - } - ], - "type" : "STATIC", - "value" : "general;libchild_process.so;" - } - ], - "kind" : "cache", - "version" : - { - "major" : 2, - "minor" : 0 - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json deleted file mode 100644 index 51616cc..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-e37c68776f2415d7fef8.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "inputs" : - [ - { - "path" : "CMakeLists.txt" - }, - { - "isGenerated" : true, - "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" - }, - { - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake" - }, - { - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake" - }, - { - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build/cmake/sdk_native_platforms.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeSystemSpecificInitialize.cmake" - }, - { - "isGenerated" : true, - "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake" - }, - { - "isGenerated" : true, - "path" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeSystemSpecificInformation.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeGenericSystem.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeInitializeConfigs.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCInformation.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeLanguageInformation.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang-C.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/CMakeCommonCompilerMacros.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/GNU.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/CMakeCommonCompilerMacros.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCommonLanguageInclude.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXInformation.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeLanguageInformation.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang-CXX.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Compiler/Clang.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/OHOS.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/Linux.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/Platform/UnixPaths.cmake" - }, - { - "isCMake" : true, - "isExternal" : true, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCommonLanguageInclude.cmake" - } - ], - "kind" : "cmakeFiles", - "paths" : - { - "build" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a", - "source" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" - }, - "version" : - { - "major" : 1, - "minor" : 0 - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json deleted file mode 100644 index f57235c..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-1ac40039f16ae038c893.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "configurations" : - [ - { - "directories" : - [ - { - "build" : ".", - "jsonFile" : "directory-.-Release-f5ebdc15457944623624.json", - "minimumCMakeVersion" : - { - "string" : "3.6.0" - }, - "projectIndex" : 0, - "source" : ".", - "targetIndexes" : - [ - 0, - 1 - ] - } - ], - "name" : "Release", - "projects" : - [ - { - "directoryIndexes" : - [ - 0 - ], - "name" : "electerm_web_runtime", - "targetIndexes" : - [ - 0, - 1 - ] - } - ], - "targets" : - [ - { - "directoryIndex" : 0, - "id" : "node_ctl::@6890427a1f51a3e7e1df", - "jsonFile" : "target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json", - "name" : "node_ctl", - "projectIndex" : 0 - }, - { - "directoryIndex" : 0, - "id" : "node_launcher::@6890427a1f51a3e7e1df", - "jsonFile" : "target-node_launcher-Release-968f49e15038ddbf7844.json", - "name" : "node_launcher", - "projectIndex" : 0 - } - ] - } - ], - "kind" : "codemodel", - "paths" : - { - "build" : "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a", - "source" : "/Users/zxd/dev/electerm-harmony/entry/src/main/cpp" - }, - "version" : - { - "major" : 2, - "minor" : 6 - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json deleted file mode 100644 index 3a67af9..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "backtraceGraph" : - { - "commands" : [], - "files" : [], - "nodes" : [] - }, - "installers" : [], - "paths" : - { - "build" : ".", - "source" : "." - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json deleted file mode 100644 index 0818490..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-08-28T04-39-21-0293.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "cmake" : - { - "generator" : - { - "multiConfig" : false, - "name" : "Ninja" - }, - "paths" : - { - "cmake" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake", - "cpack" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack", - "ctest" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest", - "root" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28" - }, - "version" : - { - "isDirty" : false, - "major" : 3, - "minor" : 28, - "patch" : 2, - "string" : "3.28.2", - "suffix" : "" - } - }, - "objects" : - [ - { - "jsonFile" : "codemodel-v2-1ac40039f16ae038c893.json", - "kind" : "codemodel", - "version" : - { - "major" : 2, - "minor" : 6 - } - }, - { - "jsonFile" : "cache-v2-49f5662a4b05781cba4a.json", - "kind" : "cache", - "version" : - { - "major" : 2, - "minor" : 0 - } - }, - { - "jsonFile" : "cmakeFiles-v1-e37c68776f2415d7fef8.json", - "kind" : "cmakeFiles", - "version" : - { - "major" : 1, - "minor" : 0 - } - } - ], - "reply" : - { - "cache-v2" : - { - "jsonFile" : "cache-v2-49f5662a4b05781cba4a.json", - "kind" : "cache", - "version" : - { - "major" : 2, - "minor" : 0 - } - }, - "cmakeFiles-v1" : - { - "jsonFile" : "cmakeFiles-v1-e37c68776f2415d7fef8.json", - "kind" : "cmakeFiles", - "version" : - { - "major" : 1, - "minor" : 0 - } - }, - "codemodel-v2" : - { - "jsonFile" : "codemodel-v2-1ac40039f16ae038c893.json", - "kind" : "codemodel", - "version" : - { - "major" : 2, - "minor" : 6 - } - } - } -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json deleted file mode 100644 index e894c80..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_ctl-Release-e8da2a6b8ac8d6b9c7ba.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "artifacts" : - [ - { - "path" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_ctl.so" - } - ], - "backtrace" : 1, - "backtraceGraph" : - { - "commands" : - [ - "add_library", - "target_link_libraries", - "include_directories", - "include", - "project" - ], - "files" : - [ - "CMakeLists.txt", - "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake", - "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" - ], - "nodes" : - [ - { - "file" : 0 - }, - { - "command" : 0, - "file" : 0, - "line" : 11, - "parent" : 0 - }, - { - "command" : 1, - "file" : 0, - "line" : 12, - "parent" : 0 - }, - { - "command" : 4, - "file" : 0, - "line" : 2, - "parent" : 0 - }, - { - "file" : 2, - "parent" : 3 - }, - { - "command" : 3, - "file" : 2, - "line" : 6, - "parent" : 4 - }, - { - "file" : 1, - "parent" : 5 - }, - { - "command" : 2, - "file" : 1, - "line" : 25, - "parent" : 6 - } - ] - }, - "compileGroups" : - [ - { - "compileCommandFragments" : - [ - { - "fragment" : "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -D__MUSL__ -O2 -DNDEBUG -fPIC" - } - ], - "defines" : - [ - { - "define" : "node_ctl_EXPORTS" - } - ], - "includes" : - [ - { - "backtrace" : 7, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/sysroot/usr/include" - } - ], - "language" : "C", - "sourceIndexes" : - [ - 0 - ], - "sysroot" : - { - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" - } - } - ], - "id" : "node_ctl::@6890427a1f51a3e7e1df", - "link" : - { - "commandFragments" : - [ - { - "fragment" : "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack", - "role" : "flags" - }, - { - "backtrace" : 2, - "fragment" : "-lace_napi.z", - "role" : "libraries" - }, - { - "fragment" : "-lm", - "role" : "libraries" - } - ], - "language" : "C", - "sysroot" : - { - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" - } - }, - "name" : "node_ctl", - "nameOnDisk" : "libnode_ctl.so", - "paths" : - { - "build" : ".", - "source" : "." - }, - "sourceGroups" : - [ - { - "name" : "Source Files", - "sourceIndexes" : - [ - 0 - ] - } - ], - "sources" : - [ - { - "backtrace" : 1, - "compileGroupIndex" : 0, - "path" : "node_ctl.c", - "sourceGroupIndex" : 0 - } - ], - "type" : "SHARED_LIBRARY" -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json b/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json deleted file mode 100644 index 1b96900..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-node_launcher-Release-968f49e15038ddbf7844.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "artifacts" : - [ - { - "path" : "/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_launcher.so" - } - ], - "backtrace" : 1, - "backtraceGraph" : - { - "commands" : - [ - "add_library", - "target_link_libraries", - "include_directories", - "include", - "project" - ], - "files" : - [ - "CMakeLists.txt", - "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake", - "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake" - ], - "nodes" : - [ - { - "file" : 0 - }, - { - "command" : 0, - "file" : 0, - "line" : 6, - "parent" : 0 - }, - { - "command" : 1, - "file" : 0, - "line" : 7, - "parent" : 0 - }, - { - "command" : 4, - "file" : 0, - "line" : 2, - "parent" : 0 - }, - { - "file" : 2, - "parent" : 3 - }, - { - "command" : 3, - "file" : 2, - "line" : 6, - "parent" : 4 - }, - { - "file" : 1, - "parent" : 5 - }, - { - "command" : 2, - "file" : 1, - "line" : 25, - "parent" : 6 - } - ] - }, - "compileGroups" : - [ - { - "compileCommandFragments" : - [ - { - "fragment" : "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -D__MUSL__ -O2 -DNDEBUG -fPIC" - } - ], - "defines" : - [ - { - "define" : "node_launcher_EXPORTS" - } - ], - "includes" : - [ - { - "backtrace" : 7, - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/sysroot/usr/include" - } - ], - "language" : "C", - "sourceIndexes" : - [ - 0 - ], - "sysroot" : - { - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" - } - } - ], - "id" : "node_launcher::@6890427a1f51a3e7e1df", - "link" : - { - "commandFragments" : - [ - { - "fragment" : "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack", - "role" : "flags" - }, - { - "backtrace" : 2, - "fragment" : "-lchild_process", - "role" : "libraries" - }, - { - "fragment" : "-lm", - "role" : "libraries" - } - ], - "language" : "C", - "sysroot" : - { - "path" : "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" - } - }, - "name" : "node_launcher", - "nameOnDisk" : "libnode_launcher.so", - "paths" : - { - "build" : ".", - "source" : "." - }, - "sourceGroups" : - [ - { - "name" : "Source Files", - "sourceIndexes" : - [ - 0 - ] - } - ], - "sources" : - [ - { - "backtrace" : 1, - "compileGroupIndex" : 0, - "path" : "node_launcher.c", - "sourceGroupIndex" : 0 - } - ], - "type" : "SHARED_LIBRARY" -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/.ninja_deps b/entry/.cxx/default/default/release/arm64-v8a/.ninja_deps deleted file mode 100644 index 0a9ec3e161ae921fa859ea7cb980c80239e7d6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 zcmd6rTW=IM7>1`nDW#lS3WY+U&=L+i1}SjY3zSfS5U5hc6&Hi!@n%fC_Q>`|yWBJv z{UP~999p1-ay~;LP|Eqx@_KhC2?)5TGTugdlF26H_dD31KYPt8CDcKh^4!jz5k*lC z=-Wew`1e9`OO#G|!kmm#VJ3cIvHlld{rF$9&vIkQp+-tlK1{ge%yCnzWYpBOY7(xT zX%cG~sZpT_%kxRY;QfCvj`QPe&vPjlbwVpk`uXqparPv%?*m#(IR6;BjVbi~ZENVYJSVQ1bK9EJk3#z_6e1l(525SV zahY`kIc}*vfY#3?Yf)-gwyn1!g{n6yI@6X62$VZ2<#@3Fh8-havZIK_eb`WX-R#t` zVw(LGBG*{M6pQH|w92m0RZd;qv=`9bZsxS(^H_lR)|J2%inBXi^f%X5G*CQl!-lfw za8(;8s!dY({Ul%Y;mm+-Syadq2D$AkVvg=<_y+oFy#BzQIwqu0gl# z{g4}@G-lORXpQ*1mDsbtYdU$ia+$gS^Z{9HaCFs_B>xiJ3W)D&lD18AW~)r`j3nXs{J`S(Wv|Ex1YVP>9& zR@uE_TdTd7oC+GxwvWGu@Udx0twvjyBELbeTYE`i!^uECTSr@G*Ardzxv=3lYzVcV z6q>&V(8S$^zXN2bNq#Gdet~A$ec`)$mC1G-gB_*U4)|VN7953M>HVNeg@1y+Nv t!5Xj@d;`7(>p%}!4>o{}U=yf-7!a@-Yyp$cMWY=XcbWxz`~Rmlz5*il!BGGJ diff --git a/entry/.cxx/default/default/release/arm64-v8a/.ninja_log b/entry/.cxx/default/default/release/arm64-v8a/.ninja_log deleted file mode 100644 index 3a75757..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/.ninja_log +++ /dev/null @@ -1,6 +0,0 @@ -# ninja log v6 -3 317 1787891931146197160 CMakeFiles/node_launcher.dir/node_launcher.c.o fc4caae051097ca9 -1 193 1787891898261950083 CMakeFiles/node_ctl.dir/node_ctl.c.o 34746826cf4d1478 -2 655 1787891961332981498 /Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_ctl.so 423c0b9631ec54d -1 713 1787891961332550252 CMakeFiles/node_launcher.dir/node_launcher.c.o fc4caae051097ca9 -714 829 1787891962044813141 /Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a/libnode_launcher.so 3422c9625d62cd06 diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt b/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt deleted file mode 100644 index 3125a89..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeCache.txt +++ /dev/null @@ -1,421 +0,0 @@ -# This is the CMakeCache file. -# For build in directory: /Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a -# It was generated by CMake: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake -# You can edit this file to change values found and used by cmake. -# If you do not want to change any of the values, simply exit the editor. -# If you do want to change a value, simply edit, save, and exit the editor. -# The syntax for the file is as follows: -# KEY:TYPE=VALUE -# KEY is the name of a variable in the cache. -# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. -# VALUE is the current value for the KEY. - -######################## -# EXTERNAL cache entries -######################## - -//Path to a program. -CMAKE_ADDR2LINE:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-addr2line - -//Archiver -CMAKE_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar - -//Flags for all build types. -CMAKE_ASM_FLAGS:STRING= - -//Flags for debug variant builds. -CMAKE_ASM_FLAGS_DEBUG:STRING= - -//Flags for release variant builds. -CMAKE_ASM_FLAGS_RELEASE:STRING= - -//Choose the type of build, options are: None Debug Release RelWithDebInfo -// MinSizeRel ... -CMAKE_BUILD_TYPE:STRING=Release - -//LLVM archiver -CMAKE_CXX_COMPILER_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar - -//`clang-scan-deps` dependency scanner -CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS:FILEPATH=CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND - -//Generate index for LLVM archive -CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib - -//Flags for all build types. -CMAKE_CXX_FLAGS:STRING= - -//Flags for debug variant builds. -CMAKE_CXX_FLAGS_DEBUG:STRING= - -//Flags used by the CXX compiler during MINSIZEREL builds. -CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG - -//Flags for release variant builds. -CMAKE_CXX_FLAGS_RELEASE:STRING= - -//Flags used by the CXX compiler during RELWITHDEBINFO builds. -CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG - -//Libraries linked by default with all C++ applications. -CMAKE_CXX_STANDARD_LIBRARIES:STRING=-lm - -//LLVM archiver -CMAKE_C_COMPILER_AR:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar - -//`clang-scan-deps` dependency scanner -CMAKE_C_COMPILER_CLANG_SCAN_DEPS:FILEPATH=CMAKE_C_COMPILER_CLANG_SCAN_DEPS-NOTFOUND - -//Generate index for LLVM archive -CMAKE_C_COMPILER_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib - -//Flags for all build types. -CMAKE_C_FLAGS:STRING= - -//Flags for debug variant builds. -CMAKE_C_FLAGS_DEBUG:STRING= - -//Flags used by the C compiler during MINSIZEREL builds. -CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG - -//Flags for release variant builds. -CMAKE_C_FLAGS_RELEASE:STRING= - -//Flags used by the C compiler during RELWITHDEBINFO builds. -CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG - -//Libraries linked by default with all C applications. -CMAKE_C_STANDARD_LIBRARIES:STRING=-lm - -//Path to a program. -CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND - -//Linker flags to be used to create executables. -CMAKE_EXE_LINKER_FLAGS:STRING= - -//Flags used by the linker during DEBUG builds. -CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during MINSIZEREL builds. -CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during RELEASE builds. -CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during RELWITHDEBINFO builds. -CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//No help, variable specified on the command line. -CMAKE_EXPORT_COMPILE_COMMANDS:UNINITIALIZED=ON - -//Value Computed by CMake. -CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/pkgRedirects - -//No help, variable specified on the command line. -CMAKE_FIND_ROOT_PATH:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a - -//Install path prefix, prepended onto install directories. -CMAKE_INSTALL_PREFIX:PATH=/usr/local - -//No help, variable specified on the command line. -CMAKE_LIBRARY_OUTPUT_DIRECTORY:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/build/default/intermediates/cmake/default/obj/arm64-v8a - -//Path to a program. -CMAKE_LINKER:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld - -//No help, variable specified on the command line. -CMAKE_MAKE_PROGRAM:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja - -//Linker flags to be used to create modules. -CMAKE_MODULE_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of modules during -// DEBUG builds. -CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during the creation of modules during -// MINSIZEREL builds. -CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of modules during -// RELEASE builds. -CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of modules during -// RELWITHDEBINFO builds. -CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//Path to a program. -CMAKE_NM:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-nm - -//Path to a program. -CMAKE_OBJCOPY:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objcopy - -//Path to a program. -CMAKE_OBJDUMP:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-objdump - -//No help, variable specified on the command line. -CMAKE_OHOS_ARCH_ABI:UNINITIALIZED=arm64-v8a - -//Value Computed by CMake -CMAKE_PROJECT_DESCRIPTION:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_HOMEPAGE_URL:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_NAME:STATIC=electerm_web_runtime - -//Ranlib -CMAKE_RANLIB:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib - -//Path to a program. -CMAKE_READELF:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-readelf - -//Linker flags to be used to create shared libraries. -CMAKE_SHARED_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of shared libraries -// during DEBUG builds. -CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during the creation of shared libraries -// during MINSIZEREL builds. -CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of shared libraries -// during RELEASE builds. -CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of shared libraries -// during RELWITHDEBINFO builds. -CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//If set, runtime paths are not added when installing shared libraries, -// but are added when building. -CMAKE_SKIP_INSTALL_RPATH:BOOL=NO - -//If set, runtime paths are not added when using shared libraries. -CMAKE_SKIP_RPATH:BOOL=NO - -//Flags used by the linker during the creation of static libraries -// during all build types. -CMAKE_STATIC_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of static libraries -// during DEBUG builds. -CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during the creation of static libraries -// during MINSIZEREL builds. -CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of static libraries -// during RELEASE builds. -CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of static libraries -// during RELWITHDEBINFO builds. -CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//Path to a program. -CMAKE_STRIP:FILEPATH=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-strip - -//No help, variable specified on the command line. -CMAKE_SYSTEM_NAME:UNINITIALIZED=OHOS - -//Path to a program. -CMAKE_TAPI:FILEPATH=CMAKE_TAPI-NOTFOUND - -//No help, variable specified on the command line. -CMAKE_TOOLCHAIN_FILE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake - -//If this value is on, makefiles will be generated without the -// .SILENT directive, and all commands will be echoed to the console -// during the make. This is useful for debugging only. With Visual -// Studio IDE projects all commands are done without /nologo. -CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE - -//No help, variable specified on the command line. -HMOS_SDK_NATIVE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native - -//No help, variable specified on the command line. -OHOS_ARCH:UNINITIALIZED=arm64-v8a - -//No help, variable specified on the command line. -OHOS_SDK_NATIVE:UNINITIALIZED=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native - -//No help, variable specified on the command line. -PACKAGE_FIND_FILE:UNINITIALIZED=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake - -//FROCE -UNIX:BOOL=TRUE - -//Value Computed by CMake -electerm_web_runtime_BINARY_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a - -//Value Computed by CMake -electerm_web_runtime_IS_TOP_LEVEL:STATIC=ON - -//Value Computed by CMake -electerm_web_runtime_SOURCE_DIR:STATIC=/Users/zxd/dev/electerm-harmony/entry/src/main/cpp - -//Dependencies for the target -node_ctl_LIB_DEPENDS:STATIC=general;libace_napi.z.so; - -//Dependencies for the target -node_launcher_LIB_DEPENDS:STATIC=general;libchild_process.so; - - -######################## -# INTERNAL cache entries -######################## - -//ADVANCED property for variable: CMAKE_ADDR2LINE -CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_AR -CMAKE_AR-ADVANCED:INTERNAL=1 -//This is the directory where this CMakeCache.txt was created -CMAKE_CACHEFILE_DIR:INTERNAL=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a -//Major version of cmake used to create the current loaded cache -CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 -//Minor version of cmake used to create the current loaded cache -CMAKE_CACHE_MINOR_VERSION:INTERNAL=28 -//Patch version of cmake used to create the current loaded cache -CMAKE_CACHE_PATCH_VERSION:INTERNAL=2 -//Path to CMake executable. -CMAKE_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cmake -//Path to cpack program executable. -CMAKE_CPACK_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/cpack -//Path to ctest program executable. -CMAKE_CTEST_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ctest -//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR -CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS -CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB -CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS -CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG -CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL -CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE -CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO -CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_STANDARD_LIBRARIES -CMAKE_CXX_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER_AR -CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER_CLANG_SCAN_DEPS -CMAKE_C_COMPILER_CLANG_SCAN_DEPS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB -CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS -CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG -CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL -CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE -CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO -CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_STANDARD_LIBRARIES -CMAKE_C_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_DLLTOOL -CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 -//Path to cache edit program executable. -CMAKE_EDIT_COMMAND:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ccmake -//Executable file format -CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS -CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG -CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL -CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE -CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//Name of external makefile project generator. -CMAKE_EXTRA_GENERATOR:INTERNAL= -//Name of generator. -CMAKE_GENERATOR:INTERNAL=Ninja -//Generator instance identifier. -CMAKE_GENERATOR_INSTANCE:INTERNAL= -//Name of generator platform. -CMAKE_GENERATOR_PLATFORM:INTERNAL= -//Name of generator toolset. -CMAKE_GENERATOR_TOOLSET:INTERNAL= -//Source directory with the top level CMakeLists.txt file for this -// project -CMAKE_HOME_DIRECTORY:INTERNAL=/Users/zxd/dev/electerm-harmony/entry/src/main/cpp -//Install .so files without execute permission. -CMAKE_INSTALL_SO_NO_EXE:INTERNAL=0 -//ADVANCED property for variable: CMAKE_LINKER -CMAKE_LINKER-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS -CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG -CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL -CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE -CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_NM -CMAKE_NM-ADVANCED:INTERNAL=1 -//number of local generators -CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 -//ADVANCED property for variable: CMAKE_OBJCOPY -CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_OBJDUMP -CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 -//Platform information initialized -CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_RANLIB -CMAKE_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_READELF -CMAKE_READELF-ADVANCED:INTERNAL=1 -//Path to CMake installation. -CMAKE_ROOT:INTERNAL=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS -CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG -CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL -CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE -CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH -CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SKIP_RPATH -CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS -CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG -CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL -CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE -CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STRIP -CMAKE_STRIP-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_TAPI -CMAKE_TAPI-ADVANCED:INTERNAL=1 -//uname command -CMAKE_UNAME:INTERNAL=/usr/bin/uname -//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE -CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 -//linker supports push/pop state -_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE - diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake deleted file mode 100755 index 1f37c04..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake +++ /dev/null @@ -1,74 +0,0 @@ -set(CMAKE_C_COMPILER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang") -set(CMAKE_C_COMPILER_ARG1 "") -set(CMAKE_C_COMPILER_ID "Clang") -set(CMAKE_C_COMPILER_VERSION "15.0.4") -set(CMAKE_C_COMPILER_VERSION_INTERNAL "") -set(CMAKE_C_COMPILER_WRAPPER "") -set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "17") -set(CMAKE_C_EXTENSIONS_COMPUTED_DEFAULT "ON") -set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23") -set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes") -set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros") -set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert") -set(CMAKE_C17_COMPILE_FEATURES "c_std_17") -set(CMAKE_C23_COMPILE_FEATURES "c_std_23") - -set(CMAKE_C_PLATFORM_ID "Linux") -set(CMAKE_C_SIMULATE_ID "") -set(CMAKE_C_COMPILER_FRONTEND_VARIANT "GNU") -set(CMAKE_C_SIMULATE_VERSION "") - - - - -set(CMAKE_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") -set(CMAKE_C_COMPILER_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") -set(CMAKE_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") -set(CMAKE_C_COMPILER_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") -set(CMAKE_LINKER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld") -set(CMAKE_MT "") -set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") -set(CMAKE_COMPILER_IS_GNUCC ) -set(CMAKE_C_COMPILER_LOADED 1) -set(CMAKE_C_COMPILER_WORKS TRUE) -set(CMAKE_C_ABI_COMPILED TRUE) - -set(CMAKE_C_COMPILER_ENV_VAR "CC") - -set(CMAKE_C_COMPILER_ID_RUN 1) -set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m) -set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC) -set(CMAKE_C_LINKER_PREFERENCE 10) -set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE) - -# Save compiler ABI information. -set(CMAKE_C_SIZEOF_DATA_PTR "8") -set(CMAKE_C_COMPILER_ABI "ELF") -set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN") -set(CMAKE_C_LIBRARY_ARCHITECTURE "") - -if(CMAKE_C_SIZEOF_DATA_PTR) - set(CMAKE_SIZEOF_VOID_P "${CMAKE_C_SIZEOF_DATA_PTR}") -endif() - -if(CMAKE_C_COMPILER_ABI) - set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_C_COMPILER_ABI}") -endif() - -if(CMAKE_C_LIBRARY_ARCHITECTURE) - set(CMAKE_LIBRARY_ARCHITECTURE "") -endif() - -set(CMAKE_C_CL_SHOWINCLUDES_PREFIX "") -if(CMAKE_C_CL_SHOWINCLUDES_PREFIX) - set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_C_CL_SHOWINCLUDES_PREFIX}") -endif() - - - - - -set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include") -set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "unwind;-l:libunwind.a;c;-l:libunwind.a") -set(CMAKE_C_IMPLICIT_LINK_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos") -set(CMAKE_C_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake deleted file mode 100755 index 799bb9c..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake +++ /dev/null @@ -1,85 +0,0 @@ -set(CMAKE_CXX_COMPILER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++") -set(CMAKE_CXX_COMPILER_ARG1 "") -set(CMAKE_CXX_COMPILER_ID "Clang") -set(CMAKE_CXX_COMPILER_VERSION "15.0.4") -set(CMAKE_CXX_COMPILER_VERSION_INTERNAL "") -set(CMAKE_CXX_COMPILER_WRAPPER "") -set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "14") -set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT "ON") -set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") -set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") -set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") -set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") -set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") -set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") -set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") - -set(CMAKE_CXX_PLATFORM_ID "Linux") -set(CMAKE_CXX_SIMULATE_ID "") -set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT "GNU") -set(CMAKE_CXX_SIMULATE_VERSION "") - - - - -set(CMAKE_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") -set(CMAKE_CXX_COMPILER_AR "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ar") -set(CMAKE_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") -set(CMAKE_CXX_COMPILER_RANLIB "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/llvm-ranlib") -set(CMAKE_LINKER "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld") -set(CMAKE_MT "") -set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") -set(CMAKE_COMPILER_IS_GNUCXX ) -set(CMAKE_CXX_COMPILER_LOADED 1) -set(CMAKE_CXX_COMPILER_WORKS TRUE) -set(CMAKE_CXX_ABI_COMPILED TRUE) - -set(CMAKE_CXX_COMPILER_ENV_VAR "CXX") - -set(CMAKE_CXX_COMPILER_ID_RUN 1) -set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;m;mm;mpp;CPP;ixx;cppm;ccm;cxxm;c++m) -set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) - -foreach (lang C OBJC OBJCXX) - if (CMAKE_${lang}_COMPILER_ID_RUN) - foreach(extension IN LISTS CMAKE_${lang}_SOURCE_FILE_EXTENSIONS) - list(REMOVE_ITEM CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${extension}) - endforeach() - endif() -endforeach() - -set(CMAKE_CXX_LINKER_PREFERENCE 30) -set(CMAKE_CXX_LINKER_PREFERENCE_PROPAGATES 1) -set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE) - -# Save compiler ABI information. -set(CMAKE_CXX_SIZEOF_DATA_PTR "8") -set(CMAKE_CXX_COMPILER_ABI "ELF") -set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") -set(CMAKE_CXX_LIBRARY_ARCHITECTURE "") - -if(CMAKE_CXX_SIZEOF_DATA_PTR) - set(CMAKE_SIZEOF_VOID_P "${CMAKE_CXX_SIZEOF_DATA_PTR}") -endif() - -if(CMAKE_CXX_COMPILER_ABI) - set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_CXX_COMPILER_ABI}") -endif() - -if(CMAKE_CXX_LIBRARY_ARCHITECTURE) - set(CMAKE_LIBRARY_ARCHITECTURE "") -endif() - -set(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX "") -if(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX) - set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_CXX_CL_SHOWINCLUDES_PREFIX}") -endif() - - - - - -set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include") -set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "unwind;c++;c++abi;unwind;m;-l:libunwind.a;c;-l:libunwind.a") -set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos") -set(CMAKE_CXX_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin deleted file mode 100755 index d5da485fd4a4f9f59e0145e3d861431ce761c2b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14168 zcmds8eQ;dWb-(Yu{nDo;*|GeEB^w*ehgK4nZP|RuwrnhaV*>$ulE0oFJc!Hz;&UyFj zKE0I+#36t5j@~=>{_Z*Fp8I|8*{8eLZC-C0hLT{Z?<-0@Uhg0sI^(4#9RcZ3?Wz** zi_{_&K-$Ncli%SGZrVzba^l7@`T>6 z8$zh)+3|WlM(y%Zc6LxBAV_;|*K((*Py{c%+<}!)Y5VITr$sc!$0H=qI&^z051psg z2$lMY+&s{xSl$-SWx}nRpSp*5sXexB*tS!{ zoF;q*9xC#>tv9M6^V~yU|EtsQANt-maeMvV=D~@d`jIGQaeMoeLD=zo->LqOe|o!`FzE+9 zy?{7!(sp`SG46ip>EU#cyBV7k7w^{Ukr9heCO~S@!`cX$ua+JV?X#SWXZ5@i+^5^+IS>A*=1=Ge zApHHa9X$*C(u2#rpndneHZfCOSI8ihlkZ8;tEMaU6VMLOJSfd)3dS8f)+pny`NrHznv%S}(VBA# z`$dd^&Zf@lmJO8`)O^$YhR^J*Y-rg~4e-@}qfCc?Gw3Ol!)q1s*K}%ru-A0D+1=&3 zi)EbT$6)vUq`0%XkqFt@hUZr3cegX;*^8_fFHrkmn2%@FQo{|6){6%oR5cC1y6cb8 zw=?d((0XzF8TI_RJI!O~4yoZIhZ;UOddU7DWOST8y0+ms#}}yKv7kCTverKSTCn3R z>1eEg4$$rzhoUWNUbh+^c|fVJoV#<)G3Xo~hwhV>{Zq1G{1BC496PsG4P$+HEVO$4 z*^r^$8LNM2&e3NYjstrxG?H7HP-;2Zg*9=;(P!-Am#F0emy?U zTLkzR`i#bf$UZl!C!fe+ymBbhjCy-OM>_gMP9LAum|hHJZ5`BdvZfx<>!z$rA}?pF z4$WBMDNokel(rA^i9=e`|M`t$k1QUthAh$%y?qtVvvaka)KiWLvQP3F2Ojhfhe}Ol zzmtQI>~lK%QzP58fgeT10HfAeym-3bTAZz~sh_*Jc5YoDhdwf=fx!wJlgdK?ybWH`ZIBxuM0O>F6J`Tv?%2 zfhz;GG-kX87&l&+tIv}OZP3v*{o2`7wu$j%kxf3YO>6=&3R_^hQBgq&m{G+lDAA5& zsAyH0VUV5W3t;XFWK~dBMa4CURytLv`i#05)vb1{AQgv=X=H}iVs0+Ct(wnrittGh z{)@P20y9CJ2A$AOY@B1kZ{K8LRoY>e7ep13c>K!XL2cm9>cjw=yvY-8S{ya{ETh_I z4iEZ_YPys8&Eb5&sM*EQ`$8e3_Vb9Uz)g4s{r*re==TM>2}uxAhIL^Rx|s@#0x+;= zqCBNC(c7ByiDWir_v4^D)J*q~=42X&+`eQi8Og_UxS^y6206pdWxMVEOg0~nHFsz8 z&HBDV@nkrsns3a-v$^K}T%sq^ozG;4ns3i!_vHE`-SOr-lCelSZs&$@e(tmTll^h1 zjpIa}ibrzs=1juQ#rD|C!!6;K=I%^ie=-$knn}mgdAmCk1J6Ld8BBZ)YFrYF@4KY2 zI~7UyHts{I$xOPjbs5Cl8keL}`}&&tvzgoC-TB7W)@9MZ%u%i5!D?UyKHzscu+w2H#`D_l`bz7G&O6Hj3Qc?Jn;&I6#_ckPJy z(8jWqDn+BT36E(l$6HmHBg*mx^N1RKRqA5Dub1E@=7`}B+)UB1IbsGnR5%#i!%4hC ze$yx;@$%a#r7A+9pug-7DSMSUVpZOQNR=;i3dHEER=fP=taP_I;#2+#<{LCe{MD6D zkVtjN90?TF0oj0us<9N7b2aV)aE7Wu1IM)ZzESBT)5!|ob$A)dej8}#{o zp+G388X!ixzDl|_qaD_P;deE{(*GYun7W3V3{CX7H1`nz-$zpwN6jD*Iu@9`HyRmB z)mjGaZ$_UFLy$IP+FdV&1`6enJc#&lJb#V&G0?rBuYo>-{0=!?;=iiQN=z$-g%9I zomgI4!`&*}oX=!Z-3i>qo841f){ATtmLzhg(9(n_iradct!&+4D0OCHJQWawr%cfLz8-MAESoxdUi%NwRMsrtCGS3NY~&0CAiM5o6_t7LtLCepW0TqIho!Q&#r3Rytz~Fs_H&%!*)=^Fn(QK zMFevjy8*=-Oq%egk+2Y-=AVxgLQ~J*-}cJbt2cdn@kM36C70cBHF2l%d&2+nRP4z| z{{5Fvy?6a*r>Qbin$oH^{8iGvHxvj6DoU%&U7hgq}qw{?->pH*%ync)wj zYW~2XfOXSGe{I0J>psL$)Vu1S-}oYzMduM}+^PGU z>blL<-?SO1pY6N6ZPlB#UznLVT7K)bKYqEB`nzWD8?%nx{I?^Cu{ZWU|6dhfq7%x; z@3?ost!}=$^6kW)*Pnm-!*4$@+Pm+I+>V#)OV)H(uXSjw-ofaw_3?|M{ZhUE%G)h( z?fg#b1F5@I@Vdy4xG0%=R;}ST%BL4ciOfZe62HIv2YzD{H{b7Q$NL03*C$=)WP3a> zON*P`d8u(-B@jXVDIPC|4=TQ$rSD#?)`fpm&LWEJ2ildLNuAr zo!=hrvWG4<=To=e*N*o|yWiR7XoE5pEu`385M(K&9f*Rgfs%I6;@gq`N6dUjUS_0| z!r@cTMoAYXo!A8+G(9N<3xx-Z#md%4i#H~svVwUlbjnZ@1Pi4Gi{-(qz?ut!%15VY z(l?IaL!of5JI-}ri||m8rJ2?Z3LB~$#mUZT;gePP6r-hl8ZIG@Iq!^nGaP?iRpEDS z$*<-}RpF>1I91O%a|_~7I)y7G-ZKZIH?>#c{J?SNwUEi&!fisbF6PKQNfzkk9CNf4 zKtx(OQCT>$NFHB&m3TJ=#qS;*ZQxr%5mjmp5(b8(^MjoJ?E+XAGZk@>q2C#Ctt|{( z!FO_`^!G&C)|~qcM+&dcacn^$Es-ckW+{gXsuW+c(atn=+#$t@`HK82?y#2=rS;th z+^0&%!vWx?swx?eblt~=zkqWRulw;Bs$R*_lg1f^qj>0AFGooR@gi>k@yijlLuZ@@ zrEOOjCuQ}7QpF7^mcCrm@t7W}}nkT`zs zA};(@Y(UW-!RuI0osy%s=$Xy@MO^sjGd@cl5I|G98PoiFl4&vX5>78r3yKiOni-#? zT0HnN#!Khjm5k3T__0N7HRGl8?mEU73R{70VSGM2qzWJ+I~i|a&x{+N0YAQB7W+%u z=N9l!Ha`1UPnDA2nMGF?xS`6`K7F;3nSZawzoPL*E%28be_Z3vdWY{|Sbw>?RpX-n zgvLd`;7@5>^b7us#znv2&oW-xo~IeFREeN)I`jqLleNRYYJTth8fAQzBDb15yw2&_ z>JXMPcX*5OIs6;28-JVe(sA`bp!$q8) zugKfz4wrGdbiHi>K3PAmV*aXwVaUv|XnHNW6e_Dzh}DfwMW@ZF5h(&JiC z{C>{(Z1pbM+a2PJFHpPOAmX<%K1V(1!844Pj*~p&^Hjfwe~9tYadJ1~#eNsDKVW=5 zd-Mb&dJp5J?R*95W17YGl=1T*@tf0?d@m-thJn|io#lHs!ACSM{(+dzctqo}{?PtM z;k%5No(I0ic_|2n8B$?_B@XS8z9hV} zy%~t%<*++tL~}V98IKP1+L3598{a1>{&Y&`z*0NY)02zi)>ech8i|p4_(`ab8^KXV zza0q!|Bl49)dgkYyA$ zTe+E9+tu1Fkv;LXYn|Wrva8p0;f4>ZTHq}1$=JousqAR7*T(HM8A+F9rIPu4Dn20> ztdc3*iA>?jjj}9a!4j}-A zR&_l0%I>^n>*_6CYgNIM?|hWVBgJvve`=pBV%!ve$l&gBeK9wsm)!f)f%8MbBx~ev z1JUt-9xSgMMs-A}_B-fYhqsVGgznpP#1nbBZwi%tUS#AvENRm3mY?c-U*VViPtr|@ z(2vYgeuv)wg`U)mR0h>k%6IPfO40YY@8O#sk;kFN37RgNm@LR^SkZU8^lP2S%Xc?I zH+bYd>iPaaCX2j$=Oa|U^U-R!4iYE21rO;I`B7bfQ2CBY%JtU&vs!))Cfc7mr=r$CM}kh+Dq!os^dMo>yz! z#vo4TU%D&mX;bhI$p063+sm8&bI9_yRiG5)y#A~%-&>wn{xQf<-KBgv&&vNr$p1gk z{G}lBqW1(as=vs~_uC2WX>BJVR}guDo1M(efR_sHs=}?>zGI)8(j^_wsww zry)<6*e`ykF)i=->vZ9ox0q7u1tiFRZxH$SJei)P8ik-2BVDg*2SS@?zQZ}}J0JA$bx zzA~AvvjP^^zvS&<-O$}*BkG{p7lrMg_563;_mWoeI?!R1`+r2Zk$5? zXIkFzw-%Ln<#$aXzg;`55=BvtdB=Y*STfjPFkVSmwq+ZGWXr~~W!c!6_%Zadt6fQpSG!_& zWh@}^E0a1y2`IFM=_JK&nZaftWm-D1C_|j$zS1%=v?Qbh)Zr1}{GrIRX=XbuxdEN8)_Gtgw_I0LVC<&JOzM|CWY6of78R3YIfHbQnRfhLv zYMu%q9bnAKZ*~ZBop96~O~V?uKuPYb&iB)qnop>dkSNJb6iyd8IYL#XlP0+!9>Jx4 zLd&Q?2tvJbRL3(VoUP7WIhxk%@(cCLsd9&3tr|_G9d6LD$dNj-H>~XqYkNYE>V^<1 zdbYlqk5jvRW;v-55Tre~Xu1C&qX>R_l>;lG()Kq%PK#)c&qqj}b?Ek1J~~^g5i0c) zx!IcEPv>blp=<+KT`C!?OT`!U_UBTIqS0(;&(g+7E)!|c{AHlTOYO0F{pM{N<}~3` z@sP>uJGQHbKl$c&UthiQ?pLnKezN+>_A}>qWQ2VTrEZ@*234vCSnW9Yk_qtZfL}5W zf5!y;RFBE3Le`%7`9#+4%0_z=;O@!* zjpjw7b6=FA(Nyw|1Tx}@@dQZi!i9FOCz?&f(bb9oq8#M=we$d4Ki$D!(%+fjQ*}F? z_2Exz{v-N>5dH%*96k5>aOsB!eYo2XdR`DcGEW@U{07P?ZAcc5dvN)52!FzZ>#7l~ zPI_=}AHAe;6U@~263%*Xuiaq}?zQ`#2lv|j*n@lRwsoxAydsDG%XHcCzI=9febdcK zb+m0>7h91ZuoKx#BECD@w%WeFV{Kb&M~gx(%ftrq2|JTTpYLvOyYa^Mwf5SM*0z=o z$&bd8yRnubsf=0H$_1cQp2{kvT0tMf$M`hpHB&Ihfi{EYLFxOZVBE2FwKDFSW6Ua} zPn737nuAVb{fHCLx~R3Hetp@+m49x2H(<7w)zq)A0Ql-Fl;sJhwo&200)$`}?G*6sAtOk!BuKD=*Vf*8-(R}XsnwpawpQ{Fki`2Pe zYwVM+6*ZqD9kr#<0oqyVP_#wmn^w)E_bc_s=kHv70y+mrp!<|%|Afjga+quwC(f@? zgP1R#2;Z>oT-Z?W4_7}l^Z2thCxM*_&&@CIQEDlb3v=YuF}j zT+}?|maS#U@*~PA+iqQ!;nL=zf7Tf6oKXkYo!g=Fs9ussbvcz9soXwJ9jMJlYShr# zS!(F#rRvdcsYOP4^Eu7)ZnKhBM+Rv^M}X1{VaKeOF#HA8kZE_gC2ZBPgLEy?p)>S z#e=nG%}=Oq@^D@nc`+xC(^;esdD454{tHj~4y6Cyll~gguQ_SQ>ex5N4u>vHC4IY2 z&(Re9$l*65BfMgOkD-e+E=1PB_w>goau~lHiZ!ENF3_=#K9SSgYxS|79A&QEr{!dh zJ*DSSnWscv_Fm1JvD9Zz=G>GnAHFXR`!)UWw@!IvGLboDo{s3{%-!$K(sEKy*+$5^ z$#Wif(0?4tG?n#Fw!!YYr}Muoa*HnDXHl_lR2lQ29c@#tn?bfsj3JJcAp_}k33I@YPMZrL*laL-l ziZL&2L^qSM7ytunDC{YfiQQV4?@4Ckb{{sv19fyxsY|A@3GPkClhJ%4hZ9V?e;;Sq zxooH1m&xW6@w(1zzD}QKD4vYuRNeMmBAcu0%k^|cJM)?BK;7+`?4Dd-v@=n6M=~Bw zC+yq+_Se03U$QR&wF&IFQ;BFUQJ3klbMZa)(nx)zzOFOV+m}owm}b(6bl&dF#KF^_ zuLBbwlWG^l6ML_y?My|}-L-pRHJM4*HY|a7W9@=eYH#nNzHH{!L}$LXpD5i~ z#f#(dXe0h&4a*V@jfwcu#+^r{QrY zkY|yg<~#_>ebQu$^uk$Dn;OW zybKjw2@+a^SJ2271%iQaC|sm!AV#|Wgmkr|9oB;3cQwM&{~tz}x|W&@P4uKR_fY`f zM^hC>%|0NsO)wqDK*my4mO<;A(Hp=Jqy?E)*DIlcLJ1@fA$}6iUn713^fu7fKp#bZ zGalim{`e}MkMLN?r5T|lc#Xd&f1&x7im|hwpohnk7&ty`aJvgh^c|UQPFS3g;C6T*@rY1ZwoaWOk zM>7X!&~%Z_iyvkY&uoh_JsFzhSWZEG_QHw_%YCW-91O!lkjvOT(R4gT?gE-zlI%N( zD|CXtS#?^2m`H#8)A`}3)s-Jf-;}P@n@VvGeQ8T!dosNzkzG;S-rlNL zRduf}!xm7(Fn&`}P6XdJRs)LFnDoIvhlGXrq~IK+5Ega)>#Z*jzk1W-^Diq7ELeEs zD&kJ%_eB2lnfTL>zV*g4AKmbkNvhbCrnD-IV0lS+RWKAf6r2&-7@Qf}5G>vRvN2eT zs2P4EXoNPJm9W_h{nRYWK*ueTK}FKVF%D8$hkvkY(+4kn`p~zUhfY1R;<3p%Z4{ew zMzBhajT_;RGRWywY|2V+O%r(dGY!%ND+aTOtF5o^{ML`A+wy^b&Ky)L;>+77QH@SvPG6R)wsEPX&uQLe>q309y4Q1dSsmHiu5JWRO@tQ!Ks3 zBmI~AHv}hztXCD}HwS~z_nrNRpxk;#Z3tNpAU-u@{X&tg4c4DJS?^QUy@GR+SMnN~RP>iF}I~CBb0H-vy10+I z74HkITwipaljZS(N?KU#E=Y~%AVJ$aXr%=|no8vd`VzTF57vI({SV}_nT&hx@UGwe zxh#$?ovD5t6k^GI?!wk^PkHEIb0Ky6eXV$3wELZ9?i!4DQ_)0<)doSPLRx_+$Q&qX z2Q9oE`5)uUcjV=ZbW%#m*C^?Nq!YUsgg#FSMXvFpf>~MmX!6E~sH~#A6+UgKQG%}3 zqJlkm6&jOwBZJP*`74D^7P#3!luwrx_i~r{OWg zDbD*N{tU-oSML7qE%_B3DR;LTf>ZUJJ+~kZrCqpE;yrUPdQ*Ejzp!z{eOg0>eaaX(W#izDm5Cg5q}&j@Ixgp@1s28VQ3AL)xz>C1_)& z0xmN2J0y;^?!Xm%8%M_e-bl-ubDrUd`?{H9bKSH=VjP*S94e?%w1iPR)2HJNDNf8$ znl1sm7%|g3}Ju zP*tuUSSAw3&t1TUznlwDD39QiS#AR^lsuVK%O z7rzGIn_(9EOWJ2A_{STcx3QjbCBHk1uD!qwRigUz4+QeN#UE+>8yc4x;W3Rrtnpf{ z=*JpAt#M}_#CQV^--4@Ov#XzLT=Wb6mc~WD;J?(k=og&488TZ6M)X6*%hW#K{Pqd( z@!H42RK*@vAAgwf>5ANK-cZi?40V`kA!jnDXYy~w1r%Ny#>bAI*^JLtw|JQmpUe2z z@pBpDbJ&aH6JJwSL+yw(focq!^N z9)1zdt>T=V3wM#|NZ24#mv*~YFD?m!6DL;k!xkL7ToE@3j)LbI1@%@>yG?=WT58WUfN?4d zw1Hcwfps)+9UHij8d$!8o2j8mt=SaalUTE3#~SC?y==?swn%4RA2m_EvUjyNETutV zd+uftdbNWVZeQ84WVemOYch%(d&pnTQl9)&GM`T+JS z*^oP`=c)KkH;-;d@Og(x!Be8U)$bYS9)gg!FMK+X96i(kxUKJa_?6wdv!i8G+ZyG1 z{GH2WEP771X&7ubV)G z&fm0E6L~p*3YGO;WMqFVY0~eNpXh$D@XNX<=|zapkIrIW?$roAr5VWv)l=*{=Y8z6 zfPI8JJ|d6Jj1x4SH#+IcYgo}eU;6b<ha2+I0 zbQ2!ZDfZ=FlTf+WBzFDv|B9Ah4N5`oH3{A4z-)ugB!2lQFz@)0^NY}!mKP%Blr-Js z@XAkgk6rks9+JM@C-0uu_4(nnFls907x>SpH-#pL zxNpDxXOQESmwS7sw0yHG=|ueU|KyXGpE9xa=HTb|r=J5CVN!ni2{f$b9six9i{5_u z7l7#|DlieBmjB3?=}W4!6UhHoI}RFs1J*D9s|n<%zE3-< zdJ3W(^N-(d$or;P=6gxYH|cp~KG04)A}{NA2J*fpX8Fo1n5cuk0{fC`U;_EUD;@dU Jeaijv{{#Ma@_hgR diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake deleted file mode 100755 index add56f6..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake +++ /dev/null @@ -1,15 +0,0 @@ -set(CMAKE_HOST_SYSTEM "Darwin-25.5.0") -set(CMAKE_HOST_SYSTEM_NAME "Darwin") -set(CMAKE_HOST_SYSTEM_VERSION "25.5.0") -set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") - -include("/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/build/cmake/hmos.toolchain.cmake") - -set(CMAKE_SYSTEM "OHOS-1") -set(CMAKE_SYSTEM_NAME "OHOS") -set(CMAKE_SYSTEM_VERSION "1") -set(CMAKE_SYSTEM_PROCESSOR "aarch64") - -set(CMAKE_CROSSCOMPILING "TRUE") - -set(CMAKE_SYSTEM_LOADED 1) diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c deleted file mode 100644 index 0a0ec9b..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c +++ /dev/null @@ -1,880 +0,0 @@ -#ifdef __cplusplus -# error "A C++ compiler has been selected for C." -#endif - -#if defined(__18CXX) -# define ID_VOID_MAIN -#endif -#if defined(__CLASSIC_C__) -/* cv-qualifiers did not exist in K&R C */ -# define const -# define volatile -#endif - -#if !defined(__has_include) -/* If the compiler does not have __has_include, pretend the answer is - always no. */ -# define __has_include(x) 0 -#endif - - -/* Version number components: V=Version, R=Revision, P=Patch - Version date components: YYYY=Year, MM=Month, DD=Day */ - -#if defined(__INTEL_COMPILER) || defined(__ICC) -# define COMPILER_ID "Intel" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# if defined(__GNUC__) -# define SIMULATE_ID "GNU" -# endif - /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, - except that a few beta releases use the old format with V=2021. */ -# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 -# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) -# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) -# if defined(__INTEL_COMPILER_UPDATE) -# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) -# else -# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) -# endif -# else -# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) -# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) - /* The third version component from --version is an update index, - but no macro is provided for it. */ -# define COMPILER_VERSION_PATCH DEC(0) -# endif -# if defined(__INTEL_COMPILER_BUILD_DATE) - /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ -# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) -# endif -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# if defined(__GNUC__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# elif defined(__GNUG__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) -# endif -# if defined(__GNUC_MINOR__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) -# define COMPILER_ID "IntelLLVM" -#if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -#endif -#if defined(__GNUC__) -# define SIMULATE_ID "GNU" -#endif -/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and - * later. Look for 6 digit vs. 8 digit version number to decide encoding. - * VVVV is no smaller than the current year when a version is released. - */ -#if __INTEL_LLVM_COMPILER < 1000000L -# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) -# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) -#else -# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) -# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) -#endif -#if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -#endif -#if defined(__GNUC__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -#elif defined(__GNUG__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) -#endif -#if defined(__GNUC_MINOR__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -#endif -#if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -#endif - -#elif defined(__PATHCC__) -# define COMPILER_ID "PathScale" -# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) -# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) -# if defined(__PATHCC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) -# endif - -#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) -# define COMPILER_ID "Embarcadero" -# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) -# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) -# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) - -#elif defined(__BORLANDC__) -# define COMPILER_ID "Borland" - /* __BORLANDC__ = 0xVRR */ -# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) -# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) - -#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 -# define COMPILER_ID "Watcom" - /* __WATCOMC__ = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__WATCOMC__) -# define COMPILER_ID "OpenWatcom" - /* __WATCOMC__ = VVRP + 1100 */ -# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__SUNPRO_C) -# define COMPILER_ID "SunPro" -# if __SUNPRO_C >= 0x5100 - /* __SUNPRO_C = 0xVRRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) -# else - /* __SUNPRO_CC = 0xVRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) -# endif - -#elif defined(__HP_cc) -# define COMPILER_ID "HP" - /* __HP_cc = VVRRPP */ -# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) -# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) - -#elif defined(__DECC) -# define COMPILER_ID "Compaq" - /* __DECC_VER = VVRRTPPPP */ -# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) -# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) -# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) - -#elif defined(__IBMC__) && defined(__COMPILER_VER__) -# define COMPILER_ID "zOS" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__open_xl__) && defined(__clang__) -# define COMPILER_ID "IBMClang" -# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) -# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) -# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) - - -#elif defined(__ibmxl__) && defined(__clang__) -# define COMPILER_ID "XLClang" -# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) -# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) -# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) - - -#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 -# define COMPILER_ID "XL" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 -# define COMPILER_ID "VisualAge" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__NVCOMPILER) -# define COMPILER_ID "NVHPC" -# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) -# if defined(__NVCOMPILER_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) -# endif - -#elif defined(__PGI) -# define COMPILER_ID "PGI" -# define COMPILER_VERSION_MAJOR DEC(__PGIC__) -# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) -# if defined(__PGIC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) -# endif - -#elif defined(__clang__) && defined(__cray__) -# define COMPILER_ID "CrayClang" -# define COMPILER_VERSION_MAJOR DEC(__cray_major__) -# define COMPILER_VERSION_MINOR DEC(__cray_minor__) -# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(_CRAYC) -# define COMPILER_ID "Cray" -# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) -# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) - -#elif defined(__TI_COMPILER_VERSION__) -# define COMPILER_ID "TI" - /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ -# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) -# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) -# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) - -#elif defined(__CLANG_FUJITSU) -# define COMPILER_ID "FujitsuClang" -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(__FUJITSU) -# define COMPILER_ID "Fujitsu" -# if defined(__FCC_version__) -# define COMPILER_VERSION __FCC_version__ -# elif defined(__FCC_major__) -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# endif -# if defined(__fcc_version) -# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) -# elif defined(__FCC_VERSION) -# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) -# endif - - -#elif defined(__ghs__) -# define COMPILER_ID "GHS" -/* __GHS_VERSION_NUMBER = VVVVRP */ -# ifdef __GHS_VERSION_NUMBER -# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) -# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) -# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) -# endif - -#elif defined(__TASKING__) -# define COMPILER_ID "Tasking" - # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) - # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) -# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) - -#elif defined(__ORANGEC__) -# define COMPILER_ID "OrangeC" -# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) - -#elif defined(__TINYC__) -# define COMPILER_ID "TinyCC" - -#elif defined(__BCC__) -# define COMPILER_ID "Bruce" - -#elif defined(__SCO_VERSION__) -# define COMPILER_ID "SCO" - -#elif defined(__ARMCC_VERSION) && !defined(__clang__) -# define COMPILER_ID "ARMCC" -#if __ARMCC_VERSION >= 1000000 - /* __ARMCC_VERSION = VRRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#else - /* __ARMCC_VERSION = VRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#endif - - -#elif defined(__clang__) && defined(__apple_build_version__) -# define COMPILER_ID "AppleClang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) - -#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) -# define COMPILER_ID "ARMClang" - # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) -# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) - -#elif defined(__clang__) -# define COMPILER_ID "Clang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif - -#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) -# define COMPILER_ID "LCC" -# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) -# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) -# if defined(__LCC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) -# endif -# if defined(__GNUC__) && defined(__GNUC_MINOR__) -# define SIMULATE_ID "GNU" -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif -# endif - -#elif defined(__GNUC__) -# define COMPILER_ID "GNU" -# define COMPILER_VERSION_MAJOR DEC(__GNUC__) -# if defined(__GNUC_MINOR__) -# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif defined(_MSC_VER) -# define COMPILER_ID "MSVC" - /* _MSC_VER = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) -# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) -# if defined(_MSC_FULL_VER) -# if _MSC_VER >= 1400 - /* _MSC_FULL_VER = VVRRPPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) -# else - /* _MSC_FULL_VER = VVRRPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) -# endif -# endif -# if defined(_MSC_BUILD) -# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) -# endif - -#elif defined(_ADI_COMPILER) -# define COMPILER_ID "ADSP" -#if defined(__VERSIONNUM__) - /* __VERSIONNUM__ = 0xVVRRPPTT */ -# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) -# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) -# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) -# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) -#endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# define COMPILER_ID "IAR" -# if defined(__VER__) && defined(__ICCARM__) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) -# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) -# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) -# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) -# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# endif - -#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) -# define COMPILER_ID "SDCC" -# if defined(__SDCC_VERSION_MAJOR) -# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) -# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) -# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) -# else - /* SDCC = VRP */ -# define COMPILER_VERSION_MAJOR DEC(SDCC/100) -# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) -# define COMPILER_VERSION_PATCH DEC(SDCC % 10) -# endif - - -/* These compilers are either not known or too old to define an - identification macro. Try to identify the platform and guess that - it is the native compiler. */ -#elif defined(__hpux) || defined(__hpua) -# define COMPILER_ID "HP" - -#else /* unknown compiler */ -# define COMPILER_ID "" -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; -#ifdef SIMULATE_ID -char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; -#endif - -#ifdef __QNXNTO__ -char const* qnxnto = "INFO" ":" "qnxnto[]"; -#endif - -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) -char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; -#endif - -#define STRINGIFY_HELPER(X) #X -#define STRINGIFY(X) STRINGIFY_HELPER(X) - -/* Identify known platforms by name. */ -#if defined(__linux) || defined(__linux__) || defined(linux) -# define PLATFORM_ID "Linux" - -#elif defined(__MSYS__) -# define PLATFORM_ID "MSYS" - -#elif defined(__CYGWIN__) -# define PLATFORM_ID "Cygwin" - -#elif defined(__MINGW32__) -# define PLATFORM_ID "MinGW" - -#elif defined(__APPLE__) -# define PLATFORM_ID "Darwin" - -#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) -# define PLATFORM_ID "Windows" - -#elif defined(__FreeBSD__) || defined(__FreeBSD) -# define PLATFORM_ID "FreeBSD" - -#elif defined(__NetBSD__) || defined(__NetBSD) -# define PLATFORM_ID "NetBSD" - -#elif defined(__OpenBSD__) || defined(__OPENBSD) -# define PLATFORM_ID "OpenBSD" - -#elif defined(__sun) || defined(sun) -# define PLATFORM_ID "SunOS" - -#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) -# define PLATFORM_ID "AIX" - -#elif defined(__hpux) || defined(__hpux__) -# define PLATFORM_ID "HP-UX" - -#elif defined(__HAIKU__) -# define PLATFORM_ID "Haiku" - -#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) -# define PLATFORM_ID "BeOS" - -#elif defined(__QNX__) || defined(__QNXNTO__) -# define PLATFORM_ID "QNX" - -#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) -# define PLATFORM_ID "Tru64" - -#elif defined(__riscos) || defined(__riscos__) -# define PLATFORM_ID "RISCos" - -#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) -# define PLATFORM_ID "SINIX" - -#elif defined(__UNIX_SV__) -# define PLATFORM_ID "UNIX_SV" - -#elif defined(__bsdos__) -# define PLATFORM_ID "BSDOS" - -#elif defined(_MPRAS) || defined(MPRAS) -# define PLATFORM_ID "MP-RAS" - -#elif defined(__osf) || defined(__osf__) -# define PLATFORM_ID "OSF1" - -#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) -# define PLATFORM_ID "SCO_SV" - -#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) -# define PLATFORM_ID "ULTRIX" - -#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) -# define PLATFORM_ID "Xenix" - -#elif defined(__WATCOMC__) -# if defined(__LINUX__) -# define PLATFORM_ID "Linux" - -# elif defined(__DOS__) -# define PLATFORM_ID "DOS" - -# elif defined(__OS2__) -# define PLATFORM_ID "OS2" - -# elif defined(__WINDOWS__) -# define PLATFORM_ID "Windows3x" - -# elif defined(__VXWORKS__) -# define PLATFORM_ID "VxWorks" - -# else /* unknown platform */ -# define PLATFORM_ID -# endif - -#elif defined(__INTEGRITY) -# if defined(INT_178B) -# define PLATFORM_ID "Integrity178" - -# else /* regular Integrity */ -# define PLATFORM_ID "Integrity" -# endif - -# elif defined(_ADI_COMPILER) -# define PLATFORM_ID "ADSP" - -#else /* unknown platform */ -# define PLATFORM_ID - -#endif - -/* For windows compilers MSVC and Intel we can determine - the architecture of the compiler being used. This is because - the compilers do not have flags that can change the architecture, - but rather depend on which compiler is being used -*/ -#if defined(_WIN32) && defined(_MSC_VER) -# if defined(_M_IA64) -# define ARCHITECTURE_ID "IA64" - -# elif defined(_M_ARM64EC) -# define ARCHITECTURE_ID "ARM64EC" - -# elif defined(_M_X64) || defined(_M_AMD64) -# define ARCHITECTURE_ID "x64" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# elif defined(_M_ARM64) -# define ARCHITECTURE_ID "ARM64" - -# elif defined(_M_ARM) -# if _M_ARM == 4 -# define ARCHITECTURE_ID "ARMV4I" -# elif _M_ARM == 5 -# define ARCHITECTURE_ID "ARMV5I" -# else -# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) -# endif - -# elif defined(_M_MIPS) -# define ARCHITECTURE_ID "MIPS" - -# elif defined(_M_SH) -# define ARCHITECTURE_ID "SHx" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__WATCOMC__) -# if defined(_M_I86) -# define ARCHITECTURE_ID "I86" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# if defined(__ICCARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__ICCRX__) -# define ARCHITECTURE_ID "RX" - -# elif defined(__ICCRH850__) -# define ARCHITECTURE_ID "RH850" - -# elif defined(__ICCRL78__) -# define ARCHITECTURE_ID "RL78" - -# elif defined(__ICCRISCV__) -# define ARCHITECTURE_ID "RISCV" - -# elif defined(__ICCAVR__) -# define ARCHITECTURE_ID "AVR" - -# elif defined(__ICC430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__ICCV850__) -# define ARCHITECTURE_ID "V850" - -# elif defined(__ICC8051__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__ICCSTM8__) -# define ARCHITECTURE_ID "STM8" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__ghs__) -# if defined(__PPC64__) -# define ARCHITECTURE_ID "PPC64" - -# elif defined(__ppc__) -# define ARCHITECTURE_ID "PPC" - -# elif defined(__ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__x86_64__) -# define ARCHITECTURE_ID "x64" - -# elif defined(__i386__) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__TI_COMPILER_VERSION__) -# if defined(__TI_ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__MSP430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__TMS320C28XX__) -# define ARCHITECTURE_ID "TMS320C28x" - -# elif defined(__TMS320C6X__) || defined(_TMS320C6X) -# define ARCHITECTURE_ID "TMS320C6x" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -# elif defined(__ADSPSHARC__) -# define ARCHITECTURE_ID "SHARC" - -# elif defined(__ADSPBLACKFIN__) -# define ARCHITECTURE_ID "Blackfin" - -#elif defined(__TASKING__) - -# if defined(__CTC__) || defined(__CPTC__) -# define ARCHITECTURE_ID "TriCore" - -# elif defined(__CMCS__) -# define ARCHITECTURE_ID "MCS" - -# elif defined(__CARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__CARC__) -# define ARCHITECTURE_ID "ARC" - -# elif defined(__C51__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__CPCP__) -# define ARCHITECTURE_ID "PCP" - -# else -# define ARCHITECTURE_ID "" -# endif - -#else -# define ARCHITECTURE_ID -#endif - -/* Convert integer to decimal digit literals. */ -#define DEC(n) \ - ('0' + (((n) / 10000000)%10)), \ - ('0' + (((n) / 1000000)%10)), \ - ('0' + (((n) / 100000)%10)), \ - ('0' + (((n) / 10000)%10)), \ - ('0' + (((n) / 1000)%10)), \ - ('0' + (((n) / 100)%10)), \ - ('0' + (((n) / 10)%10)), \ - ('0' + ((n) % 10)) - -/* Convert integer to hex digit literals. */ -#define HEX(n) \ - ('0' + ((n)>>28 & 0xF)), \ - ('0' + ((n)>>24 & 0xF)), \ - ('0' + ((n)>>20 & 0xF)), \ - ('0' + ((n)>>16 & 0xF)), \ - ('0' + ((n)>>12 & 0xF)), \ - ('0' + ((n)>>8 & 0xF)), \ - ('0' + ((n)>>4 & 0xF)), \ - ('0' + ((n) & 0xF)) - -/* Construct a string literal encoding the version number. */ -#ifdef COMPILER_VERSION -char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; - -/* Construct a string literal encoding the version number components. */ -#elif defined(COMPILER_VERSION_MAJOR) -char const info_version[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', - COMPILER_VERSION_MAJOR, -# ifdef COMPILER_VERSION_MINOR - '.', COMPILER_VERSION_MINOR, -# ifdef COMPILER_VERSION_PATCH - '.', COMPILER_VERSION_PATCH, -# ifdef COMPILER_VERSION_TWEAK - '.', COMPILER_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct a string literal encoding the internal version number. */ -#ifdef COMPILER_VERSION_INTERNAL -char const info_version_internal[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', - 'i','n','t','e','r','n','a','l','[', - COMPILER_VERSION_INTERNAL,']','\0'}; -#elif defined(COMPILER_VERSION_INTERNAL_STR) -char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; -#endif - -/* Construct a string literal encoding the version number components. */ -#ifdef SIMULATE_VERSION_MAJOR -char const info_simulate_version[] = { - 'I', 'N', 'F', 'O', ':', - 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', - SIMULATE_VERSION_MAJOR, -# ifdef SIMULATE_VERSION_MINOR - '.', SIMULATE_VERSION_MINOR, -# ifdef SIMULATE_VERSION_PATCH - '.', SIMULATE_VERSION_PATCH, -# ifdef SIMULATE_VERSION_TWEAK - '.', SIMULATE_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; -char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; - - - -#if !defined(__STDC__) && !defined(__clang__) -# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) -# define C_VERSION "90" -# else -# define C_VERSION -# endif -#elif __STDC_VERSION__ > 201710L -# define C_VERSION "23" -#elif __STDC_VERSION__ >= 201710L -# define C_VERSION "17" -#elif __STDC_VERSION__ >= 201000L -# define C_VERSION "11" -#elif __STDC_VERSION__ >= 199901L -# define C_VERSION "99" -#else -# define C_VERSION "90" -#endif -const char* info_language_standard_default = - "INFO" ":" "standard_default[" C_VERSION "]"; - -const char* info_language_extensions_default = "INFO" ":" "extensions_default[" -#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ - defined(__TI_COMPILER_VERSION__)) && \ - !defined(__STRICT_ANSI__) - "ON" -#else - "OFF" -#endif -"]"; - -/*--------------------------------------------------------------------------*/ - -#ifdef ID_VOID_MAIN -void main() {} -#else -# if defined(__CLASSIC_C__) -int main(argc, argv) int argc; char *argv[]; -# else -int main(int argc, char* argv[]) -# endif -{ - int require = 0; - require += info_compiler[argc]; - require += info_platform[argc]; - require += info_arch[argc]; -#ifdef COMPILER_VERSION_MAJOR - require += info_version[argc]; -#endif -#ifdef COMPILER_VERSION_INTERNAL - require += info_version_internal[argc]; -#endif -#ifdef SIMULATE_ID - require += info_simulate[argc]; -#endif -#ifdef SIMULATE_VERSION_MAJOR - require += info_simulate_version[argc]; -#endif -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) - require += info_cray[argc]; -#endif - require += info_language_standard_default[argc]; - require += info_language_extensions_default[argc]; - (void)argv; - return require; -} -#endif diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o deleted file mode 100644 index e8717290bb4e80438abce4e1c20b9974b50881a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3296 zcmb`JO=uHA6vrpFYD(2wzfdX~&_flvWZSff;vrH$p|+w3N+{_jyV2NWQ@7g^MNm-C zqkU)#-+{x@liaPDizWL36=DnFr-sHy7v16-Z zF~KD!UWlN@REVjyft{6RR%`?1@-6XpdADeM=o5{P-QwqRk7&e&xEGIQe}3(|+2|^l zGhceLjd-l9`^!J{;atsm#r^*^=Ld)j;=jMpk$-=3{Mf|7Vy!w;t~g$9v|_mzCxwJF z70WNxylQT&?9SB*T3+!|j_g_Aa&61A^R`p6<|=;9OjD0j_Z@e(TytlC_f3pX1{(=h zi{Jdbs9DC$|ZD$JUR53Z6DWo#{*vUpRV%N@y*!5kp zzID3=vhBnd@qJ`Nf|{Y%cOaeO`*NrQKnMKk<+MDMVUD^Ev09glLsB2+Y01}w^n;Sa z3De4eeV>u2_uL#z~iayDMbur(9gP?4Nbp*Y^`M~{9d9o>&dOll- zQp8cO@-aoP?%rudukwnb-$XXPX2{+O^Q-(eag1O66z&qocvSva(ev67Jt2;Il@k`2 zdONwHyqRxR7@ExYx8WmgIPc3&>R%0hSE*XVhioMbNP$(y1;cYHmSJ1IWt81gEsrT= z&bsL2!#SWI-(*0eJ^N*Vv8Z4`wu>wCUcfcMoZzW3V30jx4urLh{pY8a~&Rzp|~Vr9bg z%@x2LCN%ffjikE0e`|Nb=Mdbl`hVTV|I-NncpK^d54Z8ZxI+Jj+W23H@Q=5f?jJwb zsP(^Dq5q9-{4YiL$NNzCk3YN7m7YwN$o~cCQww1^2Kg&^n5jBbKakX^Yd_O?U7jJ( quTBa!$Iti^7*Xf%Tg3YDKIQRz{?>24 & 0x00FF) -# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) -# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) - -#elif defined(__BORLANDC__) -# define COMPILER_ID "Borland" - /* __BORLANDC__ = 0xVRR */ -# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) -# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) - -#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 -# define COMPILER_ID "Watcom" - /* __WATCOMC__ = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__WATCOMC__) -# define COMPILER_ID "OpenWatcom" - /* __WATCOMC__ = VVRP + 1100 */ -# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__SUNPRO_CC) -# define COMPILER_ID "SunPro" -# if __SUNPRO_CC >= 0x5100 - /* __SUNPRO_CC = 0xVRRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>12) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xFF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) -# else - /* __SUNPRO_CC = 0xVRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>8) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) -# endif - -#elif defined(__HP_aCC) -# define COMPILER_ID "HP" - /* __HP_aCC = VVRRPP */ -# define COMPILER_VERSION_MAJOR DEC(__HP_aCC/10000) -# define COMPILER_VERSION_MINOR DEC(__HP_aCC/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__HP_aCC % 100) - -#elif defined(__DECCXX) -# define COMPILER_ID "Compaq" - /* __DECCXX_VER = VVRRTPPPP */ -# define COMPILER_VERSION_MAJOR DEC(__DECCXX_VER/10000000) -# define COMPILER_VERSION_MINOR DEC(__DECCXX_VER/100000 % 100) -# define COMPILER_VERSION_PATCH DEC(__DECCXX_VER % 10000) - -#elif defined(__IBMCPP__) && defined(__COMPILER_VER__) -# define COMPILER_ID "zOS" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__open_xl__) && defined(__clang__) -# define COMPILER_ID "IBMClang" -# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) -# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) -# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) - - -#elif defined(__ibmxl__) && defined(__clang__) -# define COMPILER_ID "XLClang" -# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) -# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) -# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) - - -#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ >= 800 -# define COMPILER_ID "XL" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ < 800 -# define COMPILER_ID "VisualAge" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__NVCOMPILER) -# define COMPILER_ID "NVHPC" -# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) -# if defined(__NVCOMPILER_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) -# endif - -#elif defined(__PGI) -# define COMPILER_ID "PGI" -# define COMPILER_VERSION_MAJOR DEC(__PGIC__) -# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) -# if defined(__PGIC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) -# endif - -#elif defined(__clang__) && defined(__cray__) -# define COMPILER_ID "CrayClang" -# define COMPILER_VERSION_MAJOR DEC(__cray_major__) -# define COMPILER_VERSION_MINOR DEC(__cray_minor__) -# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(_CRAYC) -# define COMPILER_ID "Cray" -# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) -# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) - -#elif defined(__TI_COMPILER_VERSION__) -# define COMPILER_ID "TI" - /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ -# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) -# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) -# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) - -#elif defined(__CLANG_FUJITSU) -# define COMPILER_ID "FujitsuClang" -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(__FUJITSU) -# define COMPILER_ID "Fujitsu" -# if defined(__FCC_version__) -# define COMPILER_VERSION __FCC_version__ -# elif defined(__FCC_major__) -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# endif -# if defined(__fcc_version) -# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) -# elif defined(__FCC_VERSION) -# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) -# endif - - -#elif defined(__ghs__) -# define COMPILER_ID "GHS" -/* __GHS_VERSION_NUMBER = VVVVRP */ -# ifdef __GHS_VERSION_NUMBER -# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) -# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) -# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) -# endif - -#elif defined(__TASKING__) -# define COMPILER_ID "Tasking" - # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) - # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) -# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) - -#elif defined(__ORANGEC__) -# define COMPILER_ID "OrangeC" -# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) - -#elif defined(__SCO_VERSION__) -# define COMPILER_ID "SCO" - -#elif defined(__ARMCC_VERSION) && !defined(__clang__) -# define COMPILER_ID "ARMCC" -#if __ARMCC_VERSION >= 1000000 - /* __ARMCC_VERSION = VRRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#else - /* __ARMCC_VERSION = VRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#endif - - -#elif defined(__clang__) && defined(__apple_build_version__) -# define COMPILER_ID "AppleClang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) - -#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) -# define COMPILER_ID "ARMClang" - # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) -# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) - -#elif defined(__clang__) -# define COMPILER_ID "Clang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif - -#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) -# define COMPILER_ID "LCC" -# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) -# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) -# if defined(__LCC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) -# endif -# if defined(__GNUC__) && defined(__GNUC_MINOR__) -# define SIMULATE_ID "GNU" -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif -# endif - -#elif defined(__GNUC__) || defined(__GNUG__) -# define COMPILER_ID "GNU" -# if defined(__GNUC__) -# define COMPILER_VERSION_MAJOR DEC(__GNUC__) -# else -# define COMPILER_VERSION_MAJOR DEC(__GNUG__) -# endif -# if defined(__GNUC_MINOR__) -# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif defined(_MSC_VER) -# define COMPILER_ID "MSVC" - /* _MSC_VER = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) -# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) -# if defined(_MSC_FULL_VER) -# if _MSC_VER >= 1400 - /* _MSC_FULL_VER = VVRRPPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) -# else - /* _MSC_FULL_VER = VVRRPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) -# endif -# endif -# if defined(_MSC_BUILD) -# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) -# endif - -#elif defined(_ADI_COMPILER) -# define COMPILER_ID "ADSP" -#if defined(__VERSIONNUM__) - /* __VERSIONNUM__ = 0xVVRRPPTT */ -# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) -# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) -# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) -# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) -#endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# define COMPILER_ID "IAR" -# if defined(__VER__) && defined(__ICCARM__) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) -# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) -# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) -# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) -# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# endif - - -/* These compilers are either not known or too old to define an - identification macro. Try to identify the platform and guess that - it is the native compiler. */ -#elif defined(__hpux) || defined(__hpua) -# define COMPILER_ID "HP" - -#else /* unknown compiler */ -# define COMPILER_ID "" -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; -#ifdef SIMULATE_ID -char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; -#endif - -#ifdef __QNXNTO__ -char const* qnxnto = "INFO" ":" "qnxnto[]"; -#endif - -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) -char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; -#endif - -#define STRINGIFY_HELPER(X) #X -#define STRINGIFY(X) STRINGIFY_HELPER(X) - -/* Identify known platforms by name. */ -#if defined(__linux) || defined(__linux__) || defined(linux) -# define PLATFORM_ID "Linux" - -#elif defined(__MSYS__) -# define PLATFORM_ID "MSYS" - -#elif defined(__CYGWIN__) -# define PLATFORM_ID "Cygwin" - -#elif defined(__MINGW32__) -# define PLATFORM_ID "MinGW" - -#elif defined(__APPLE__) -# define PLATFORM_ID "Darwin" - -#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) -# define PLATFORM_ID "Windows" - -#elif defined(__FreeBSD__) || defined(__FreeBSD) -# define PLATFORM_ID "FreeBSD" - -#elif defined(__NetBSD__) || defined(__NetBSD) -# define PLATFORM_ID "NetBSD" - -#elif defined(__OpenBSD__) || defined(__OPENBSD) -# define PLATFORM_ID "OpenBSD" - -#elif defined(__sun) || defined(sun) -# define PLATFORM_ID "SunOS" - -#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) -# define PLATFORM_ID "AIX" - -#elif defined(__hpux) || defined(__hpux__) -# define PLATFORM_ID "HP-UX" - -#elif defined(__HAIKU__) -# define PLATFORM_ID "Haiku" - -#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) -# define PLATFORM_ID "BeOS" - -#elif defined(__QNX__) || defined(__QNXNTO__) -# define PLATFORM_ID "QNX" - -#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) -# define PLATFORM_ID "Tru64" - -#elif defined(__riscos) || defined(__riscos__) -# define PLATFORM_ID "RISCos" - -#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) -# define PLATFORM_ID "SINIX" - -#elif defined(__UNIX_SV__) -# define PLATFORM_ID "UNIX_SV" - -#elif defined(__bsdos__) -# define PLATFORM_ID "BSDOS" - -#elif defined(_MPRAS) || defined(MPRAS) -# define PLATFORM_ID "MP-RAS" - -#elif defined(__osf) || defined(__osf__) -# define PLATFORM_ID "OSF1" - -#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) -# define PLATFORM_ID "SCO_SV" - -#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) -# define PLATFORM_ID "ULTRIX" - -#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) -# define PLATFORM_ID "Xenix" - -#elif defined(__WATCOMC__) -# if defined(__LINUX__) -# define PLATFORM_ID "Linux" - -# elif defined(__DOS__) -# define PLATFORM_ID "DOS" - -# elif defined(__OS2__) -# define PLATFORM_ID "OS2" - -# elif defined(__WINDOWS__) -# define PLATFORM_ID "Windows3x" - -# elif defined(__VXWORKS__) -# define PLATFORM_ID "VxWorks" - -# else /* unknown platform */ -# define PLATFORM_ID -# endif - -#elif defined(__INTEGRITY) -# if defined(INT_178B) -# define PLATFORM_ID "Integrity178" - -# else /* regular Integrity */ -# define PLATFORM_ID "Integrity" -# endif - -# elif defined(_ADI_COMPILER) -# define PLATFORM_ID "ADSP" - -#else /* unknown platform */ -# define PLATFORM_ID - -#endif - -/* For windows compilers MSVC and Intel we can determine - the architecture of the compiler being used. This is because - the compilers do not have flags that can change the architecture, - but rather depend on which compiler is being used -*/ -#if defined(_WIN32) && defined(_MSC_VER) -# if defined(_M_IA64) -# define ARCHITECTURE_ID "IA64" - -# elif defined(_M_ARM64EC) -# define ARCHITECTURE_ID "ARM64EC" - -# elif defined(_M_X64) || defined(_M_AMD64) -# define ARCHITECTURE_ID "x64" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# elif defined(_M_ARM64) -# define ARCHITECTURE_ID "ARM64" - -# elif defined(_M_ARM) -# if _M_ARM == 4 -# define ARCHITECTURE_ID "ARMV4I" -# elif _M_ARM == 5 -# define ARCHITECTURE_ID "ARMV5I" -# else -# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) -# endif - -# elif defined(_M_MIPS) -# define ARCHITECTURE_ID "MIPS" - -# elif defined(_M_SH) -# define ARCHITECTURE_ID "SHx" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__WATCOMC__) -# if defined(_M_I86) -# define ARCHITECTURE_ID "I86" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# if defined(__ICCARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__ICCRX__) -# define ARCHITECTURE_ID "RX" - -# elif defined(__ICCRH850__) -# define ARCHITECTURE_ID "RH850" - -# elif defined(__ICCRL78__) -# define ARCHITECTURE_ID "RL78" - -# elif defined(__ICCRISCV__) -# define ARCHITECTURE_ID "RISCV" - -# elif defined(__ICCAVR__) -# define ARCHITECTURE_ID "AVR" - -# elif defined(__ICC430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__ICCV850__) -# define ARCHITECTURE_ID "V850" - -# elif defined(__ICC8051__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__ICCSTM8__) -# define ARCHITECTURE_ID "STM8" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__ghs__) -# if defined(__PPC64__) -# define ARCHITECTURE_ID "PPC64" - -# elif defined(__ppc__) -# define ARCHITECTURE_ID "PPC" - -# elif defined(__ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__x86_64__) -# define ARCHITECTURE_ID "x64" - -# elif defined(__i386__) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__TI_COMPILER_VERSION__) -# if defined(__TI_ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__MSP430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__TMS320C28XX__) -# define ARCHITECTURE_ID "TMS320C28x" - -# elif defined(__TMS320C6X__) || defined(_TMS320C6X) -# define ARCHITECTURE_ID "TMS320C6x" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -# elif defined(__ADSPSHARC__) -# define ARCHITECTURE_ID "SHARC" - -# elif defined(__ADSPBLACKFIN__) -# define ARCHITECTURE_ID "Blackfin" - -#elif defined(__TASKING__) - -# if defined(__CTC__) || defined(__CPTC__) -# define ARCHITECTURE_ID "TriCore" - -# elif defined(__CMCS__) -# define ARCHITECTURE_ID "MCS" - -# elif defined(__CARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__CARC__) -# define ARCHITECTURE_ID "ARC" - -# elif defined(__C51__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__CPCP__) -# define ARCHITECTURE_ID "PCP" - -# else -# define ARCHITECTURE_ID "" -# endif - -#else -# define ARCHITECTURE_ID -#endif - -/* Convert integer to decimal digit literals. */ -#define DEC(n) \ - ('0' + (((n) / 10000000)%10)), \ - ('0' + (((n) / 1000000)%10)), \ - ('0' + (((n) / 100000)%10)), \ - ('0' + (((n) / 10000)%10)), \ - ('0' + (((n) / 1000)%10)), \ - ('0' + (((n) / 100)%10)), \ - ('0' + (((n) / 10)%10)), \ - ('0' + ((n) % 10)) - -/* Convert integer to hex digit literals. */ -#define HEX(n) \ - ('0' + ((n)>>28 & 0xF)), \ - ('0' + ((n)>>24 & 0xF)), \ - ('0' + ((n)>>20 & 0xF)), \ - ('0' + ((n)>>16 & 0xF)), \ - ('0' + ((n)>>12 & 0xF)), \ - ('0' + ((n)>>8 & 0xF)), \ - ('0' + ((n)>>4 & 0xF)), \ - ('0' + ((n) & 0xF)) - -/* Construct a string literal encoding the version number. */ -#ifdef COMPILER_VERSION -char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; - -/* Construct a string literal encoding the version number components. */ -#elif defined(COMPILER_VERSION_MAJOR) -char const info_version[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', - COMPILER_VERSION_MAJOR, -# ifdef COMPILER_VERSION_MINOR - '.', COMPILER_VERSION_MINOR, -# ifdef COMPILER_VERSION_PATCH - '.', COMPILER_VERSION_PATCH, -# ifdef COMPILER_VERSION_TWEAK - '.', COMPILER_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct a string literal encoding the internal version number. */ -#ifdef COMPILER_VERSION_INTERNAL -char const info_version_internal[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', - 'i','n','t','e','r','n','a','l','[', - COMPILER_VERSION_INTERNAL,']','\0'}; -#elif defined(COMPILER_VERSION_INTERNAL_STR) -char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; -#endif - -/* Construct a string literal encoding the version number components. */ -#ifdef SIMULATE_VERSION_MAJOR -char const info_simulate_version[] = { - 'I', 'N', 'F', 'O', ':', - 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', - SIMULATE_VERSION_MAJOR, -# ifdef SIMULATE_VERSION_MINOR - '.', SIMULATE_VERSION_MINOR, -# ifdef SIMULATE_VERSION_PATCH - '.', SIMULATE_VERSION_PATCH, -# ifdef SIMULATE_VERSION_TWEAK - '.', SIMULATE_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; -char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; - - - -#if defined(__INTEL_COMPILER) && defined(_MSVC_LANG) && _MSVC_LANG < 201403L -# if defined(__INTEL_CXX11_MODE__) -# if defined(__cpp_aggregate_nsdmi) -# define CXX_STD 201402L -# else -# define CXX_STD 201103L -# endif -# else -# define CXX_STD 199711L -# endif -#elif defined(_MSC_VER) && defined(_MSVC_LANG) -# define CXX_STD _MSVC_LANG -#else -# define CXX_STD __cplusplus -#endif - -const char* info_language_standard_default = "INFO" ":" "standard_default[" -#if CXX_STD > 202002L - "23" -#elif CXX_STD > 201703L - "20" -#elif CXX_STD >= 201703L - "17" -#elif CXX_STD >= 201402L - "14" -#elif CXX_STD >= 201103L - "11" -#else - "98" -#endif -"]"; - -const char* info_language_extensions_default = "INFO" ":" "extensions_default[" -#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ - defined(__TI_COMPILER_VERSION__)) && \ - !defined(__STRICT_ANSI__) - "ON" -#else - "OFF" -#endif -"]"; - -/*--------------------------------------------------------------------------*/ - -int main(int argc, char* argv[]) -{ - int require = 0; - require += info_compiler[argc]; - require += info_platform[argc]; - require += info_arch[argc]; -#ifdef COMPILER_VERSION_MAJOR - require += info_version[argc]; -#endif -#ifdef COMPILER_VERSION_INTERNAL - require += info_version_internal[argc]; -#endif -#ifdef SIMULATE_ID - require += info_simulate[argc]; -#endif -#ifdef SIMULATE_VERSION_MAJOR - require += info_simulate_version[argc]; -#endif -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) - require += info_cray[argc]; -#endif - require += info_language_standard_default[argc]; - require += info_language_extensions_default[argc]; - (void)argv; - return require; -} diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o deleted file mode 100644 index 4577e9e20c9c2ea3d7535bfe83a259b6bae16be7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3320 zcmb`J!EYN?5Qk^$7E)T;w513srL|BcQlKku;y5nlU=-R?6cPkNO0kNy_O2Tfdu=wG z$e>ECL=QQ(m#Qa}1AhP)1QwTGkScMZJyhaWZs`RHiAyhpnYT09*SBkA1e@f&+3)@4 z&Ahj7XLIe`{P|s$1#(&NJtTKR0s7A-c14*LcoCJ`U%~zD6EJu*0fXQ6!ISNSFc<^4 zIc8O!Jf65d*xPEAw+>bYW7gh%TmNB<=8ETqTmRRbk1#LH|JL1H?cv3R^NVjbx}9FD z?T6L5wi{fz3<~zzZq)3Co$7omSnD%%!^U!z?bXN)JU8@ep5JuW+ELXh@`&G${9v`! z4OVx?78foj8##jYR9>}pKcwlZ1$8+yc5D<1Vqj!ge0TAq{G{ivpUO9A9eMW3lin`o zC)@4y&gov*{mgGfdB>Tlm!`_z^t9)>Mf|Nhv%XXGz0w)aE7xa>jlxX1UM!!AlO5-r ztuH?W>*{f9V*d#s_7d}Z)W35e&$p@W8j{`c$98G}K@NY+>ykQ*Y5p||EfqQTn9>|` zut&R#&2ws=G_NTBOiF)NaXfLxRZ{#pg912;?@#eJ6hDyS1aeybB6wBlWtW8lyrTG_ z1hDn8;zv^aM63V&8y?f}I4Q}JZI!Y?SEtXKGi;>mi2zkr;s zBbcwg3OP5&4PJ=lN`IJb>WXZWUgz`7X?}e-KG5_!Z)^Hw6#zZvM{ypV-_Z237D5}$ zDIcA0GN*iwvQ6DLY?EH+BB4^8KBaipnVUYsUy}*A7hW2|WnYf*_#T{pm#(!VtX^Pa zN$mIm8lm5IZO@HdyA?FMH7YD??iIh5u5rV)W~EFir2xa+`|gTAx3o0J75SoPH+ntP zcU20tPv)H|l?T^0o{4g{J8mmT%`8Pxfs&+xm9@Tl;x@Ug@fc zb(ml6PjOtoj9c}MCze!#SY6SbG<&yZlldL~{geMaLvQ+ji{r#9hHjaKc}|$lJFQdd zh}4VsaGWH1-KVL>*iYVXVstUy=~tMUd)Q~8#8b}LnD>5NzcnQOFB$Qv zj+^o6xn!;X&WQ2Hhs58?h)-WbGyZLk|5VLIw#o6$bIlkP@kRfR6Ekbc0~TIlopj search starts here: - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include - End of search list. - [2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -o cmTC_f3d66 && : - OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) - Target: aarch64-unknown-linux-ohos - Thread model: posix - InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_f3d66 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o - - exitCode: 0 - - - kind: "message-v1" - backtrace: - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:127 (message)" - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed C implicit include dir info: rv=done - found start of include info - found start of implicit include info - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - end of search list found - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - implicit include dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - - - - - kind: "message-v1" - backtrace: - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:159 (message)" - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed C implicit link information: - link line regex: [^( *|.*[/\\])(ld\\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] - ignore line: [Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r'] - ignore line: [] - ignore line: [Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_f3d66] - ignore line: [[1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa --noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -MF CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o.d -o CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCCompilerABI.c] - ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] - ignore line: [Target: aarch64-unknown-linux-ohos] - ignore line: [Thread model: posix] - ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] - ignore line: [clang: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]] - ignore line: [ (in-process)] - ignore line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCCompilerABI.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o.d -MT CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-PDBJ6r -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -x c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCCompilerABI.c] - ignore line: [clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0] - ignore line: [ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include"] - ignore line: [#include "..." search starts here:] - ignore line: [#include <...> search starts here:] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - ignore line: [End of search list.] - ignore line: [[2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o -o cmTC_f3d66 && :] - ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] - ignore line: [Target: aarch64-unknown-linux-ohos] - ignore line: [Thread model: posix] - ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] - link line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_f3d66 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld] ==> ignore - arg [--sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot] ==> ignore - arg [-pie] ==> ignore - arg [-EL] ==> ignore - arg [--fix-cortex-a53-843419] ==> ignore - arg [-znow] ==> ignore - arg [-zrelro] ==> ignore - arg [-zmax-page-size=4096] ==> ignore - arg [--hash-style=gnu] ==> ignore - arg [--hash-style=both] ==> ignore - arg [--enable-new-dtags] ==> ignore - arg [--eh-frame-hdr] ==> ignore - arg [-m] ==> ignore - arg [aarch64linux] ==> ignore - arg [-dynamic-linker] ==> ignore - arg [/lib/ld-musl-aarch64.so.1] ==> ignore - arg [-o] ==> ignore - arg [cmTC_f3d66] ==> ignore - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] - arg [--build-id=sha1] ==> ignore - arg [--warn-shared-textrel] ==> ignore - arg [--fatal-warnings] ==> ignore - arg [-lunwind] ==> lib [unwind] - arg [--no-undefined] ==> ignore - arg [-znoexecstack] ==> ignore - arg [--gc-sections] ==> ignore - arg [CMakeFiles/cmTC_f3d66.dir/CMakeCCompilerABI.c.o] ==> ignore - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - arg [-l:libunwind.a] ==> lib [-l:libunwind.a] - arg [-lc] ==> lib [c] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - arg [-l:libunwind.a] ==> lib [-l:libunwind.a] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] - implicit libs: [unwind;-l:libunwind.a;c;-l:libunwind.a] - implicit objs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - implicit dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] - implicit fwks: [] - - - - - kind: "try_compile-v1" - backtrace: - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:57 (try_compile)" - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - checks: - - "Detecting CXX compiler ABI info" - directories: - source: "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK" - binary: "/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK" - cmakeVariables: - CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS: "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND" - CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm" - CMAKE_CXX_COMPILER_TARGET: "aarch64-linux-ohos" - CMAKE_CXX_FLAGS: "-fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__" - CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm" - CMAKE_C_COMPILER_TARGET: "aarch64-linux-ohos" - CMAKE_EXE_LINKER_FLAGS: "--rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections " - CMAKE_POSITION_INDEPENDENT_CODE: "TRUE" - CMAKE_SYSROOT: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot" - HMOS_SDK_NATIVE: "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native" - OHOS_ARCH: "arm64-v8a" - OHOS_SDK_NATIVE: "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native" - buildResult: - variable: "CMAKE_CXX_ABI_COMPILED" - cached: true - stdout: | - Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK' - - Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_5b7f7 - [1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -MF CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp - OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) - Target: aarch64-unknown-linux-ohos - Thread model: posix - InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin - clang++: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument] - (in-process) - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCXXCompilerABI.cpp -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdeprecated-macro -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -x c++ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp - clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0 - ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include" - #include "..." search starts here: - #include <...> search starts here: - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos - /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include - End of search list. - [2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_5b7f7 && : - OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48) - Target: aarch64-unknown-linux-ohos - Thread model: posix - InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_5b7f7 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -lc++ -lc++abi -lunwind -lm /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o - - exitCode: 0 - - - kind: "message-v1" - backtrace: - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:127 (message)" - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed CXX implicit include dir info: rv=done - found start of include info - found start of implicit include info - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - add: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - end of search list found - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1] - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - collapse include dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - implicit include dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/include/libcxx-ohos/include/c++/v1;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - - - - - kind: "message-v1" - backtrace: - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeDetermineCompilerABI.cmake:159 (message)" - - "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed CXX implicit link information: - link line regex: [^( *|.*[/\\])(ld\\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] - ignore line: [Change Dir: '/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK'] - ignore line: [] - ignore line: [Run Build Command(s): /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/bin/ninja -v cmTC_5b7f7] - ignore line: [[1/2] /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa --noexecstack -Wformat -D__MUSL__ -fPIE -v -MD -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -MF CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -c /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp] - ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] - ignore line: [Target: aarch64-unknown-linux-ohos] - ignore line: [Thread model: posix] - ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] - ignore line: [clang++: warning: argument unused during compilation: '--gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]] - ignore line: [ (in-process)] - ignore line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++" -cc1 -triple aarch64-unknown-linux-ohos -emit-obj -mrelax-all --mrelax-relocations -mnoexecstack -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCXXCompilerABI.cpp -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=1 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -fallow-half-arguments-and-returns -mllvm -treat-scalable-fixed-error-as-warning -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -resource-dir /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4 -dependency-file CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o.d -MT CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -sys-header-deps -D __MUSL__ -isysroot /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1 -internal-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include -internal-externc-isystem /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -Wformat -fdeprecated-macro -fdebug-compilation-dir=/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeScratch/TryCompile-sv8bCK -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -x c++ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/build-tools/cmake/share/cmake-3.28/Modules/CMakeCXXCompilerABI.cpp] - ignore line: [clang -cc1 version 15.0.4 based upon LLVM 15.0.4 default target x86_64-apple-darwin25.5.0] - ignore line: [ignoring nonexistent directory "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/include"] - ignore line: [#include "..." search starts here:] - ignore line: [#include <...> search starts here:] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../include/libcxx-ohos/include/c++/v1] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/include] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos] - ignore line: [ /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include] - ignore line: [End of search list.] - ignore line: [[2/2] : && /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/clang++ --target=aarch64-linux-ohos --gcc-toolchain=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -D__MUSL__ --rtlib=compiler-rt -fuse-ld=lld -Wl,--build-id=sha1 -Wl,--warn-shared-textrel -Wl,--fatal-warnings -lunwind -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,--gc-sections -v CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_5b7f7 && :] - ignore line: [OHOS (dev) clang version 15.0.4 (llvm-project 115b628d33dda4da4b17e14ed69dd8b74c058b48)] - ignore line: [Target: aarch64-unknown-linux-ohos] - ignore line: [Thread model: posix] - ignore line: [InstalledDir: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin] - link line: [ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld" --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --hash-style=both --enable-new-dtags --eh-frame-hdr -m aarch64linux -dynamic-linker /lib/ld-musl-aarch64.so.1 -o cmTC_5b7f7 /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/ -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/ --build-id=sha1 --warn-shared-textrel --fatal-warnings -lunwind --no-undefined -z noexecstack --gc-sections CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o -lc++ -lc++abi -lunwind -lm /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a -lc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a -l:libunwind.a /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/ld.lld] ==> ignore - arg [--sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot] ==> ignore - arg [-pie] ==> ignore - arg [-EL] ==> ignore - arg [--fix-cortex-a53-843419] ==> ignore - arg [-znow] ==> ignore - arg [-zrelro] ==> ignore - arg [-zmax-page-size=4096] ==> ignore - arg [--hash-style=gnu] ==> ignore - arg [--hash-style=both] ==> ignore - arg [--enable-new-dtags] ==> ignore - arg [--eh-frame-hdr] ==> ignore - arg [-m] ==> ignore - arg [aarch64linux] ==> ignore - arg [-dynamic-linker] ==> ignore - arg [/lib/ld-musl-aarch64.so.1] ==> ignore - arg [-o] ==> ignore - arg [cmTC_5b7f7] ==> ignore - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] - arg [-L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] - arg [--build-id=sha1] ==> ignore - arg [--warn-shared-textrel] ==> ignore - arg [--fatal-warnings] ==> ignore - arg [-lunwind] ==> lib [unwind] - arg [--no-undefined] ==> ignore - arg [-znoexecstack] ==> ignore - arg [--gc-sections] ==> ignore - arg [CMakeFiles/cmTC_5b7f7.dir/CMakeCXXCompilerABI.cpp.o] ==> ignore - arg [-lc++] ==> lib [c++] - arg [-lc++abi] ==> lib [c++abi] - arg [-lunwind] ==> lib [unwind] - arg [-lm] ==> lib [m] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - arg [-l:libunwind.a] ==> lib [-l:libunwind.a] - arg [-lc] ==> lib [c] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] ==> lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - arg [-l:libunwind.a] ==> lib [-l:libunwind.a] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o] - arg [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] ==> obj [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - remove lib [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/libclang_rt.builtins.a] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin/../lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos] - collapse library dir [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/] ==> [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] - implicit libs: [unwind;c++;c++abi;unwind;m;-l:libunwind.a;c;-l:libunwind.a] - implicit objs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtbegin.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos/clang_rt.crtend.o;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o] - implicit dirs: [/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/lib/aarch64-linux-ohos;/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos] - implicit fwks: [] - - -... diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt deleted file mode 100644 index c8c4bcd..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt +++ /dev/null @@ -1,4 +0,0 @@ -/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_launcher.dir -/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir -/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/edit_cache.dir -/Users/zxd/dev/electerm-harmony/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/rebuild_cache.dir diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache deleted file mode 100644 index 3dccd73..0000000 --- a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache +++ /dev/null @@ -1 +0,0 @@ -# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o b/entry/.cxx/default/default/release/arm64-v8a/CMakeFiles/node_ctl.dir/node_ctl.c.o deleted file mode 100644 index 5408e91fc1327a324a4d3455a4bcee6f2235ff38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4000 zcmbVOO>9(E6h5zy(xO030YMNu{DibbUON4uC2=vNfHhQT3M6VYkJtBJ+mZS6&1+M@ zL=46an@)6RRERFxY9kAcu*2ZSm9lYROq^CpFeV1mO}ilVoIB@qZr{wr;BDufbHDTR zopbMfKkpnLI8kR9Aj5!fp*#~5;QSM1J)zVDY%6(A;k1Citt)qpdyg*H&n$Mr@}oYZ z&+`X z>;wD5^^^WEchx;1{BcJn>-n}Drr<&{l{$r!$4&5G{-^s-pNX}4{zO~MO*xs-*n}VC zli5rx-fgv8U9r|wY9f6o7i8b`-7pr9cMl)#==C}~J-AtAEkLYLs-W@#pML}soy*4YU)JvU) ztDX?Mrt(+%QLh@_$mFD*c@Duf$9K zvTNnALFHeGpH%qfvLkCJ6fUQuY(A>+<}$$AHz=F6{SqeHhsrDff7ig{HSkL{aIXe# zW1ZD_$7}e%Q3DTa{T?i{N;RsIe3AZLE`!8ha3bTEn-pPWhWOT<%NC zuj8%k(;LZijPYHFZ`bn3{SoCIsF5es@<l(gO!*d$GOT*vO zaC-L0m|}(c)&0NJ@ZI8!@+Rxib-2ZNbUlA%obp#&2YU7EPY>}Sd=tm&LU6hBWjzU( zGZXcfvq8A@UXT9xeq0^xtP!r)GpONf@>_wQa;#(~8QM+|I2XYR{mBq4H=9oT8B~;= zFzY3Bf{c?(+MYj>%=k9GeR6&fCjEQ~PG`OGly3+AXfhxA0gUHUzMm`EM*YxshwWr$ zB&*(;VB7i7aWB~J*adsUNv6QcWJBM2@zl9P%D~M2m^~6WXf!cWROTIU=ccLGn3#YU|lYgq%f?PfFiQooon?}EJTEf=hO7=+F+%8O}|`#r0d zEx*8uzjLu(P8Cx`FS6g6yBPIsh+mtS74)L6$g95tAKV-QxeH z#PK{-zC8hM@nQHJ?fQsJ@flzGc3Q7Hp`M7$Wh&1@nSX@}<*XGi?+x*f#?uERVF__y z`G603KhNnNp+4n|%YAqNv1w85k2smQugKAE8pM{~SB^vm^CSXF=kn|07sXl$%0!@P_=vMyi`~MC1Pj Dvw6)`ub(Rznk|j1c zJA2NXbL^9wc~sgfYFHWx6)K7GVBcC90C?1rJznJvEa8Kds}=+D0ApD*&IXFvE^yy;1m zUZ&LXDu+s!D|KbTD6ejCq@JIx+;O(bs7-r};`FYP+NNEGYPwYE81ppOrgs^|O}mrRz+D^)sGC;S%xpD|qH;L6DM0+s&SGBwd=_?q0yRQ;5(dVIGd z<;T8xLydPSHDMa7-!^bO^dVlpP;_M)+G}b@o}U7p7mPI)j_scmr2ORv{<0C}%o~}B zi@@>YQq?$H;+rpbto(4&r8;LTeP{pWKkvQc(w04E+>Vm_svOJjb32w*yFbs=W8d#Z+OvICj#bv!S*NPMWUL+E^-cA7bUxGR`KIs0 z_B?gRr4nCy2JEVOL!}+5l;5m!A9F%X_9&G`8yAe#)xXJP?t<8)RVpiU2Cr{}SUFSv zUt+t!H~$&bIFFv9J&oras(uFA%!7W1a4f_YIq|yUsg(cQ_1QQ=?2kjboV{-N=COa; zP}NW$hd;{(ZcoF!V86%M*W}KFxq$hah8#gGJ-K75 z=+&Lm&!l!#LM|}pG}wr|Dlh9R=i72CU&_X(3GZ43_GmV$&odKFc&o{!SkD@z)t>^#jOd^Ax{rlO=GlC82XbNmq<7`w+;jhkvwZVX zf9Wm#*q602I}ggG>H)K2Hw}8R|MyN4d?>HxcuzqtF3Qjx%vyeySFg(RrCIJ20{LwY_c6)aRkV}?)7so}PTZZH+Y#oq!>1v?Sr+CH{jhwhNU1dAp5~#Z zb4u6<>wx=yT%aDm*P%MEmH5(EjkVPykV8{OsWopS&f;_;UtM`3lj$r0-Y%!_?4+7& zs36<$dN;K(7C+gU&7E9xJOlk#VeS%?qtnp7kj=TQy)e&Lov!%{`L6lQa+j5h$b)j2 zCl!+>ud$I5GO=Xnq58WWXF1sM80gVb3mdoL=$qA3mva_~tL(OAsYGaTc%R<7dy z1)TUd3bx|-Nn@_zf50YQ3sZOFKABz5D1V}0u3@?o^LJ(S-RhM8I<#>d?yKc4b>**@ zsm?j0e0a`<{>G8tj)Gh%axv#EISlsO>3RcvK)Z|n3`efHnJ&Pw zE35yUZ775Khx6uMLtTsO8rm>7;~aC4SzLF#r(ZNw+N@0Zr$KfG_K`gGZGXm4*?Y?u z`kYAg$f5p3@+QS7V|Mos2IGe< zt_M0=S_7>QJgkP|vGBoAvOhMc+BzQWRCSvhytVLOo!Zyo?^Lbrt<9aS{`&(hV0obB ztA6!htN-Cvf8fE^W`Ap^y4k(Rp-5;L`8BsK-0TVVMFb0!QrM7NuXiyNkU`u~oWuvTyV)3L( zL_+aM63pER7Q^%|g-vQqM4~-rxIeCkI}|$@4O_DjjfHSxSkN5q51KU)!%z)EH3Pw+ z#8S-mJoG?IduyN>V~wG!LCb0q$yhwt8>xvz>uS1TZle)2$VwVCD^U}S4{WR7G~5`J zR^|}Vy6wm62F4Y+AKb7=o+uRu5vLzf2*2BNZm18{Zf)$Y zZ>)m74BiE^#qkZlavc4#W6g?kwGp5)!c+OsXesR4zhYh23FnCG+5GRjf3WqPc@+t?u3vh>8z9{XY*85O9Ro)_Q`2kn=fV@)` zEaH~^bY%#67Wl5EcZr8_tPR1#c*>Gr;%7W74^Jdo8Mfqa#k2BDe(|u!E%{sVsD+2E))m;Co^@KP9%Oa8BHA)NjC54wJxYPOT{Nx>uSgw4<@ z%tK?UP`#`?|Ombn&RRS{BjrCNccJ+mUzA3!52?1I|L8TBm3YZob6=1MexYo zxJ~2X{AkyB81J;<=@U5LThZ1bjsIP`PEyTwGJZ&tpQ7udnmpqv!ZH3|;^092oWNHL z{3U^}5%?(KJn#8*{W8^TC*!XQ9vRQqHJ)8`J*n|9{*K_m=L|0FAKSB^j}B}<<8Hz+Zqoi@f#Y7vrIc{yzd-uRG=9cCg6GRj0`(4o%Xw)RIDErdERPCY zuJ<9r*&g11l7zFrjHd(-uL)FgT;qA1{2I}C7$3Fanb3Gfi06#P!}vKHp5JLazaXCX zG#N}9>(9Y;dw{n zxs7<<)p!_x--c&atU!wwFFYJ6-;2Q*f zM&S1de3EeXYcJXJ7UAp{vVczB=h z(s&r}v*C$qJU_^WQ4MK4j3;e)jtX4PODXX+fS-RsZLC_T-KgPD6OQxZp*B_{)YkWb zlZ5O0zy#sQV|}2a>oZjA_PkEGZqG%*Q%!BGOls>qwS?G=TP zm$7i(GQK|xTkm)GQX4CtXXM`~aE_Z@91!0ir;BZ z;&`%OjKdkbRaQeS^IX24N;~np4Hn%_es5*HEfi0ajjV0yHVhqvZ}1#TEg`> z;P(V9dK~r%9%=u6fy+2|5zh8kko{r8*?z|R1dp^o`&~{c-OeGB*X?|XaE!n7_hrE^ zaem*&_Au&^Dw?3c;tFfl#hD; zy9F-irI>KG{}kC@N;vCfyiD*&`%Qt%c=C4)eZD*-ug}+`f=A|6Sm4tBDB)~>57|FN zINQ%Se>cGRNc;Kw0OE2#TuTAZ1Dq%=g!eJTB~Mhy%jX0IXBwfQ!)oYYGBP*}XH-_z zo2{&a59Lip4keX06dM?T6CxFWlf9(s0g;3XhC-14!D3b;)Mr(3tOEh- zbSz*UT;VC7Hy()wEm|)gWvU)L<-y@mtPUqaaNY)^5=l6X3xxU(1n^u&MH7+80p*1Q z$%ywW4?MKV>Q=*v!J&BnV6sP9r#q4xPE8-d6Cc$RipCNV>WcIQdf;3%a+8AIgTZ(h zA_l3F^+3Mnr*lMHr4eRu%t z^_4ua5cy0ZE>PH|CDD`d{ew>o^ zMd+7ByHu6XiCDfAg0@e)=1UhhsQw1jUo<|Y5$od*!g$1d@F&ZD`e@?5J&WS^cl6Uu zLj3NF|1(D}{gb43mgKq8^`D21T>87r?1=YhAnMP9KV82G%5&)-rNx<&ixEDb(qIl?N={H((psiTAZldv;n3|=y>-Or`_$-KAdM;n4 z@n1t_qOe}B{|WlIEyPr7p}bmZ>#|p9ycTBFM&pk!4YgVvKZY+uBUk*}X#KR2AjThE a;_;k+0We6{jYHle{V56<+I>fk@&5 Date: Fri, 28 Aug 2026 12:54:57 +0800 Subject: [PATCH 04/52] fix: generate src/client/electerm-react in CI (build/bin/install.js step) The dir is gitignored and was only present locally from an earlier manual run, so CI's vite build failed on unresolved ../electerm-react imports. Co-Authored-By: Claude Fable 5 --- scripts/prepare-web.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/prepare-web.sh b/scripts/prepare-web.sh index 7c44e7b..1f2be54 100755 --- a/scripts/prepare-web.sh +++ b/scripts/prepare-web.sh @@ -44,6 +44,13 @@ npm ci --legacy-peer-deps --ignore-scripts || { npm install --legacy-peer-deps --ignore-scripts } +# Copy @electerm/electerm-react's client sources into src/client/electerm-react +# (gitignored generated dir the vite build imports from). This is the android +# repo's build/bin/install.js step; run it directly — `npm run install` would +# collide with npm's install lifecycle script. +echo " Installing electerm-react client sources ..." +node build/bin/install.js + # Build frontend + backend into entry resfile echo " Building electerm web app ..." npm run build:web From 3b2204cbdf31b0d38c4b24356ffc849a7676076b Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 12:55:58 +0800 Subject: [PATCH 05/52] fix: track rawfile/loading.html (ArkWeb initial page) The bare entry/src/main/resources/rawfile ignore rule was for the old electron build's generated rawfile content; dev2 generates nothing there and the Web component loads loading.html from $rawfile. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 - entry/src/main/resources/rawfile/loading.html | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 entry/src/main/resources/rawfile/loading.html diff --git a/.gitignore b/.gitignore index b0a0183..d1f2f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,6 @@ Thumbs.db /build-download/ build/harmony/rawfile src/client/electerm-react/ -entry/src/main/resources/rawfile /src/client/electerm-react/ /data .workbuddy diff --git a/entry/src/main/resources/rawfile/loading.html b/entry/src/main/resources/rawfile/loading.html new file mode 100644 index 0000000..59d383a --- /dev/null +++ b/entry/src/main/resources/rawfile/loading.html @@ -0,0 +1,24 @@ + + + + + + electerm + + + + +

+ + From 7fc33d8cbf9b7c44707a8d2b4dfd341efc860756 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 12:59:17 +0800 Subject: [PATCH 06/52] fix: name the final .app -signed and drop the unsigned copy Co-Authored-By: Claude Fable 5 --- scripts/build-web-app.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 612fb27..29eab9f 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -351,8 +351,10 @@ if [ ! -f "${SIGNED_APP}" ]; then exit 1 fi -mv -f "${SIGNED_APP}" "${UNSIGNED_APP}" -APP_FILE="${UNSIGNED_APP}" +# Keep only the signed APP (under a name that says so) so artifact +# pickup (find … -name '*.app') can never grab the unsigned one. +APP_FILE="${SIGNED_APP}" +rm -f "${UNSIGNED_APP}" echo " ✓ Signed APP: ${APP_FILE} ($(du -h "${APP_FILE}" | cut -f1))" From ccb5b47e6208eabbc52ed2eb2cce6dda3bd1b225 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 13:02:28 +0800 Subject: [PATCH 07/52] fix: derive signed app name correctly (strip -unsigned) Co-Authored-By: Claude Fable 5 --- scripts/build-web-app.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 29eab9f..2faf43b 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -332,7 +332,9 @@ if [ ! -f "${SIGN_TOOL_JAR}" ]; then exit 1 fi -SIGNED_APP="${UNSIGNED_APP%.app}-signed.app" +# electerm-harmony-default-unsigned.app -> electerm-harmony-default-signed.app +SIGNED_APP="${UNSIGNED_APP%.app}" +SIGNED_APP="${SIGNED_APP%-unsigned}-signed.app" java -jar "${SIGN_TOOL_JAR}" sign-app \ -mode localSign \ From d59314d81e4c852e44e7fba5ac8a9c722850bf1e Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 13:19:18 +0800 Subject: [PATCH 08/52] =?UTF-8?q?fix:=20resfile=20script=20path=20was=20en?= =?UTF-8?q?try/resource=20(singular)=20=E2=80=94=20HAP=20layout=20is=20res?= =?UTF-8?q?ources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device log (cloud debug) showed the native child spawn worked (cterm:Native_libnode_launcher0, pid 56633) but exited with the launcher's code 40 (script missing): the packaged HAP contains resources/resfile/electerm (plural), Index.ets built .../entry/resource/... - resolveScriptPath(): try both bundleCodeDir shapes, verify with fs.stat - switch Index logging to hilog (console.info is filtered in release — that's why no electerm.Index lines appeared in the device log) - on boot failure, dump node-boot.log tail via hilog for cloud-debug - EntryAbility.onDestroy: actually kill the child (BackendManager), the old LocalStorage lookup always failed - launcher: retry boot log via /data/storage/el2/base junction if the parent's filesDir path is not mounted in the child namespace - build-web-app.sh: extract SDK version with python (BSD sed lacks \+, broke local builds with SDK component missing) Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 10 ++- .../main/ets/entryability/EntryAbility.ets | 16 ++--- entry/src/main/ets/pages/Index.ets | 61 +++++++++++++++++-- scripts/build-web-app.sh | 12 +++- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 0f745bd..31a5150 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -231,11 +231,19 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { const char *params = args.entryParams ? args.entryParams : ""; parseEntryParams(params, &cfg, extraEnv, &extraEnvCount); - /* 1. Open the boot log inside the writable data dir */ + /* 1. Open the boot log inside the writable data dir. The dataDir string + * from the parent process may not be mounted in this child's namespace + * (sandbox paths differ) — retry via the per-process el2 junction, which + * points at the same files dir. */ if (cfg.dataDir[0]) { char logPath[MAX_LINE * 2]; snprintf(logPath, sizeof(logPath), "%s/node-boot.log", cfg.dataDir); g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (g_logFd < 0) { + snprintf(logPath, sizeof(logPath), + "/data/storage/el2/base/electerm-data/node-boot.log"); + g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + } } logWrite("[launcher] Main() entered, pid=%d", (int)getpid()); logWrite("[launcher] entryParams: %s", params); diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 7bf912b..0d56be2 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -17,6 +17,7 @@ import AbilityConstant from '@ohos.app.ability.AbilityConstant'; import { Configuration } from '@ohos.app.ability.Configuration'; import { abilityAccessCtrl, common, Permissions, PermissionRequestResult, UIAbility } from '@kit.AbilityKit'; import { Environment } from '@kit.CoreFileKit'; +import { BackendManager } from '../BackendManager'; const TAG: string = 'ElectermEntryAbility'; @@ -63,18 +64,9 @@ export default class EntryAbility extends UIAbility { } async onDestroy(): Promise { - // Index registered a cleanup callback (kills the node child process) - // in its LocalStorage when it appeared. - try { - const storage = LocalStorage.getShared(); - const cleanup = storage.get('backendCleanup') as () => void; - if (cleanup) { - cleanup(); - console.info(`[${TAG}] backend cleanup done`); - } - } catch (e) { - console.warn(`[${TAG}] backend cleanup failed: ${JSON.stringify(e)}`); - } + // Terminate the node child process so port 5577 is freed. If the child + // is already dead (crashed / exec failed) killNode just returns -1. + BackendManager.killBackend(); } /** diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 1555db3..928235d 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -11,17 +11,22 @@ * * While the engine is starting (or failed to start) a native overlay is * shown; the Web component stays loaded with rawfile/loading.html behind it. + * + * All logging goes through hilog (visible in release-build hilog, unlike + * console.info which is filtered out) — `hdc hilog | grep electerm.Index`. */ import { webview } from '@kit.ArkWeb'; import { common } from '@kit.AbilityKit'; import { childProcessManager } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; import fs from '@ohos.file.fs'; import http from '@ohos.net.http'; import { BackendManager } from '../BackendManager'; const TAG: string = 'electerm.Index'; +const DOMAIN: number = 0xE1EC; const BACKEND_PORT: number = 5577; const SERVER_URL: string = `http://127.0.0.1:${BACKEND_PORT}`; const POLL_INTERVAL_MS: number = 500; @@ -39,14 +44,36 @@ struct Index { this.startBackend(); } + /** + * Resolve the backend entry script inside the installed HAP. + * The HAP layout is entry/src/main/resources/resfile/electerm → installed + * at /entry/resources/resfile/electerm (note: "resources", + * plural — matches the packaged HAP contents). bundleCodeDir may or may not + * already include the module segment, so try both shapes and verify. + */ + resolveScriptPath(bundleCodeDir: string): string { + const candidates: string[] = [ + `${bundleCodeDir}/entry/resources/resfile/electerm/index.js`, + `${bundleCodeDir}/resources/resfile/electerm/index.js` + ]; + for (let i = 0; i < candidates.length; i++) { + hilog.info(DOMAIN, TAG, 'script candidate: %{public}s', candidates[i]); + if (this.fileExists(candidates[i])) { + hilog.info(DOMAIN, TAG, 'using script: %{public}s', candidates[i]); + return candidates[i]; + } + hilog.warn(DOMAIN, TAG, 'script candidate missing: %{public}s', candidates[i]); + } + return candidates[0]; // let the launcher report the failure + } + /** Create the writable data dir and spawn the node backend. */ async startBackend(): Promise { try { const context = getContext(this) as common.Context; const filesDir: string = context.filesDir; const dataDir: string = `${filesDir}/electerm-data`; - const bundleCodeDir: string = context.bundleCodeDir; - const scriptPath: string = `${bundleCodeDir}/entry/resource/resfile/electerm/index.js`; + const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); // 1. writable data dir (db, ssh keys, logs — the resfile install dir // the backend itself runs from is read-only) @@ -63,13 +90,13 @@ struct Index { ].join('\n'); this.statusMessage = 'Starting Node.js engine …'; - console.info(`[${TAG}] starting native child process, params:\n${entryParams}`); + hilog.info(DOMAIN, TAG, 'starting native child process, params: %{public}s', entryParams); const pid: number = await childProcessManager.startNativeChildProcess( 'libnode_launcher.so:Main', { entryParams: entryParams } ); BackendManager.setPid(pid); - console.info(`[${TAG}] node child process started, pid=${pid}`); + hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); // 3. wait for the HTTP server this.statusMessage = 'Starting engine …'; @@ -78,6 +105,7 @@ struct Index { this.bootFailed = true; this.statusMessage = 'Engine failed to start. See electerm-data/node-boot.log'; + this.logBootTail(dataDir); return; } @@ -88,7 +116,30 @@ struct Index { const err = e as BusinessError; this.bootFailed = true; this.statusMessage = `Startup error: [${err.code}] ${err.message}`; - console.error(`[${TAG}] startBackend failed: ${JSON.stringify(e)}`); + hilog.error(DOMAIN, TAG, 'startBackend failed: %{public}s', JSON.stringify(e)); + } + } + + /** Dump the launcher's boot log tail to hilog — the fastest way to see why + * exec failed when only hilog (no file pull) is available, e.g. cloud debug. */ + logBootTail(dataDir: string): void { + try { + const text: string = fs.readTextSync(`${dataDir}/node-boot.log`); + const tail: string = text.length > 800 + ? text.substring(text.length - 800) + : text; + hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); + } catch (e) { + hilog.error(DOMAIN, TAG, 'node-boot.log not readable: %{public}s', JSON.stringify(e)); + } + } + + fileExists(path: string): boolean { + try { + const stat = fs.statSync(path); + return stat.isFile(); + } catch { + return false; } } diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 2faf43b..501f820 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -170,9 +170,15 @@ BUILD_PROFILE="${PROJECT_ROOT}/build-profile.json5" SDK_PKG_JSON="${OHOS_SDK_HOME}/default/sdk-pkg.json" if [ -f "${SDK_PKG_JSON}" ]; then - SDK_API_VERSION=$(python3 -c "import json; d=json.load(open('${SDK_PKG_JSON}')); print(d['data']['apiVersion'])" 2>/dev/null || echo "") - SDK_DISPLAY_NAME=$(python3 -c "import json; d=json.load(open('${SDK_PKG_JSON}')); print(d['data']['displayName'])" 2>/dev/null || echo "") - SDK_VERSION=$(echo "${SDK_DISPLAY_NAME}" | sed -n 's/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/p') + # Extract both fields with python (portable — BSD sed lacks \+ quantifiers) + read -r SDK_API_VERSION SDK_VERSION SDK_DISPLAY_NAME </dev/null || echo " ") +SDKINFO if [ -n "${SDK_API_VERSION}" ] && [ -n "${SDK_VERSION}" ]; then COMPILE_SDK_VERSION="${SDK_VERSION}(${SDK_API_VERSION})" echo " Detected SDK: ${SDK_DISPLAY_NAME} (API ${SDK_API_VERSION})" From 2f21361d20bb7d707d9532b2770e0ee8e15cfc80 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 13:28:39 +0800 Subject: [PATCH 09/52] diagnostics: pass el2-junction dataDir, show boot-log last line on screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 13:25 device capture started after spawn and showed only connection- refused polling — child fate unknown, node-boot.log unreachable from the cloud-debug mirror. Make the next round self-diagnosing: - dataDir now uses /data/storage/el2/base/files/electerm-data when the junction exists (mounted in every app process incl. the native child, unlike the parent's sandbox-style filesDir string) - while polling, the overlay shows the latest node-boot.log line (~2s), so a stuck/failing boot is readable from the screen mirror alone - on timeout the overlay shows the last log line (or 'no node-boot.log — child never ran'), and the 800-char tail goes to hilog (electerm.Index) Co-Authored-By: Claude Fable 5 --- entry/src/main/ets/pages/Index.ets | 91 ++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 928235d..cedc738 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -31,6 +31,13 @@ const BACKEND_PORT: number = 5577; const SERVER_URL: string = `http://127.0.0.1:${BACKEND_PORT}`; const POLL_INTERVAL_MS: number = 500; const BOOT_TIMEOUT_MS: number = 90_000; +/** + * Prefer the per-process el2 junction over the parent's filesDir string: + * /data/storage/el2/base/files points at the same storage but is mounted in + * EVERY process of the app (including the native child), while the + * sandbox-style absolute path the parent gets may not be. + */ +const EL2_FILES_DIR: string = '/data/storage/el2/base/files'; @Entry @Component @@ -39,6 +46,7 @@ struct Index { @State serverReady: boolean = false; @State bootFailed: boolean = false; @State statusMessage: string = 'Starting electerm …'; + dataDir: string = ''; aboutToAppear(): void { this.startBackend(); @@ -72,19 +80,22 @@ struct Index { try { const context = getContext(this) as common.Context; const filesDir: string = context.filesDir; - const dataDir: string = `${filesDir}/electerm-data`; + // el2 junction path (visible in the child too) when available + this.dataDir = this.dirExists(EL2_FILES_DIR) + ? `${EL2_FILES_DIR}/electerm-data` + : `${filesDir}/electerm-data`; const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); // 1. writable data dir (db, ssh keys, logs — the resfile install dir // the backend itself runs from is read-only) - if (!this.dirExists(dataDir)) { - fs.mkdirSync(dataDir, true); + if (!this.dirExists(this.dataDir)) { + fs.mkdirSync(this.dataDir, true); } // 2. start the native child process // entryParams is a plain "key=value\n" string parsed by node_launcher.c const entryParams: string = [ - `dataDir=${dataDir}`, + `dataDir=${this.dataDir}`, `script=${scriptPath}`, `port=${BACKEND_PORT}` ].join('\n'); @@ -99,13 +110,14 @@ struct Index { hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); // 3. wait for the HTTP server - this.statusMessage = 'Starting engine …'; const ok: boolean = await this.waitForBackend(); if (!ok) { this.bootFailed = true; - this.statusMessage = - 'Engine failed to start. See electerm-data/node-boot.log'; - this.logBootTail(dataDir); + const lastLine: string = this.readBootLogLastLine(); + this.statusMessage = lastLine + ? `Engine failed: ${lastLine}` + : 'Engine failed to start (no node-boot.log — child never ran?)'; + this.logBootTail(); return; } @@ -120,18 +132,48 @@ struct Index { } } + /** Read the launcher/node boot log (written by the child). */ + readBootLog(): string { + if (!this.dataDir) { + return ''; + } + try { + return fs.readTextSync(`${this.dataDir}/node-boot.log`); + } catch { + return ''; + } + } + + /** Last non-empty log line — shown in the overlay while booting so the + * failure reason is visible on a plain screen mirror (cloud debug). */ + readBootLogLastLine(): string { + const text: string = this.readBootLog(); + if (!text) { + return ''; + } + const lines: string[] = text.split('\n'); + for (let i = lines.length - 1; i >= 0; i--) { + const line: string = lines[i].trim(); + if (line) { + return line.length > 160 ? `…${line.substring(line.length - 159)}` : line; + } + } + return ''; + } + /** Dump the launcher's boot log tail to hilog — the fastest way to see why * exec failed when only hilog (no file pull) is available, e.g. cloud debug. */ - logBootTail(dataDir: string): void { - try { - const text: string = fs.readTextSync(`${dataDir}/node-boot.log`); - const tail: string = text.length > 800 - ? text.substring(text.length - 800) - : text; - hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); - } catch (e) { - hilog.error(DOMAIN, TAG, 'node-boot.log not readable: %{public}s', JSON.stringify(e)); + logBootTail(): void { + const text: string = this.readBootLog(); + if (!text) { + hilog.error(DOMAIN, TAG, + 'node-boot.log empty/missing at %{public}s — child never wrote anything', this.dataDir); + return; } + const tail: string = text.length > 800 + ? text.substring(text.length - 800) + : text; + hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); } fileExists(path: string): boolean { @@ -152,13 +194,26 @@ struct Index { } } - /** Poll http://127.0.0.1:5577 until it answers or BOOT_TIMEOUT_MS elapses. */ + /** Poll http://127.0.0.1:5577 until it answers or BOOT_TIMEOUT_MS elapses. + * While waiting, surface the child's latest boot-log line in the overlay so + * a stuck boot is diagnosable from the screen alone. */ async waitForBackend(): Promise { const deadline: number = Date.now() + BOOT_TIMEOUT_MS; + let tick: number = 0; + let shownLine: string = ''; while (Date.now() < deadline) { if (await this.probe()) { return true; } + tick++; + if (tick % 4 === 0) { // every ~2s + const line: string = this.readBootLogLastLine(); + if (line && line !== shownLine) { + shownLine = line; + this.statusMessage = line; + hilog.info(DOMAIN, TAG, 'boot: %{public}s', line); + } + } await this.sleep(POLL_INTERVAL_MS); } return false; From 6f1a5a024c5e771dd1ec91592f7cf179d89de3bf Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 13:49:01 +0800 Subject: [PATCH 10/52] fix: locate libnode.so in the child namespace (dladdr + maps scan + node= param) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device round 2: script path fixed (exit 40 gone) but launcher exited 41 — none of the el1-junction candidates matched in the native child's mount namespace. Now: - launcher logs every step to native hilog (electerm.launcher, via libhilog_ndk.z.so) so diagnosis no longer depends on pulling the boot log - node binary candidates: parent-provided node= param (verbatim), dladdr() on Main() (authoritative load path), every arm64 .so dir visible in /proc/self/maps, then the el1 junction layouts - Index.ets logs the libs dir listing (parent-side proof whether the installer extracted the 92MB libnode.so) and passes node= through Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/CMakeLists.txt | 4 +- entry/src/main/cpp/node_launcher.c | 199 +++++++++++++++++++---------- entry/src/main/ets/pages/Index.ets | 37 +++++- 3 files changed, 166 insertions(+), 74 deletions(-) diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index 80aa35e..c6fa117 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -4,7 +4,9 @@ project(electerm_web_runtime) # libnode_launcher.so — loaded into the native child process; its Main() # execv()s the bundled node binary (installed as libnode.so). add_library(node_launcher SHARED node_launcher.c) -target_link_libraries(node_launcher PUBLIC libchild_process.so) +# libhilog_ndk.z.so — native hilog so launcher diagnostics survive release +# builds even when the boot-log file cannot be pulled (cloud debug) +target_link_libraries(node_launcher PUBLIC libchild_process.so libhilog_ndk.z.so) # libnode_ctl.so — NAPI module for the main (ArkTS) process: kill the node # child by pid on app destroy. diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 31a5150..18b18a6 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -5,30 +5,37 @@ * ArkTS cannot exec() a binary. It starts this library as a *native child * process* via childProcessManager.startNativeChildProcess( * 'libnode_launcher.so:Main', { entryParams }) — the system forks a child - * (through appspawn), loads this .so into it and calls Main() below. + * (through nativespawn), loads this .so into it and calls Main() below. * * Main() then: * 1. parses the entryParams string ("key=value" lines — plain text, no - * JSON parser needed); + * JSON parser needed); recognized keys: dataDir, script, node, port, + * secret (unknown keys are exported as env vars for the node process); * 2. locates the node binary (installed as libnode.so in the app's native - * lib dir, discovered via /proc/self/maps where this very library was - * loaded from, plus fallback candidates); + * lib dir): parent-provided "node=" path, then dladdr() on this very + * function, then every mapped-.so directory from /proc/self/maps, then + * the el1/bundle junction layout; * 3. redirects stdout/stderr to a boot log for on-device debugging; * 4. execv()s node with the electerm entry script. * - * If execv fails (e.g. the lib dir turns out to be noexec) a memfd fallback - * is attempted: the binary is copied into an anonymous executable memory - * file and execveat()d — bypassing mount noexec flags entirely. + * If execv fails (e.g. the lib dir turns out to be noexec or the binary is + * blocked by code-integrity checks) a memfd fallback is attempted: the + * binary is copied into an anonymous executable memory file and execveat()d + * — bypassing mount noexec flags entirely. * - * Every step is logged to /node-boot.log (plus the errno of any - * failure), so `hdc file recv` of that one file answers "why is the engine - * not starting" on a real device. + * Every step is logged BOTH to /node-boot.log AND to hilog + * (tag electerm.launcher, error level so release builds keep it) — so the + * boot sequence is visible even when the log file cannot be pulled. + * + * Exit codes: 40 script missing · 41 node binary not found · 42 all exec + * strategies failed. */ #include /* native_child_process.h uses `bool` */ #include "AbilityKit/native_child_process.h" +#include #include #include #include @@ -40,20 +47,28 @@ #include #include #include +#include #define LOG_BUF_SIZE 4096 #define MAX_ENV_VARS 32 #define MAX_LINE 1024 +#define MAX_CANDIDATES 12 typedef struct { - char dataDir[MAX_LINE]; /* writable app data dir (el2 filesDir) */ + char dataDir[MAX_LINE]; /* writable app data dir (el2 filesDir) */ char script[MAX_LINE * 2]; /* path to resfile/electerm/index.js */ + char node[MAX_LINE * 2]; /* optional parent-provided libnode.so path */ char port[16]; - char secret[MAX_LINE]; /* SERVER_SECRET */ + char secret[MAX_LINE]; /* SERVER_SECRET */ } LauncherConfig; static int g_logFd = -1; +/* Forward declaration — dladdr() below takes Main's address. */ +void Main(NativeChildProcess_Args args); + +/* Every message goes to the boot log file AND hilog (error level: release + * builds keep it, and `hdc hilog` shows it under electerm.launcher). */ static void logWrite(const char *fmt, ...) { char buf[LOG_BUF_SIZE]; va_list ap; @@ -61,13 +76,14 @@ static void logWrite(const char *fmt, ...) { int n = vsnprintf(buf, sizeof(buf) - 1, fmt, ap); va_end(ap); if (n < 0) return; - buf[n] = '\n'; + buf[n] = '\0'; if (g_logFd >= 0) { - ssize_t ignored = write(g_logFd, buf, (size_t)n + 1); + ssize_t ignored = write(g_logFd, buf, (size_t)n); + ignored = write(g_logFd, "\n", 1); (void)ignored; } - /* also surface in hilog (stderr was dup2'd to the log file, so keep a - * copy on fd 1 before redirection happens via this early path) */ + (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.launcher", + "%{public}s", buf); } /* Parse "key=value\n" lines into the config struct. Unknown keys are @@ -79,6 +95,7 @@ static void parseEntryParams(const char *params, LauncherConfig *cfg, snprintf(cfg->port, sizeof(cfg->port), "5577"); cfg->dataDir[0] = '\0'; cfg->script[0] = '\0'; + cfg->node[0] = '\0'; cfg->secret[0] = '\0'; char line[MAX_LINE]; @@ -101,6 +118,8 @@ static void parseEntryParams(const char *params, LauncherConfig *cfg, snprintf(cfg->dataDir, sizeof(cfg->dataDir), "%s", value); } else if (strcmp(key, "script") == 0) { snprintf(cfg->script, sizeof(cfg->script), "%s", value); + } else if (strcmp(key, "node") == 0) { + snprintf(cfg->node, sizeof(cfg->node), "%s", value); } else if (strcmp(key, "port") == 0) { snprintf(cfg->port, sizeof(cfg->port), "%s", value); } else if (strcmp(key, "secret") == 0) { @@ -111,67 +130,77 @@ static void parseEntryParams(const char *params, LauncherConfig *cfg, } } -/* Find the directory this library was loaded from by scanning - * /proc/self/maps for "libnode_launcher.so" — the sibling libnode.so lives - * in the same (executable) native lib dir. */ -static int findSelfDir(char *out, size_t outSize) { +static void addCandidate(char (*candidates)[MAX_LINE * 2], int *n, + const char *dir, const char *tag) { + if (*n >= MAX_CANDIDATES) return; + if (!dir || !dir[0]) return; + /* dedupe */ + char path[MAX_LINE * 2]; + snprintf(path, sizeof(path), "%s/libnode.so", dir); + for (int i = 0; i < *n; i++) { + if (strcmp(candidates[i], path) == 0) return; + } + snprintf(candidates[(*n)], MAX_LINE * 2, "%s", path); + logWrite("[launcher] candidate(%s): %s", tag, candidates[(*n)]); + (*n)++; +} + +/* Directory this library was loaded from, via the dynamic linker — the most + * reliable source (works even if /proc is restricted). */ +static void selfDirViaDladdr(char *out, size_t outSize) { + out[0] = '\0'; + Dl_info info; + if (dladdr((void *)&Main, &info) && info.dli_fname && info.dli_fname[0]) { + logWrite("[launcher] dladdr dli_fname: %s", info.dli_fname); + const char *slash = strrchr(info.dli_fname, '/'); + if (slash) { + snprintf(out, outSize, "%.*s", (int)(slash - info.dli_fname), + info.dli_fname); + } + } else { + logWrite("[launcher] dladdr failed: %s", dlerror() ? dlerror() : "?"); + } +} + +/* Collect the directory of every mapped .so that looks like an app native + * lib (path contains "arm64"). The child process loads several .so from the + * app libs dir; any of their directories may hold libnode.so. */ +static int collectMapDirs(char (*candidates)[MAX_LINE * 2], int *n) { FILE *f = fopen("/proc/self/maps", "r"); - if (!f) return -1; + if (!f) { + logWrite("[launcher] cannot open /proc/self/maps: %s", strerror(errno)); + return -1; + } char line[MAX_LINE]; + int found = 0; while (fgets(line, sizeof(line), f)) { - char *hit = strstr(line, "libnode_launcher.so"); - if (hit) { - /* trim trailing newline */ - char *nl = strchr(hit, '\n'); - if (nl) *nl = '\0'; - char *slash = strrchr(hit, '/'); - if (slash) { - *slash = '\0'; - snprintf(out, outSize, "%s", hit); - fclose(f); - return 0; - } + char *sp = strchr(line, ' '); + while (sp && *sp == ' ') sp++; + if (!sp) continue; + char *nm = sp; + /* skip perms/offset/dev columns to the pathname */ + for (int col = 0; col < 4 && nm; nm = strchr(nm, ' '), col++) { + if (nm) nm++; } + if (!nm || *nm != '/') continue; + char *nl = strchr(nm, '\n'); + if (nl) *nl = '\0'; + if (!strstr(nm, ".so")) continue; + if (!strstr(nm, "arm64")) continue; + char *slash = strrchr(nm, '/'); + if (!slash) continue; + *slash = '\0'; + addCandidate(candidates, n, nm, "maps"); + found++; } fclose(f); - return -1; + return found; } static int fileExists(const char *path) { return access(path, F_OK) == 0; } -/* Build the candidate node binary paths. Returns the number of candidates - * actually appended. */ -static int buildNodeCandidates(char (*candidates)[MAX_LINE * 2], - int maxCandidates) { - int n = 0; - char selfDir[MAX_LINE]; - char bundleDir[MAX_LINE * 2]; - - if (findSelfDir(selfDir, sizeof(selfDir)) == 0) { - snprintf(candidates[n++], MAX_LINE * 2, "%s/libnode.so", selfDir); - logWrite("[launcher] self dir: %s", selfDir); - } else { - logWrite("[launcher] could not locate self dir via /proc/self/maps"); - } - - /* Fallbacks based on the standard install layout */ - const char *bundleCodeDir = getenv("ELECTERM_BUNDLE_CODE_DIR"); - if (bundleCodeDir && bundleCodeDir[0]) { - snprintf(bundleDir, sizeof(bundleDir), "%s", bundleCodeDir); - } else { - snprintf(bundleDir, sizeof(bundleDir), "/data/storage/el1/bundle"); - } - if (n < maxCandidates) - snprintf(candidates[n++], MAX_LINE * 2, "%s/entry/libs/arm64-v8a/libnode.so", bundleDir); - if (n < maxCandidates) - snprintf(candidates[n++], MAX_LINE * 2, "%s/entry/libs/arm64/libnode.so", bundleDir); - if (n < maxCandidates) - snprintf(candidates[n++], MAX_LINE * 2, "%s/libs/arm64-v8a/libnode.so", bundleDir); - return n; -} - /* execveat on a memfd copy of the binary — the noexec-bypass fallback. */ static int execFromMemfd(const char *binaryPath, char *const argv[], char *const envp[]) { @@ -241,7 +270,7 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); if (g_logFd < 0) { snprintf(logPath, sizeof(logPath), - "/data/storage/el2/base/electerm-data/node-boot.log"); + "/data/storage/el2/base/files/electerm-data/node-boot.log"); g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); } } @@ -249,23 +278,51 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { logWrite("[launcher] entryParams: %s", params); if (!cfg.script[0] || !fileExists(cfg.script)) { - logWrite("[launcher] FATAL: script missing: %s", cfg.script); + logWrite("[launcher] FATAL: script missing: %s (errno=%d %s)", cfg.script, + errno, strerror(errno)); _exit(40); } /* 2. Locate node */ - char candidates[6][MAX_LINE * 2]; - int nCand = buildNodeCandidates(candidates, 6); + char candidates[MAX_CANDIDATES][MAX_LINE * 2]; + int nCand = 0; + + if (cfg.node[0] && nCand < MAX_CANDIDATES) { + /* parent-provided full path, used verbatim */ + snprintf(candidates[nCand], MAX_LINE * 2, "%s", cfg.node); + logWrite("[launcher] candidate(parent): %s", candidates[nCand]); + nCand++; + } + { + char selfDir[MAX_LINE]; + selfDirViaDladdr(selfDir, sizeof(selfDir)); + addCandidate(candidates, &nCand, selfDir, "dladdr"); + } + collectMapDirs(candidates, &nCand); + { + const char *bundleDir = getenv("ELECTERM_BUNDLE_CODE_DIR"); + if (!bundleDir || !bundleDir[0]) bundleDir = "/data/storage/el1/bundle"; + char dir[MAX_LINE * 2]; + snprintf(dir, sizeof(dir), "%s/entry/libs/arm64-v8a", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/entry/libs/arm64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/libs/arm64-v8a", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + } + const char *nodePath = NULL; for (int i = 0; i < nCand; i++) { if (fileExists(candidates[i])) { nodePath = candidates[i]; break; } - logWrite("[launcher] candidate not found: %s", candidates[i]); + logWrite("[launcher] candidate not found: %s (errno=%d)", candidates[i], + errno); } if (!nodePath) { - logWrite("[launcher] FATAL: no libnode.so candidate exists"); + logWrite("[launcher] FATAL: no libnode.so candidate exists (tried %d)", + nCand); _exit(41); } logWrite("[launcher] node binary: %s", nodePath); diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index cedc738..7267f51 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -75,6 +75,34 @@ struct Index { return candidates[0]; // let the launcher report the failure } + /** + * Resolve libnode.so in the app's native libs dir and log the dir listing — + * this tells us from the parent side whether the installer actually + * extracted the 92MB node binary. Empty string when not found. + */ + resolveNodePath(bundleCodeDir: string): string { + const libsDirs: string[] = [ + `${bundleCodeDir}/entry/libs/arm64-v8a`, + `${bundleCodeDir}/libs/arm64-v8a` + ]; + for (let i = 0; i < libsDirs.length; i++) { + const libsDir: string = libsDirs[i]; + try { + const names: string[] = fs.listFileSync(libsDir); + hilog.info(DOMAIN, TAG, 'libs dir %{public}s → %{public}s', libsDir, names.join(', ')); + const nodePath: string = `${libsDir}/libnode.so`; + if (this.fileExists(nodePath)) { + hilog.info(DOMAIN, TAG, 'using node: %{public}s', nodePath); + return nodePath; + } + } catch (e) { + hilog.error(DOMAIN, TAG, 'libs dir not listable: %{public}s (%{public}s)', + libsDir, JSON.stringify(e)); + } + } + return ''; + } + /** Create the writable data dir and spawn the node backend. */ async startBackend(): Promise { try { @@ -85,6 +113,7 @@ struct Index { ? `${EL2_FILES_DIR}/electerm-data` : `${filesDir}/electerm-data`; const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); + const nodePath: string = this.resolveNodePath(context.bundleCodeDir); // 1. writable data dir (db, ssh keys, logs — the resfile install dir // the backend itself runs from is read-only) @@ -94,11 +123,15 @@ struct Index { // 2. start the native child process // entryParams is a plain "key=value\n" string parsed by node_launcher.c - const entryParams: string = [ + const paramLines: string[] = [ `dataDir=${this.dataDir}`, `script=${scriptPath}`, `port=${BACKEND_PORT}` - ].join('\n'); + ]; + if (nodePath) { + paramLines.push(`node=${nodePath}`); + } + const entryParams: string = paramLines.join('\n'); this.statusMessage = 'Starting Node.js engine …'; hilog.info(DOMAIN, TAG, 'starting native child process, params: %{public}s', entryParams); From 537f0eedef71fc26c5dbe76ca32df18444beea0f Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 14:20:47 +0800 Subject: [PATCH 11/52] loader-exec fallback: exec signed system musl loader with node as argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execv(libnode.so) and memfd execveat both failed on device (exit 42) — suspected XPM code-integrity / noexec on the bundle mount. New ladder step 6a: exec the system's own signed ld-musl loader with the node binary as its program argument; the loader maps the binary itself (PROT_EXEC mmap, same as dlopen which provably works for app .so files in this process). Also logs node stat() mode/size and the mountinfo options of the containing mount, so noexec/code-integrity is visible in node-boot.log. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 113 +++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 18b18a6..b28d8a5 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -201,6 +201,78 @@ static int fileExists(const char *path) { return access(path, F_OK) == 0; } +/* Find the system musl dynamic loader — first from /proc/self/maps (it + * mapped us, so it is definitely present at that path), then well-known + * locations. */ +static int findLoader(char *out, size_t outSize) { + FILE *f = fopen("/proc/self/maps", "r"); + if (f) { + char line[MAX_LINE]; + while (fgets(line, sizeof(line), f)) { + char *hit = strstr(line, "ld-musl"); + if (hit && strstr(hit, ".so")) { + char *nl = strchr(hit, '\n'); + if (nl) *nl = '\0'; + snprintf(out, outSize, "%s", hit); + fclose(f); + return 0; + } + } + fclose(f); + } + const char *fallbacks[] = { + "/lib/ld-musl-aarch64.so.1", + "/system/lib/ld-musl-aarch64.so.1", + "/system/lib64/ld-musl-aarch64.so.1", + NULL + }; + for (int i = 0; fallbacks[i]; i++) { + if (fileExists(fallbacks[i])) { + snprintf(out, outSize, "%s", fallbacks[i]); + return 0; + } + } + return -1; +} + +/* Log the mount that contains `path` (from /proc/self/mountinfo) so noexec + * and other enforcement is visible in the boot log. */ +static void logMountFlagsFor(const char *path) { + /* find the deepest mount point that prefixes path */ + char best[MAX_LINE]; + char bestLine[MAX_LINE * 2]; + best[0] = '\0'; + bestLine[0] = '\0'; + FILE *f = fopen("/proc/self/mountinfo", "r"); + if (!f) { + logWrite("[launcher] cannot open /proc/self/mountinfo: %s", + strerror(errno)); + return; + } + char line[MAX_LINE * 2]; + while (fgets(line, sizeof(line), f)) { + /* format: id parent maj:min root mountpoint options ... */ + unsigned id, parent; + unsigned maj, min; + char root[MAX_LINE], mnt[MAX_LINE], opts[MAX_LINE]; + if (sscanf(line, "%u %u %u:%u %s %s %s", &id, &parent, &maj, &min, root, + mnt, opts) != 7) { + continue; + } + if (strncmp(path, mnt, strlen(mnt)) == 0 && + strlen(mnt) > strlen(best)) { + snprintf(best, sizeof(best), "%s", mnt); + snprintf(bestLine, sizeof(bestLine), "mount %s → options: %s", mnt, opts); + } + } + fclose(f); + if (best[0]) { + logWrite("[launcher] %s (for %s)", bestLine, path); + } else { + logWrite("[launcher] no mountinfo entry prefixes %s", path); + } +} + /* execveat on a memfd copy of the binary — the noexec-bypass fallback. */ static int execFromMemfd(const char *binaryPath, char *const argv[], char *const envp[]) { @@ -346,9 +418,21 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { dup2(g_logFd, 2); } - /* 5. Ensure the executable bit is set (it should already be, but a - * remounted/restored file could lose it) and exec. */ - chmod(nodePath, 0755); + /* 5. Log the node file's mode + the mount flags of its directory — + * noexec / code-integrity enforcement shows up here. */ + { + struct stat st; + if (stat(nodePath, &st) == 0) { + logWrite("[launcher] node stat: mode=%o size=%ld", st.st_mode, + (long)st.st_size); + } else { + logWrite("[launcher] node stat failed: %s", strerror(errno)); + } + logMountFlagsFor(nodePath); + } + + /* 6. exec. */ + chmod(nodePath, 0755); /* no-op on the read-only bundle mount; logged above */ char nodeArg0[MAX_LINE * 2]; snprintf(nodeArg0, sizeof(nodeArg0), "%s", nodePath); @@ -357,10 +441,29 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { logWrite("[launcher] execv: %s %s", nodeArg0, cfg.script); execv(nodeArg0, argv); int execErr = errno; - logWrite("[launcher] execv failed: %s — trying memfd fallback", + logWrite("[launcher] execv failed: errno=%d (%s)", execErr, strerror(execErr)); - /* 6. noexec fallback */ + /* 6a. Loader-exec fallback: exec the SYSTEM dynamic loader (signed, on an + * exec mount) with the node binary as its program argument. The loader + * maps the binary itself — the same PROT_EXEC file mapping dlopen() uses, + * which demonstrably works for app .so files in this very process. This + * sidesteps execve() of an unsigned app file entirely. */ + { + char loader[MAX_LINE]; + if (findLoader(loader, sizeof(loader)) == 0) { + char *const largv[] = {loader, nodeArg0, cfg.script, NULL}; + logWrite("[launcher] execv via loader: %s %s %s", loader, nodeArg0, + cfg.script); + execv(loader, largv); + logWrite("[launcher] loader execv failed: errno=%d (%s)", errno, + strerror(errno)); + } else { + logWrite("[launcher] no dynamic loader found for fallback"); + } + } + + /* 6b. memfd fallback (noexec mounts) */ if (execFromMemfd(nodeArg0, argv, NULL) == 0) { _exit(0); /* unreachable */ } From 538061ef7e279014662fe3f529f1b1470be88999 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 14:41:05 +0800 Subject: [PATCH 12/52] code-sign libnode.so with binary-sign-tool (XPM exec fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device test: execv errno=13 EACCES — even loader-exec. XPM only runs signed code. Build now embeds a code signature in the node ELF via the official binary-sign-tool (openharmony/developtools_hapsigner dist, sha256-pinned download): - cert mode with the APP signing identity when KEYSTORE_PASSWORD/ KEY_PASSWORD are set (CI), self-sign fallback (local) - packed-HAP verification step fails the build if the signature is lost during packaging - entry build-profile: nativeLib.debugSymbol.strip=false so the non-alloc signature section is never stripped - libnode.so is signed in place; restore locally with git checkout -- entry/libs/arm64-v8a/libnode.so Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + entry/build-profile.json5 | 5 +++ scripts/build-web-app.sh | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/.gitignore b/.gitignore index d1f2f9c..38eccbd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Note: /build/ contains committed source (build scripts, vite config). # Only ignore HarmonyOS build outputs and electerm-web build artifacts. /build/outputs/ +/build/tools/ /build/intermediates/ /oh_modules/ /entry/build/ diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 index 0807827..f8d3e77 100644 --- a/entry/build-profile.json5 +++ b/entry/build-profile.json5 @@ -17,6 +17,11 @@ "enable": false } } + }, + "nativeLib": { + "debugSymbol": { + "strip": false + } } } ], diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 501f820..788ec25 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -296,6 +296,73 @@ echo "==> Installing ohpm dependencies ..." cd "${PROJECT_ROOT}" "${OHPM}" install +# --- Code-sign libnode.so ----------------------------------------------------- +# HarmonyOS XPM only lets signed code execute on device — execv of the +# bundled node binary is refused with EACCES otherwise. binary-sign-tool +# (official tool, openharmony/developtools_hapsigner dist) embeds a code +# signature in the ELF. Cert mode reuses the same identity that signs the +# APP (set KEYSTORE_PASSWORD/KEY_PASSWORD — CI does); otherwise self-sign. +# NOTE: signs entry/libs/arm64-v8a/libnode.so IN PLACE — after a local +# build restore the pristine copy with: +# git checkout -- entry/libs/arm64-v8a/libnode.so + +echo "==> Code-signing libnode.so ..." + +BINSIGN_JAR="${BINSIGN_JAR:-${PROJECT_ROOT}/build/tools/binary-sign-tool.jar}" +BINSIGN_SHA256="d984474a09f6a1255ccde31f36e8a580be77aabd35b0ca2b3d94d1962ae3778d" +if [ ! -f "${BINSIGN_JAR}" ]; then + mkdir -p "$(dirname "${BINSIGN_JAR}")" + echo " Downloading binary-sign-tool.jar (developtools_hapsigner dist) ..." + curl -fsSL --retry 5 --retry-delay 3 -o "${BINSIGN_JAR}" \ + "https://raw.githubusercontent.com/openharmony/developtools_hapsigner/master/dist/binary-sign-tool.jar" +fi +BINSIGN_ACTUAL=$(shasum -a 256 "${BINSIGN_JAR}" | cut -d' ' -f1) +if [ "${BINSIGN_ACTUAL}" != "${BINSIGN_SHA256}" ]; then + echo " ✗ binary-sign-tool.jar checksum mismatch: ${BINSIGN_ACTUAL}" + exit 1 +fi +echo " ✓ binary-sign-tool.jar ready" + +NODE_LIB="${PROJECT_ROOT}/entry/libs/arm64-v8a/libnode.so" +NODE_LIB_SIGNED="$(mktemp -t libnode).signed" +NODE_SIGNED=0 + +if [ -n "${KEYSTORE_PASSWORD:-}" ] && [ -n "${KEY_PASSWORD:-}" ]; then + echo " Signing with the APP certificate ..." + if java -jar "${BINSIGN_JAR}" sign \ + -keyAlias "${KEY_ALIAS}" \ + -keyPwd "${KEY_PASSWORD}" \ + -appCertFile "${CERT_PATH}" \ + -inFile "${NODE_LIB}" \ + -signAlg SHA256withECDSA \ + -keystoreFile "${KEYSTORE_PATH}" \ + -keystorePwd "${KEYSTORE_PASSWORD}" \ + -outFile "${NODE_LIB_SIGNED}" >/dev/null 2>&1; then + mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" + NODE_SIGNED=1 + echo " ✓ libnode.so cert-signed (APP identity)" + else + echo " ⚠ cert-sign failed — falling back to self-sign" + rm -f "${NODE_LIB_SIGNED}" + fi +fi + +if [ "${NODE_SIGNED}" = "0" ]; then + if java -jar "${BINSIGN_JAR}" sign \ + -inFile "${NODE_LIB}" -outFile "${NODE_LIB_SIGNED}" \ + -selfSign 1 >/dev/null 2>&1; then + mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" + NODE_SIGNED=1 + echo " ✓ libnode.so self-signed" + else + echo " ⚠ self-sign failed — shipping unsigned (device may refuse to exec)" + fi + rm -f "${NODE_LIB_SIGNED}" +fi + +java -jar "${BINSIGN_JAR}" display-sign -inFile "${NODE_LIB}" 2>/dev/null \ + | grep -E 'INFO - (verify|code signature)' | sed 's/^/ /' || true + # --- Build the unsigned APP ------------------------------------------------- echo "==> Building unsigned APP (${BUILD_MODE}) ..." @@ -393,6 +460,18 @@ check_file() { } check_file "${HAP_DIR}/libs/arm64-v8a/libnode.so" "libs/arm64-v8a/libnode.so" + +# The code signature must survive packaging (hvigor strip could drop the +# non-alloc .codesign section — entry/build-profile.json5 disables strip). +if [ "${NODE_SIGNED}" = "1" ]; then + if java -jar "${BINSIGN_JAR}" display-sign \ + -inFile "${HAP_DIR}/libs/arm64-v8a/libnode.so" 2>/dev/null \ + | grep -q "code signature is not found"; then + ERRORS="${ERRORS}\n ✗ libnode.so code signature lost during packaging" + else + echo " ✓ libnode.so code signature present in packed HAP" + fi +fi check_file "${HAP_DIR}/libs/arm64-v8a/libnode_launcher.so" "libs/arm64-v8a/libnode_launcher.so" check_file "${HAP_DIR}/libs/arm64-v8a/libnode_ctl.so" "libs/arm64-v8a/libnode_ctl.so" check_file "${HAP_DIR}/resources/resfile/electerm/index.js" "resfile/electerm/index.js" From a6988fe1853048e7f25395e7c30002e01beecddf Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 14:43:51 +0800 Subject: [PATCH 13/52] fix: portable mktemp (GNU rejects -t templates without X's) Co-Authored-By: Claude Fable 5 --- scripts/build-web-app.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 788ec25..5b248fa 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -324,7 +324,8 @@ fi echo " ✓ binary-sign-tool.jar ready" NODE_LIB="${PROJECT_ROOT}/entry/libs/arm64-v8a/libnode.so" -NODE_LIB_SIGNED="$(mktemp -t libnode).signed" +# plain mktemp (no -t template) — GNU mktemp rejects -t templates without X's +NODE_LIB_SIGNED="$(mktemp).signed" NODE_SIGNED=0 if [ -n "${KEYSTORE_PASSWORD:-}" ] && [ -n "${KEY_PASSWORD:-}" ]; then From 50812a363ed6d16f85da0d1ba4cda89f8f72a7b2 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 15:09:01 +0800 Subject: [PATCH 14/52] run node in-process via dlopen + node::Start (exec is blocked on device) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third device failure with errno=13: direct execv, exec via the signed system loader, and memfd execveat are ALL refused for the app child — even with the binary cert-signed with the APP identity. XPM blocks the exec syscall for app processes, not the signature. New strategy 1 (nodejs-mobile / WineHua pattern): dlopen the bundled node binary and call its exported embedder entry int node::Start(int argc, char *argv[]) (_ZN4node5StartEiPPc) on a dedicated 32MB-stack thread. OHOS musl's load_library has no PT_INTERP/DF_1_PIE rejection, so the PIE executable loads as a shared object. dlopen is how app code legitimately gets mapped executable (nativespawn loaded this very launcher). Falls back to the exec ladder only if dlopen/dlsym cannot start. Also: chdir(dataDir) before running node; the failure overlay now shows the last 8 boot-log lines so the whole dlopen → dlsym → Start ladder is readable from the screen alone. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 113 ++++++++++++++++++++++++++++- entry/src/main/ets/pages/Index.ets | 23 ++++-- 2 files changed, 126 insertions(+), 10 deletions(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index b28d8a5..83036bf 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -27,8 +27,9 @@ * (tag electerm.launcher, error level so release builds keep it) — so the * boot sequence is visible even when the log file cannot be pulled. * - * Exit codes: 40 script missing · 41 node binary not found · 42 all exec - * strategies failed. + * Exit codes: 40 script missing · 41 node binary not found · 42 in-process + * start failed AND all exec strategies failed · else node's own exit code + * (in-process mode exits from nodeThreadMain). */ #include /* native_child_process.h uses `bool` */ @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -273,6 +275,87 @@ static void logMountFlagsFor(const char *path) { } } +/* ── Strategy 1: run node IN-PROCESS (the nodejs-mobile / WineHua pattern) ── + * + * execve of any new image is refused inside an app child process on + * HarmonyOS (errno EACCES — direct, via the system loader, and via memfd, + * even with a code-signed binary: XPM blocks the syscall for app uids). + * But dlopen() of a shared object demonstrably works — nativespawn loaded + * this very library. The bundled node is a dynamic PIE, and OHOS musl's + * loader does not reject executables: no PT_INTERP / DF_1_PIE check in + * load_library(). node exports its embedder entry + * int node::Start(int argc, char *argv[]) (_ZN4node5StartEiPPc) + * so we dlopen the binary, resolve node::Start, and call it on a dedicated + * big-stack thread (node needs a large stack; nativespawn's Main() thread + * cannot be assumed to have one). + * + * Returns only on failure (-1); on success node::Start runs until exit and + * the thread wrapper _exit()s the process with node's exit code. */ + +typedef int (*node_start_fn)(int argc, char *argv[]); + +struct NodeThreadArgs { + node_start_fn start; + char *argv[3]; + int rc; +}; + +static void *nodeThreadMain(void *p) { + struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; + a->rc = a->start(2, a->argv); + logWrite("[launcher] node::Start returned %d", a->rc); + _exit(a->rc & 0xff); + return NULL; /* unreachable */ +} + +static int runNodeInProcess(const char *nodePath, const char *script) { + logWrite("[launcher] in-process: dlopen(%s)", nodePath); + void *h = dlopen(nodePath, RTLD_NOW | RTLD_LOCAL); + if (!h) { + const char *e1 = dlerror(); + const char *e2 = dlerror(); + logWrite("[launcher] dlopen failed: %s / %s", e1 ? e1 : "-", + e2 ? e2 : "-"); + return -1; + } + dlerror(); + node_start_fn start = (node_start_fn)dlsym(h, "_ZN4node5StartEiPPc"); + const char *e = dlerror(); + if (!start || (e && e[0])) { + logWrite("[launcher] dlsym(node::Start) failed: %s", e ? e : "null sym"); + return -1; + } + logWrite("[launcher] node::Start resolved at %p", (void *)start); + + /* argv must outlive the thread — static storage. */ + static char arg0[MAX_LINE * 2]; + static char arg1[MAX_LINE * 2]; + snprintf(arg0, sizeof(arg0), "%s", nodePath); + snprintf(arg1, sizeof(arg1), "%s", script); + + static struct NodeThreadArgs na; + na.start = start; + na.argv[0] = arg0; + na.argv[1] = arg1; + na.argv[2] = NULL; + na.rc = -1; + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 32 * 1024 * 1024); /* node wants a big stack */ + pthread_t th; + int prc = pthread_create(&th, &attr, nodeThreadMain, &na); + if (prc != 0) { + logWrite("[launcher] pthread_create failed: %s", strerror(prc)); + return -1; + } + void *ret = NULL; + pthread_join(th, &ret); /* nodeThreadMain _exits, so this returns on error only */ + (void)ret; + logWrite("[launcher] node thread ended without _exit (rc=%d)", na.rc); + return -1; +} + /* execveat on a memfd copy of the binary — the noexec-bypass fallback. */ static int execFromMemfd(const char *binaryPath, char *const argv[], char *const envp[]) { @@ -431,7 +514,28 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { logMountFlagsFor(nodePath); } - /* 6. exec. */ + /* 5b. node writes relative paths into the data dir — make that the cwd + * (the resfile install dir it runs from is read-only). */ + if (cfg.dataDir[0]) { + if (chdir(cfg.dataDir) == 0) { + logWrite("[launcher] cwd: %s", cfg.dataDir); + } else { + logWrite("[launcher] chdir(%s) failed: %s", cfg.dataDir, + strerror(errno)); + } + } + + /* 6. STRATEGY 1 — in-process node::Start via dlopen. Exec of a new image + * is blocked on device (direct, loader, memfd: all EACCES, even + * code-signed); dlopen is how app code legitimately gets mapped + * executable (nativespawn loaded this very library). Runs until exit + * on success; falls through to the exec ladder only if it cannot start. + */ + if (runNodeInProcess(nodePath, cfg.script) == 0) { + _exit(0); /* unreachable — nodeThreadMain exits the process */ + } + + /* 7. exec ladder (kept for environments where exec is permitted). */ chmod(nodePath, 0755); /* no-op on the read-only bundle mount; logged above */ char nodeArg0[MAX_LINE * 2]; @@ -468,7 +572,8 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { _exit(0); /* unreachable */ } - logWrite("[launcher] FATAL: all exec strategies failed (execv errno=%d)", + logWrite("[launcher] FATAL: in-process start failed AND all exec strategies " + "failed (execv errno=%d)", execErr); _exit(42); } diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 7267f51..f7d52a9 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -146,9 +146,9 @@ struct Index { const ok: boolean = await this.waitForBackend(); if (!ok) { this.bootFailed = true; - const lastLine: string = this.readBootLogLastLine(); - this.statusMessage = lastLine - ? `Engine failed: ${lastLine}` + const tail: string = this.readBootLogTailLines(8); + this.statusMessage = tail + ? tail : 'Engine failed to start (no node-boot.log — child never ran?)'; this.logBootTail(); return; @@ -180,18 +180,29 @@ struct Index { /** Last non-empty log line — shown in the overlay while booting so the * failure reason is visible on a plain screen mirror (cloud debug). */ readBootLogLastLine(): string { + return this.readBootLogTailLines(1); + } + + /** Last `n` non-empty log lines joined by newline — the failure overlay + * shows the whole launcher ladder (stat → dlopen → dlsym → start) so one + * device round-trip is enough to see exactly which step failed. */ + readBootLogTailLines(n: number): string { const text: string = this.readBootLog(); if (!text) { return ''; } const lines: string[] = text.split('\n'); - for (let i = lines.length - 1; i >= 0; i--) { + const picked: string[] = []; + for (let i = lines.length - 1; i >= 0 && picked.length < n; i--) { const line: string = lines[i].trim(); if (line) { - return line.length > 160 ? `…${line.substring(line.length - 159)}` : line; + const clipped: string = line.length > 120 + ? `…${line.substring(line.length - 119)}` + : line; + picked.unshift(clipped); } } - return ''; + return picked.join('\n'); } /** Dump the launcher's boot log tail to hilog — the fastest way to see why From 569b6c1063924ed6da15a5e195e72190eb633783 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 15:11:48 +0800 Subject: [PATCH 15/52] =?UTF-8?q?exec=20diagnosis:=20bundle=20installs=20n?= =?UTF-8?q?ode=200644=20(no=20+x)=20=E2=86=92=20EACCES;=20loader=20path=20?= =?UTF-8?q?lost=20its=20dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device log (temp/2026-08-28 15_03_43.log) shows: - node stat mode=100644: hvigor installs libs without the execute bit, execve returns EACCES for that plain reason — the el1 mount is rw,nosuid,nodev,noatime, NOT noexec - loader execv errno=2: findLoader sliced the maps line at 'ld-musl', dropping /lib/ — exec'd a bare filename Fixes: - strategy 2: copy the signed binary into the app-owned el2 data dir (data/bin/node), chmod 0755, exec the copy — size-checked, copied once, kept across restarts - findLoader takes the full last path token of the maps line - chmod failures are logged everywhere now Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 129 +++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 83036bf..a80d9bb 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -205,17 +205,22 @@ static int fileExists(const char *path) { /* Find the system musl dynamic loader — first from /proc/self/maps (it * mapped us, so it is definitely present at that path), then well-known - * locations. */ + * locations. The path is the LAST whitespace-delimited token of the maps + * line — substring-searching for "ld-musl" and slicing from there drops + * the directory prefix (device round-trip taught us: "ld-musl-…so.1" alone + * execve's to ENOENT). */ static int findLoader(char *out, size_t outSize) { FILE *f = fopen("/proc/self/maps", "r"); if (f) { char line[MAX_LINE]; while (fgets(line, sizeof(line), f)) { - char *hit = strstr(line, "ld-musl"); - if (hit && strstr(hit, ".so")) { - char *nl = strchr(hit, '\n'); - if (nl) *nl = '\0'; - snprintf(out, outSize, "%s", hit); + char *nl = strchr(line, '\n'); + if (nl) *nl = '\0'; + char *sp = strrchr(line, ' '); + char *path = sp ? sp + 1 : line; + if (strstr(path, "ld-musl") && strstr(path, ".so") && + access(path, F_OK) == 0) { + snprintf(out, outSize, "%s", path); fclose(f); return 0; } @@ -356,6 +361,105 @@ static int runNodeInProcess(const char *nodePath, const char *script) { return -1; } +/* ── Strategy 2: copy the signed node binary into the writable data dir, + * chmod +x there, and exec the copy ───────────────────────────────────────── + * + * Device log finding: the bundled libnode.so installs with mode 0644 (no + * execute bit) on a mount that is NOT noexec — execve then fails with + * EACCES for the plainest Unix reason, and the app cannot chmod a file it + * does not own inside el1/bundle. The el2 files dir IS app-owned: copy the + * (code-signed) binary there once, give it 0755, exec it. */ + +static int copyFile(const char *src, const char *dst) { + int in = open(src, O_RDONLY); + if (in < 0) { + logWrite("[launcher] copy: open(%s) failed: %s", src, strerror(errno)); + return -1; + } + int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (out < 0) { + logWrite("[launcher] copy: open(%s) failed: %s", dst, strerror(errno)); + close(in); + return -1; + } + char buf[262144]; + ssize_t r; + while ((r = read(in, buf, sizeof(buf))) > 0) { + ssize_t off = 0; + while (off < r) { + ssize_t w = write(out, buf + off, (size_t)(r - off)); + if (w < 0) { + logWrite("[launcher] copy: write failed: %s", strerror(errno)); + close(in); + close(out); + return -1; + } + off += w; + } + } + int rc = 0; + if (r < 0) { + logWrite("[launcher] copy: read failed: %s", strerror(errno)); + rc = -1; + } + close(in); + close(out); + return rc; +} + +/* Returns only on failure (-1) — like every exec strategy, success never + * returns. */ +static int execFromDataDir(const char *nodePath, const char *dataDir, + const char *script) { + char binDir[MAX_LINE * 2]; + char dest[MAX_LINE * 2]; + char tmp[MAX_LINE * 2]; + snprintf(binDir, sizeof(binDir), "%s/bin", dataDir); + snprintf(dest, sizeof(dest), "%s/bin/node", dataDir); + snprintf(tmp, sizeof(tmp), "%s/bin/node.tmp", dataDir); + + mkdir(binDir, 0755); /* ok if it exists */ + + /* copy only if missing or different size (96MB copy ~ a few seconds) */ + struct stat ss, sd; + int needCopy = 1; + if (stat(dest, &sd) == 0 && stat(nodePath, &ss) == 0 && + sd.st_size == ss.st_size) { + needCopy = 0; + logWrite("[launcher] el2 copy already present: %s", dest); + } + if (needCopy) { + logWrite("[launcher] copying %s → %s (%ld bytes)", nodePath, tmp, + (long)ss.st_size); + if (copyFile(nodePath, tmp) != 0) { + return -1; + } + if (rename(tmp, dest) != 0) { + logWrite("[launcher] rename failed: %s", strerror(errno)); + unlink(tmp); + return -1; + } + logWrite("[launcher] copy complete"); + } + + if (chmod(dest, 0755) != 0) { + logWrite("[launcher] chmod(%s, 0755) failed: %s", dest, strerror(errno)); + } + if (stat(dest, &sd) == 0) { + logWrite("[launcher] el2 node stat: mode=%o size=%ld", sd.st_mode, + (long)sd.st_size); + logMountFlagsFor(dest); + } + char arg0[MAX_LINE * 2]; + snprintf(arg0, sizeof(arg0), "%s", dest); + char *const argv[] = {arg0, (char *)script, NULL}; + logWrite("[launcher] execv(el2): %s %s", arg0, script); + execv(arg0, argv); + logWrite("[launcher] execv(el2) failed: errno=%d (%s)", errno, + strerror(errno)); + return -1; +} + /* execveat on a memfd copy of the binary — the noexec-bypass fallback. */ static int execFromMemfd(const char *binaryPath, char *const argv[], char *const envp[]) { @@ -535,8 +639,19 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { _exit(0); /* unreachable — nodeThreadMain exits the process */ } + /* 6b. STRATEGY 2 — the bundled file installs 0644 (no +x) and cannot be + * chmod'd in el1; copy the signed binary into the app-owned el2 data + * dir, chmod +x, exec the copy. */ + if (cfg.dataDir[0]) { + if (execFromDataDir(nodePath, cfg.dataDir, cfg.script) == 0) { + _exit(0); /* unreachable */ + } + } + /* 7. exec ladder (kept for environments where exec is permitted). */ - chmod(nodePath, 0755); /* no-op on the read-only bundle mount; logged above */ + if (chmod(nodePath, 0755) != 0) { + logWrite("[launcher] chmod(bundle node) failed: %s", strerror(errno)); + } char nodeArg0[MAX_LINE * 2]; snprintf(nodeArg0, sizeof(nodeArg0), "%s", nodePath); From 0c0f24b9445983c02bf3b7745c86a33c1b1a92b9 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 16:04:36 +0800 Subject: [PATCH 16/52] SIGSYS shim: turn seccomp-trapped syscalls into logged ENOSYS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device log (temp/2026-08-28 16_00_16.log): dlopen succeeded, node::Start resolved and entered, then the node thread died of signal 31 (SIGSYS, si_code=1 SYS_SECCOMP) — the app sandbox seccomp filter kills syscalls V8 probes at startup (membarrier / pkey_mprotect / perf_event_open class). The signal was delivered to a handler (OHOS DfxSignalHandler), so it is catchable: install our own SIGSYS handler that logs the trapped syscall number (si_syscall), advances PC past the 4-byte svc instruction, and sets x0 = -ENOSYS. V8/uv treat those probes as optional and degrade gracefully on ENOSYS. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index a80d9bb..211e73d 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ #include #include #include +#include #include #include @@ -299,6 +301,47 @@ static void logMountFlagsFor(const char *path) { typedef int (*node_start_fn)(int argc, char *argv[]); +/* ── SIGSYS: the app sandbox's seccomp filter traps syscalls node/V8 probe + * for at startup (membarrier, pkey_mprotect, perf_event_open, …) and the + * default action kills the thread (device: signo 31, si_code SYS_SECCOMP). + * V8/uv handle ENOSYS gracefully for all of those probes — they are + * optional accelerations. So: catch SIGSYS, log which syscall was trapped, + * skip the svc instruction, and return -ENOSYS from it. */ + +static int g_sigsysSeen[32]; +static int g_sigsysSeenCount = 0; + +static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { + (void)sig; + int sc = si->si_syscall; /* musl: #define si_syscall __si_fields.__sigsys.si_syscall */ + int known = 0; + for (int i = 0; i < g_sigsysSeenCount; i++) { + if (g_sigsysSeen[i] == sc) { + known = 1; + break; + } + } + if (!known && g_sigsysSeenCount < 32) { + g_sigsysSeen[g_sigsysSeenCount++] = sc; + logWrite("[launcher] SIGSYS: syscall %d blocked by seccomp → ENOSYS", sc); + } + ucontext_t *uc = (ucontext_t *)ctx; + /* aarch64: the trapped instruction is the 4-byte `svc #0`; skip it and + * put -ENOSYS in x0 (the syscall return register). */ + uc->uc_mcontext.pc += 4; + uc->uc_mcontext.regs[0] = (unsigned long)-ENOSYS; +} + +static void installSigsysShim(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = sigsysHandler; + sa.sa_flags = SA_SIGINFO; + if (sigaction(SIGSYS, &sa, NULL) != 0) { + logWrite("[launcher] sigaction(SIGSYS) failed: %s", strerror(errno)); + } +} + struct NodeThreadArgs { node_start_fn start; char *argv[3]; @@ -332,6 +375,10 @@ static int runNodeInProcess(const char *nodePath, const char *script) { } logWrite("[launcher] node::Start resolved at %p", (void *)start); + /* seccomp shim BEFORE node runs: trapped syscalls become logged ENOSYS + * instead of a SIGSYS thread kill. */ + installSigsysShim(); + /* argv must outlive the thread — static storage. */ static char arg0[MAX_LINE * 2]; static char arg1[MAX_LINE * 2]; From 9fb25d7d7050bbfbc23619ffe45310c50b3094be Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 16:18:29 +0800 Subject: [PATCH 17/52] hardened stdio for embedded node + crash signal markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device: SIGSYS shim worked — node ran deep into libuv and died on assert(fd > STDERR_FILENO) in uv__close. That assert fires when a uv handle ends up on fd 0/1/2, i.e. a stdio slot was closed and reused. - setupStdioForNode(): snapshot the fd table the child was born with into the boot log, then close 0/1/2 and rebuild deterministically (0=/dev/null, 1=2=boot log, log reopened on slot 1) — the nodejs-mobile pattern; no closed or aliased stdio slots can exist - installCrashMarkers(): SIGABRT/SEGV/BUS/ILL/FPE handlers log which signal killed the child before re-raising — an assert-abort now leaves a marker in the boot log instead of a silent end Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 94 +++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 211e73d..bc8ccb7 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -67,6 +67,7 @@ typedef struct { } LauncherConfig; static int g_logFd = -1; +static char g_logPath[MAX_LINE * 2] = ""; /* for the stdio rebuild below */ /* Forward declaration — dladdr() below takes Main's address. */ void Main(NativeChildProcess_Args args); @@ -90,6 +91,80 @@ static void logWrite(const char *fmt, ...) { "%{public}s", buf); } +/* ── Crash markers: log which signal killed the child before dying ── + * (node's assert → abort() = SIGABRT; without this the boot log just ends). */ +static void crashMarkerHandler(int sig) { + int saved = errno; + char b[64]; + int n = snprintf(b, sizeof(b), "[launcher] process dying: signal %d\n", sig); + if (n > 0) { + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, b, (size_t)n); + (void)ign; + } else if (g_logPath[0]) { + int fd = open(g_logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd >= 0) { + ssize_t ign = write(fd, b, (size_t)n); + (void)ign; + close(fd); + } + } + } + errno = saved; + signal(sig, SIG_DFL); + raise(sig); +} + +static void installCrashMarkers(void) { + const int sigs[] = {SIGABRT, SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGSYS}; + for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) { + signal(sigs[i], crashMarkerHandler); + } +} + +/* ── Deterministic stdio for the embedded node runtime ── + * + * nodejs-mobile lesson + libuv's `assert(fd > STDERR_FILENO)` in uv__close: + * whatever fd state the nativespawn child is born with, node must see + * 0=/dev/null, 1=2=our log — every slot open, no aliasing with higher fds, + * so no uv handle can ever end up on fd 0/1/2 through a closed-then-reused + * slot. Also snapshots the inherited fd table into the boot log (answers + * "what was the child born with" for good). */ +static void setupStdioForNode(void) { + for (int fd = 0; fd <= 9; fd++) { + struct stat st; + if (fstat(fd, &st) == 0) { + const char *tag = "other"; + if (S_ISCHR(st.st_mode)) tag = "chardev"; + else if (S_ISREG(st.st_mode)) tag = "regular"; + else if (S_ISFIFO(st.st_mode)) tag = "fifo/pipe"; + else if (S_ISSOCK(st.st_mode)) tag = "socket"; + logWrite("[launcher] fd %d open at birth: %s", fd, tag); + } + } + + close(0); + close(1); + close(2); + if (g_logFd > 2) { + close(g_logFd); /* reopen below right on slot 1 */ + } + g_logFd = -1; + + int f0 = open("/dev/null", O_RDONLY); /* → 0 */ + int f1 = g_logPath[0] + ? open(g_logPath, O_WRONLY | O_CREAT | O_APPEND, 0644) + : -1; /* → 1 */ + int f2 = dup2(f1 >= 0 ? f1 : f0, 2); /* → 2 */ + g_logFd = 1; + logWrite("[launcher] stdio rebuilt: f0=%d f1=%d f2=%d " + "(0=/dev/null, 1=2=%s)", + f0, f1, f2, g_logPath[0] ? g_logPath : "?"); + (void)f0; + (void)f1; + (void)f2; +} + /* Parse "key=value\n" lines into the config struct. Unknown keys are * also exported as environment variables for the node process. */ static void parseEntryParams(const char *params, LauncherConfig *cfg, @@ -574,10 +649,15 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { char logPath[MAX_LINE * 2]; snprintf(logPath, sizeof(logPath), "%s/node-boot.log", cfg.dataDir); g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); - if (g_logFd < 0) { + if (g_logFd >= 0) { + snprintf(g_logPath, sizeof(g_logPath), "%s", logPath); + } else { snprintf(logPath, sizeof(logPath), "/data/storage/el2/base/files/electerm-data/node-boot.log"); g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (g_logFd >= 0) { + snprintf(g_logPath, sizeof(g_logPath), "%s", logPath); + } } } logWrite("[launcher] Main() entered, pid=%d", (int)getpid()); @@ -645,12 +725,12 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { putenv(extraEnv[i]); } - /* 4. Redirect stdout/stderr into the boot log so node console output and - * crash messages are captured on device. */ - if (g_logFd >= 0) { - dup2(g_logFd, 1); - dup2(g_logFd, 2); - } + /* 4. Rebuild stdio deterministically (0=/dev/null, 1=2=boot log) so node's + * libuv can never see a closed/aliased fd 0/1/2 — the exact condition + * behind libuv's `assert(fd > STDERR_FILENO)`. Also installs crash + * markers: the boot log records which signal killed the child. */ + setupStdioForNode(); + installCrashMarkers(); /* 5. Log the node file's mode + the mount flags of its directory — * noexec / code-integrity enforcement shows up here. */ From f594260b636f73030d23e19a92c2eff06a059402 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 16:21:55 +0800 Subject: [PATCH 18/52] log seccomp-trapped syscall names in the SIGSYS shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Numbers taken from the SDK's asm-generic/unistd.h (aarch64) — the log now reads 'syscall 283 (membarrier)' instead of a bare number. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_launcher.c | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index bc8ccb7..6317016 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -386,6 +386,40 @@ typedef int (*node_start_fn)(int argc, char *argv[]); static int g_sigsysSeen[32]; static int g_sigsysSeenCount = 0; +/* aarch64 (asm-generic) syscall numbers worth naming in the log — the + * suspects an app seccomp policy actually fences off. */ +static const char *syscallName(int sc) { + switch (sc) { + case 19: return "eventfd2"; + case 20: return "epoll_create1"; + case 220: return "clone"; + case 221: return "execve"; + case 241: return "perf_event_open"; + case 265: return "open_by_handle_at"; + case 270: return "process_vm_readv"; + case 272: return "kcmp"; + case 277: return "seccomp"; + case 278: return "getrandom"; + case 280: return "bpf"; + case 281: return "execveat"; + case 282: return "userfaultfd"; + case 283: return "membarrier"; + case 288: return "pkey_mprotect"; + case 291: return "statx"; + case 293: return "rseq"; + case 403: return "clock_gettime64"; + case 424: return "pidfd_send_signal"; + case 425: return "io_uring_setup"; + case 434: return "pidfd_open"; + case 435: return "clone3"; + case 436: return "close_range"; + case 437: return "openat2"; + case 439: return "faccessat2"; + case 440: return "process_madvise"; + default: return "?"; + } +} + static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { (void)sig; int sc = si->si_syscall; /* musl: #define si_syscall __si_fields.__sigsys.si_syscall */ @@ -398,7 +432,8 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } if (!known && g_sigsysSeenCount < 32) { g_sigsysSeen[g_sigsysSeenCount++] = sc; - logWrite("[launcher] SIGSYS: syscall %d blocked by seccomp → ENOSYS", sc); + logWrite("[launcher] SIGSYS: syscall %d (%s) blocked by seccomp → ENOSYS", + sc, syscallName(sc)); } ucontext_t *uc = (ucontext_t *)ctx; /* aarch64: the trapped instruction is the 4-byte `svc #0`; skip it and From cd36857803523b2e1827a07909a797a87563b213 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 16:37:05 +0800 Subject: [PATCH 19/52] feat: run node in-process in the main app (napi), child fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nativespawn child runs under a stricter seccomp filter than the app itself — node's libuv aborts there (SIGABRT; the fd>-2 assert in uv__close fires on uv_loop_init's error path when epoll-class syscalls come back ENOSYS from the SIGSYS shim). The MAIN app process runs libuv-class loops of its own (NETSTACK/curl), so node now runs there, the electron-harmony pattern: - libnode_ctl.so startBackend(entryParams): dlopen libnode.so + node::Start on a 32MB-stack pthread in the app process, SIGSYS shim + crash markers, same node-boot.log channel (tag electerm.embed) - Index.ets tries in-process first, falls back to startNativeChildProcess - BackendManager: skip kill for in-process mode (it IS the app) - node-boot.log hilog dump at failure: 800 → 2400 chars Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/CMakeLists.txt | 7 +- entry/src/main/cpp/node_ctl.c | 422 +++++++++++++++++++++++++- entry/src/main/ets/BackendManager.ets | 24 +- entry/src/main/ets/pages/Index.ets | 48 ++- 4 files changed, 475 insertions(+), 26 deletions(-) diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index c6fa117..663fd9e 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -8,7 +8,8 @@ add_library(node_launcher SHARED node_launcher.c) # builds even when the boot-log file cannot be pulled (cloud debug) target_link_libraries(node_launcher PUBLIC libchild_process.so libhilog_ndk.z.so) -# libnode_ctl.so — NAPI module for the main (ArkTS) process: kill the node -# child by pid on app destroy. +# libnode_ctl.so — NAPI module for the main (ArkTS) process: run node +# IN-PROCESS (dlopen libnode.so + node::Start — the electron-harmony / +# nodejs-mobile pattern) and kill the spawned child by pid on app destroy. add_library(node_ctl SHARED node_ctl.c) -target_link_libraries(node_ctl PUBLIC libace_napi.z.so) +target_link_libraries(node_ctl PUBLIC libace_napi.z.so libhilog_ndk.z.so) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 93708e8..3453820 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -1,17 +1,422 @@ /** - * node_ctl.c — tiny NAPI module used by the ArkTS main process to control - * the Node.js child process (currently: terminate it by pid). + * node_ctl.c — NAPI module for the main (ArkTS) process. * - * Import from ArkTS with: - * import nodeCtl from 'libnode_ctl.so' + * Two jobs: + * 1. killNode(pid) — terminate the native-child-process node backend. + * 2. startBackend(entryParams) — run node IN THIS (main app) PROCESS: + * dlopen the bundled libnode.so and call node::Start on a dedicated + * 32MB-stack pthread (the nodejs-mobile / electron-harmony pattern). * - * Kept in a separate .so from node_launcher.c: that library is loaded into - * the native child process (which has no ArkTS/NAPI runtime), so it must - * not have undefined NAPI symbols. + * Why in-process: the nativespawn CHILD process runs under a stricter + * seccomp filter than the app itself — node's libuv dies there (SIGSYS / + * abort) because event-loop syscalls are fenced off. The MAIN app process + * demonstrably runs libuv-class loops (NETSTACK/curl, the ArkTS runtime), + * so node lives there. electron-harmony works the same way: its node core + * ships as .so libraries inside the app process, never a spawned binary. + * + * Everything is logged to /node-boot.log (same file the child + * launcher writes) and to hilog (tag electerm.embed), so the on-screen + * boot-log overlay shows this path's ladder exactly like the child's. + * + * Returns "ok" synchronously once the node thread is launched; on any + * failure returns "err:" so ArkTS can fall back to the native + * child process. */ + #include "napi/native_api.h" + +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + +#define LOG_BUF_SIZE 4096 +#define MAX_ENV_VARS 32 +#define MAX_LINE 1024 +#define MAX_CANDIDATES 12 + +typedef struct { + char dataDir[MAX_LINE]; /* writable app data dir (el2 filesDir) */ + char script[MAX_LINE * 2]; /* path to resfile/electerm/index.js */ + char node[MAX_LINE * 2]; /* optional parent-provided libnode.so path */ + char port[16]; + char secret[MAX_LINE]; /* SERVER_SECRET */ +} CtlConfig; + +static int g_logFd = -1; +static char g_logPath[MAX_LINE * 2] = ""; +static int g_started = 0; /* startBackend may only run once per process */ + +static void logWrite(const char *fmt, ...) { + char buf[LOG_BUF_SIZE]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf) - 1, fmt, ap); + va_end(ap); + if (n < 0) return; + buf[n] = '\0'; + if (g_logFd >= 0) { + ssize_t ignored = write(g_logFd, buf, (size_t)n); + ignored = write(g_logFd, "\n", 1); + (void)ignored; + } + (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.embed", + "%{public}s", buf); +} + +/* ── Crash markers — one boot-log line naming the killer signal, then the + * default action (so system crash handling still runs). Without this an + * abort() inside node just ends the log silently. ── */ +static void crashMarkerHandler(int sig) { + int saved = errno; + char b[64]; + int n = snprintf(b, sizeof(b), "[embed] process dying: signal %d\n", sig); + if (n > 0 && g_logFd >= 0) { + ssize_t ign = write(g_logFd, b, (size_t)n); + (void)ign; + } + errno = saved; + signal(sig, SIG_DFL); + raise(sig); +} + +static void installCrashMarkers(void) { + const int sigs[] = {SIGABRT, SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGSYS}; + for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) { + signal(sigs[i], crashMarkerHandler); + } +} + +/* ── SIGSYS shim — same as the child launcher's: if a seccomp filter in + * THIS process traps a syscall node probes (perf_event_open, membarrier, + * …), convert the kill into a logged ENOSYS. node's ResetSignalHandlers() + * preserves SA_SIGINFO handlers, so this survives into node's lifetime. ── */ +static const char *syscallName(int sc) { + switch (sc) { + case 19: return "eventfd2"; + case 20: return "epoll_create1"; + case 220: return "clone"; + case 221: return "execve"; + case 241: return "perf_event_open"; + case 265: return "open_by_handle_at"; + case 270: return "process_vm_readv"; + case 272: return "kcmp"; + case 277: return "seccomp"; + case 278: return "getrandom"; + case 280: return "bpf"; + case 281: return "execveat"; + case 282: return "userfaultfd"; + case 283: return "membarrier"; + case 288: return "pkey_mprotect"; + case 291: return "statx"; + case 293: return "rseq"; + case 403: return "clock_gettime64"; + case 424: return "pidfd_send_signal"; + case 425: return "io_uring_setup"; + case 434: return "pidfd_open"; + case 435: return "clone3"; + case 436: return "close_range"; + case 437: return "openat2"; + case 439: return "faccessat2"; + case 440: return "process_madvise"; + default: return "?"; + } +} + +static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { + (void)sig; + static int seen[32]; + static int seenCount = 0; + int sc = si->si_syscall; + int known = 0; + for (int i = 0; i < seenCount; i++) { + if (seen[i] == sc) { + known = 1; + break; + } + } + if (!known && seenCount < 32) { + seen[seenCount++] = sc; + logWrite("[embed] SIGSYS: syscall %d (%s) blocked by seccomp → ENOSYS", + sc, syscallName(sc)); + } + ucontext_t *uc = (ucontext_t *)ctx; + uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ + uc->uc_mcontext.regs[0] = (unsigned long)-ENOSYS; +} + +static void installSigsysShim(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = sigsysHandler; + sa.sa_flags = SA_SIGINFO; + if (sigaction(SIGSYS, &sa, NULL) != 0) { + logWrite("[embed] sigaction(SIGSYS) failed: %s", strerror(errno)); + } +} + +/* Parse "key=value\n" lines (same format the child launcher parses). */ +static void parseEntryParams(const char *params, CtlConfig *cfg, + char extraEnv[MAX_ENV_VARS][MAX_LINE], + int *extraEnvCount) { + snprintf(cfg->port, sizeof(cfg->port), "5577"); + cfg->dataDir[0] = '\0'; + cfg->script[0] = '\0'; + cfg->node[0] = '\0'; + cfg->secret[0] = '\0'; + + char line[MAX_LINE]; + const char *p = params; + while (p && *p) { + const char *eol = strchr(p, '\n'); + size_t len = eol ? (size_t)(eol - p) : strlen(p); + if (len >= sizeof(line)) len = sizeof(line) - 1; + memcpy(line, p, len); + line[len] = '\0'; + p = eol ? eol + 1 : NULL; + + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + const char *key = line; + const char *value = eq + 1; + + if (strcmp(key, "dataDir") == 0) { + snprintf(cfg->dataDir, sizeof(cfg->dataDir), "%s", value); + } else if (strcmp(key, "script") == 0) { + snprintf(cfg->script, sizeof(cfg->script), "%s", value); + } else if (strcmp(key, "node") == 0) { + snprintf(cfg->node, sizeof(cfg->node), "%s", value); + } else if (strcmp(key, "port") == 0) { + snprintf(cfg->port, sizeof(cfg->port), "%s", value); + } else if (strcmp(key, "secret") == 0) { + snprintf(cfg->secret, sizeof(cfg->secret), "%s", value); + } else if (*extraEnvCount < MAX_ENV_VARS) { + snprintf(extraEnv[(*extraEnvCount)++], MAX_LINE, "%s=%s", key, value); + } + } +} + +static void addCandidate(char (*candidates)[MAX_LINE * 2], int *n, + const char *dir, const char *tag) { + if (*n >= MAX_CANDIDATES) return; + if (!dir || !dir[0]) return; + char path[MAX_LINE * 2]; + snprintf(path, sizeof(path), "%s/libnode.so", dir); + for (int i = 0; i < *n; i++) { + if (strcmp(candidates[i], path) == 0) return; + } + snprintf(candidates[(*n)], MAX_LINE * 2, "%s", path); + logWrite("[embed] candidate(%s): %s", tag, candidates[(*n)]); + (*n)++; +} + +typedef int (*node_start_fn)(int argc, char *argv[]); + +struct NodeThreadArgs { + node_start_fn start; + char *argv[3]; + int rc; +}; + +static struct NodeThreadArgs g_nodeArgs; + +/* node::Start returning is abnormal (the server should run forever) — log + * it and let the thread end; the ArkTS probe timeout surfaces the failure. + * NEVER _exit() here: this is the app's main process. */ +static void *nodeThreadMain(void *p) { + struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; + a->rc = a->start(2, a->argv); + logWrite("[embed] node::Start returned %d (backend stopped)", a->rc); + return NULL; +} + +static const char *startEmbeddedNode(const char *params) { + static char errBuf[256]; + + if (g_started) { + return "err:already started"; + } + + char extraEnv[MAX_ENV_VARS][MAX_LINE]; + int extraEnvCount = 0; + CtlConfig cfg; + parseEntryParams(params, &cfg, extraEnv, &extraEnvCount); + + /* boot log — same file the child launcher uses, so the ArkTS overlay and + * hilog dump cover both paths. el2 junction fallback like the child. */ + if (cfg.dataDir[0]) { + char logPath[MAX_LINE * 2]; + snprintf(logPath, sizeof(logPath), "%s/node-boot.log", cfg.dataDir); + g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (g_logFd < 0) { + snprintf(logPath, sizeof(logPath), + "/data/storage/el2/base/files/electerm-data/node-boot.log"); + g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + } + if (g_logFd >= 0) { + snprintf(g_logPath, sizeof(g_logPath), "%s", logPath); + } + } + logWrite("[embed] startBackend: pid=%d params=%s", (int)getpid(), params); + + if (!cfg.script[0] || access(cfg.script, F_OK) != 0) { + logWrite("[embed] FATAL: script missing: %s", cfg.script); + snprintf(errBuf, sizeof(errBuf), "err:script missing"); + return errBuf; + } + + /* locate libnode.so — parent-provided path, then this .so's own dir + * (libnode_ctl.so and libnode.so sit in the same app libs dir), then the + * el1/bundle junction layouts. */ + char candidates[MAX_CANDIDATES][MAX_LINE * 2]; + int nCand = 0; + if (cfg.node[0]) { + snprintf(candidates[nCand], MAX_LINE * 2, "%s", cfg.node); + logWrite("[embed] candidate(parent): %s", candidates[nCand]); + nCand++; + } + { + Dl_info info; + if (dladdr((void *)&startEmbeddedNode, &info) && info.dli_fname && + info.dli_fname[0]) { + const char *slash = strrchr(info.dli_fname, '/'); + if (slash) { + char dir[MAX_LINE * 2]; + snprintf(dir, sizeof(dir), "%.*s", (int)(slash - info.dli_fname), + info.dli_fname); + addCandidate(candidates, &nCand, dir, "dladdr"); + } + } + } + { + const char *bundleDir = "/data/storage/el1/bundle"; + char dir[MAX_LINE * 2]; + snprintf(dir, sizeof(dir), "%s/entry/libs/arm64-v8a", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/entry/libs/arm64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/libs/arm64-v8a", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); + } + + const char *nodePath = NULL; + for (int i = 0; i < nCand; i++) { + if (access(candidates[i], F_OK) == 0) { + nodePath = candidates[i]; + break; + } + logWrite("[embed] candidate not found: %s", candidates[i]); + } + if (!nodePath) { + logWrite("[embed] FATAL: no libnode.so candidate exists (tried %d)", nCand); + snprintf(errBuf, sizeof(errBuf), "err:no libnode.so"); + return errBuf; + } + logWrite("[embed] node binary: %s", nodePath); + + /* environment — mirrors the child launcher exactly */ + setenv("NODE_ENV", "production", 1); + setenv("HOST", "127.0.0.1", 1); + setenv("PORT", cfg.port, 1); + setenv("ELECTERM_DATA_DIR", cfg.dataDir, 1); + if (cfg.secret[0]) { + setenv("SERVER_SECRET", cfg.secret, 1); + } + for (int i = 0; i < extraEnvCount; i++) { + putenv(extraEnv[i]); + } + + /* DO NOT touch stdio here: fds 0/1/2 belong to the app process and are + * all valid (node's PlatformInit only fstats them). The boot log keeps a + * private fd > 2. */ + installCrashMarkers(); + installSigsysShim(); + + if (cfg.dataDir[0] && chdir(cfg.dataDir) == 0) { + logWrite("[embed] cwd: %s", cfg.dataDir); + } + + logWrite("[embed] in-process(main): dlopen(%s)", nodePath); + void *h = dlopen(nodePath, RTLD_NOW | RTLD_LOCAL); + if (!h) { + /* retry by bare name — the linker namespace search path contains the + * app libs dir even when an absolute-path dlopen is refused */ + const char *e1 = dlerror(); + logWrite("[embed] dlopen(abs) failed: %s — retrying bare name", e1 ? e1 : "?"); + h = dlopen("libnode.so", RTLD_NOW | RTLD_LOCAL); + if (!h) { + const char *e2 = dlerror(); + logWrite("[embed] dlopen failed: %s / %s", e1 ? e1 : "-", e2 ? e2 : "-"); + snprintf(errBuf, sizeof(errBuf), "err:dlopen failed"); + return errBuf; + } + } + dlerror(); + node_start_fn start = (node_start_fn)dlsym(h, "_ZN4node5StartEiPPc"); + const char *e = dlerror(); + if (!start || (e && e[0])) { + logWrite("[embed] dlsym(node::Start) failed: %s", e ? e : "null sym"); + snprintf(errBuf, sizeof(errBuf), "err:node::Start not found"); + return errBuf; + } + logWrite("[embed] node::Start resolved at %p", (void *)start); + + /* argv must outlive the thread — static storage */ + static char arg0[MAX_LINE * 2]; + static char arg1[MAX_LINE * 2]; + snprintf(arg0, sizeof(arg0), "%s", nodePath); + snprintf(arg1, sizeof(arg1), "%s", cfg.script); + g_nodeArgs.start = start; + g_nodeArgs.argv[0] = arg0; + g_nodeArgs.argv[1] = arg1; + g_nodeArgs.argv[2] = NULL; + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 32 * 1024 * 1024); /* node wants a big stack */ + pthread_t th; + int prc = pthread_create(&th, &attr, nodeThreadMain, &g_nodeArgs); + if (prc != 0) { + logWrite("[embed] pthread_create failed: %s", strerror(prc)); + snprintf(errBuf, sizeof(errBuf), "err:pthread_create failed"); + return errBuf; + } + pthread_detach(th); + g_started = 1; + logWrite("[embed] node thread launched in main app process"); + return "ok"; +} + +/* ── NAPI surface ── */ + +static napi_value StartBackend(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + + char params[8192] = ""; + if (argc >= 1) { + size_t copied = 0; + napi_get_value_string_utf8(env, args[0], params, sizeof(params), &copied); + } + const char *result = startEmbeddedNode(params); + + napi_value napiResult = NULL; + napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &napiResult); + return napiResult; +} static napi_value KillNode(napi_env env, napi_callback_info info) { size_t argc = 1; @@ -40,7 +445,8 @@ static napi_value KillNode(napi_env env, napi_callback_info info) { EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { - {"killNode", NULL, KillNode, NULL, NULL, NULL, napi_default, NULL}}; + {"killNode", NULL, KillNode, NULL, NULL, NULL, napi_default, NULL}, + {"startBackend", NULL, StartBackend, NULL, NULL, NULL, napi_default, NULL}}; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } diff --git a/entry/src/main/ets/BackendManager.ets b/entry/src/main/ets/BackendManager.ets index d56d3f2..6b38a2d 100644 --- a/entry/src/main/ets/BackendManager.ets +++ b/entry/src/main/ets/BackendManager.ets @@ -1,21 +1,39 @@ /** - * BackendManager — tracks the Node.js child process for the app lifetime. + * BackendManager — tracks the Node.js backend for the app lifetime. + * + * pages/Index starts the backend and records how it runs: + * - in-process (node::Start on a thread of the MAIN app process, via + * libnode_ctl.so startBackend — the electron-harmony pattern; dodges + * the nativespawn child's stricter seccomp) → inProcess = true + * - native child process (libnode_launcher.so:Main) → pid * - * pages/Index starts the backend and records the pid here; * EntryAbility.onDestroy() calls BackendManager.killBackend() so the port - * is freed when the app is terminated. + * is freed when the app is terminated — killing is only possible for the + * child variant (in-process node dies with the app itself). */ import { killNode } from 'libnode_ctl.so'; export class BackendManager { static pid: number = -1; + static inProcess: boolean = false; static setPid(pid: number): void { BackendManager.pid = pid; + BackendManager.inProcess = false; + } + + static setInProcess(): void { + BackendManager.inProcess = true; + BackendManager.pid = -1; } static killBackend(): void { + if (BackendManager.inProcess) { + // node runs on a thread of the app process itself — it ends with the + // process; killing "our own pid" would kill the UI too. + return; + } if (BackendManager.pid > 0) { try { killNode(BackendManager.pid); diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index f7d52a9..2375a6a 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -3,9 +3,11 @@ * * Startup sequence: * 1. create the writable data dir (filesDir/electerm-data) - * 2. start the Node.js backend as a *native child process* - * (libnode_launcher.so:Main → execv libnode.so index.js — the backend - * serves the UI + SSH/SFTP/... API on http://127.0.0.1:5577) + * 2. start the Node.js backend — primarily IN-PROCESS in this app + * process (libnode_ctl.so startBackend → dlopen libnode.so → + * node::Start; the electron-harmony pattern), falling back to a + * *native child process* (libnode_launcher.so:Main). The backend + * serves the UI + SSH/SFTP/... API on http://127.0.0.1:5577 * 3. poll the backend with plain HTTP until it answers * 4. navigate the Web component from the local loading page to the backend * @@ -23,6 +25,7 @@ import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import fs from '@ohos.file.fs'; import http from '@ohos.net.http'; +import { startBackend } from 'libnode_ctl.so'; import { BackendManager } from '../BackendManager'; const TAG: string = 'electerm.Index'; @@ -134,13 +137,34 @@ struct Index { const entryParams: string = paramLines.join('\n'); this.statusMessage = 'Starting Node.js engine …'; - hilog.info(DOMAIN, TAG, 'starting native child process, params: %{public}s', entryParams); - const pid: number = await childProcessManager.startNativeChildProcess( - 'libnode_launcher.so:Main', - { entryParams: entryParams } - ); - BackendManager.setPid(pid); - hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); + hilog.info(DOMAIN, TAG, 'backend params: %{public}s', entryParams); + + // Primary path: run node IN the main app process (dlopen libnode.so + + // node::Start via libnode_ctl.so — the electron-harmony / nodejs-mobile + // pattern). The nativespawn child runs under a stricter seccomp filter + // whose event-loop syscall blocks kill libuv there; the main process + // runs libuv-class loops of its own (NETSTACK/curl), so node lives here. + let startedInProcess: boolean = false; + try { + const rc: string = startBackend(entryParams); + startedInProcess = rc === 'ok'; + hilog.info(DOMAIN, TAG, 'in-process startBackend → %{public}s', rc); + } catch (e) { + hilog.error(DOMAIN, TAG, 'in-process startBackend threw: %{public}s', JSON.stringify(e)); + } + + if (startedInProcess) { + BackendManager.setInProcess(); + hilog.info(DOMAIN, TAG, 'node running in-process (main app process)'); + } else { + // Fallback: native child process (libnode_launcher.so:Main). + const pid: number = await childProcessManager.startNativeChildProcess( + 'libnode_launcher.so:Main', + { entryParams: entryParams } + ); + BackendManager.setPid(pid); + hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); + } // 3. wait for the HTTP server const ok: boolean = await this.waitForBackend(); @@ -214,8 +238,8 @@ struct Index { 'node-boot.log empty/missing at %{public}s — child never wrote anything', this.dataDir); return; } - const tail: string = text.length > 800 - ? text.substring(text.length - 800) + const tail: string = text.length > 2400 + ? text.substring(text.length - 2400) : text; hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); } From cff07ef88c38f737a5be650518d5406e1c0dbc6d Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 16:53:05 +0800 Subject: [PATCH 20/52] =?UTF-8?q?fix:=20survive=20node=20crashes=20in-proc?= =?UTF-8?q?ess=20=E2=80=94=20park=20the=20node=20thread,=20log=20to=20hilo?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-process run killed the whole app on node's first fatal signal (app closes instantly, no overlay, no dump — the capture window missed it entirely). Now: - fatal signals print the marker to hilog IMMEDIATELY (electerm.embed 'fatal: signal N on tid T') — survives the process - if the signal hit the node thread, park that thread forever instead of dying: the app stays on the loading screen and the 8-line overlay shows the boot-log tail - boot log truncated per attempt so the overlay reflects this run Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_ctl.c | 57 +++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 3453820..4e9caa3 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -75,26 +75,53 @@ static void logWrite(const char *fmt, ...) { "%{public}s", buf); } -/* ── Crash markers — one boot-log line naming the killer signal, then the - * default action (so system crash handling still runs). Without this an - * abort() inside node just ends the log silently. ── */ -static void crashMarkerHandler(int sig) { +/* ── Crash markers — name the killer signal in the boot log AND hilog (the + * process may die right after; hilog keeps the line). If the signal came + * from node's own thread, PARK that thread instead of dying: the app UI + * process survives, the ArkTS probe times out and the on-screen overlay + * shows the boot-log tail — instead of the app just closing. ── */ +static pid_t g_nodeTid = 0; /* tid of the node thread once launched */ + +static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { + (void)si; + (void)ctx; int saved = errno; - char b[64]; - int n = snprintf(b, sizeof(b), "[embed] process dying: signal %d\n", sig); - if (n > 0 && g_logFd >= 0) { - ssize_t ign = write(g_logFd, b, (size_t)n); - (void)ign; + long tid = (long)syscall(__NR_gettid); + char b[128]; + int n = snprintf(b, sizeof(b), "[embed] fatal: signal %d on tid %ld", + sig, tid); + if (n > 0) { + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, b, (size_t)n); + ign = write(g_logFd, "\n", 1); + (void)ign; + } + (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.embed", + "%{public}s", b); } errno = saved; + if (g_nodeTid > 0 && tid == (long)g_nodeTid) { + /* node's thread crashed — freeze it, keep the app alive. Never returns; + * if the crash corrupted a libc lock the UI may eventually freeze too, + * but the evidence is already on disk and in hilog. */ + for (;;) { + pause(); + } + } signal(sig, SIG_DFL); raise(sig); } static void installCrashMarkers(void) { - const int sigs[] = {SIGABRT, SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGSYS}; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = crashMarkerHandler; + sa.sa_flags = SA_SIGINFO; + const int sigs[] = {SIGABRT, SIGSEGV, SIGBUS, SIGILL, SIGFPE}; for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) { - signal(sigs[i], crashMarkerHandler); + if (sigaction(sigs[i], &sa, NULL) != 0) { + logWrite("[embed] sigaction(%d) failed: %s", sigs[i], strerror(errno)); + } } } @@ -237,6 +264,8 @@ static struct NodeThreadArgs g_nodeArgs; * NEVER _exit() here: this is the app's main process. */ static void *nodeThreadMain(void *p) { struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; + g_nodeTid = (pid_t)syscall(__NR_gettid); + logWrite("[embed] node thread tid=%ld, calling node::Start", (long)g_nodeTid); a->rc = a->start(2, a->argv); logWrite("[embed] node::Start returned %d (backend stopped)", a->rc); return NULL; @@ -255,11 +284,13 @@ static const char *startEmbeddedNode(const char *params) { parseEntryParams(params, &cfg, extraEnv, &extraEnvCount); /* boot log — same file the child launcher uses, so the ArkTS overlay and - * hilog dump cover both paths. el2 junction fallback like the child. */ + * hilog dump cover both paths. Truncated per attempt: the overlay only + * shows the last lines, and hilog keeps the history anyway. el2 junction + * fallback like the child. */ if (cfg.dataDir[0]) { char logPath[MAX_LINE * 2]; snprintf(logPath, sizeof(logPath), "%s/node-boot.log", cfg.dataDir); - g_logFd = open(logPath, O_WRONLY | O_CREAT | O_APPEND, 0644); + g_logFd = open(logPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (g_logFd < 0) { snprintf(logPath, sizeof(logPath), "/data/storage/el2/base/files/electerm-data/node-boot.log"); From 7d54ecad186c9bbcdb1cea413e0191112b8bf4d5 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 17:04:06 +0800 Subject: [PATCH 21/52] diag: periodic boot-log tail dumps to hilog + 14-line failure overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cloud-debug hilog export delivers the OLDEST buffered lines and system noise (~1600 lines/s) flushes the ring long before the 90s boot deadline — every evidence dump so far landed outside the exported window. Now the tail (last 1100 chars) is re-dumped to hilog every ~2s while waiting, so a copy of the ladder + crash marker always sits inside whatever window survives. Failure overlay: 8 → 14 lines. Co-Authored-By: Claude Fable 5 --- entry/src/main/ets/pages/Index.ets | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 2375a6a..2eff79d 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -170,7 +170,7 @@ struct Index { const ok: boolean = await this.waitForBackend(); if (!ok) { this.bootFailed = true; - const tail: string = this.readBootLogTailLines(8); + const tail: string = this.readBootLogTailLines(14); this.statusMessage = tail ? tail : 'Engine failed to start (no node-boot.log — child never ran?)'; @@ -232,14 +232,18 @@ struct Index { /** Dump the launcher's boot log tail to hilog — the fastest way to see why * exec failed when only hilog (no file pull) is available, e.g. cloud debug. */ logBootTail(): void { + this.logBootTailWindow(2400); + } + + /** Dump the last `n` chars of the boot log to hilog (periodic ring-buffer + * insurance — see waitForBackend). */ + logBootTailWindow(n: number): void { const text: string = this.readBootLog(); if (!text) { - hilog.error(DOMAIN, TAG, - 'node-boot.log empty/missing at %{public}s — child never wrote anything', this.dataDir); return; } - const tail: string = text.length > 2400 - ? text.substring(text.length - 2400) + const tail: string = text.length > n + ? text.substring(text.length - n) : text; hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); } @@ -281,6 +285,12 @@ struct Index { this.statusMessage = line; hilog.info(DOMAIN, TAG, 'boot: %{public}s', line); } + // Periodic tail dump: the cloud-debug hilog export grabs the OLDEST + // buffered lines, and ~1600 lines/s of system noise flushes the ring + // long before the 90s deadline — re-dumping the tail every ~2s keeps + // a copy of the boot ladder (and any crash marker) inside whatever + // window survives. + this.logBootTailWindow(1100); } await this.sleep(POLL_INTERVAL_MS); } @@ -343,7 +353,7 @@ struct Index { .color('#4aa3ff') } Text(this.statusMessage) - .fontSize(13) + .fontSize(this.bootFailed ? 11 : 13) .fontColor(this.bootFailed ? '#e5484d' : '#8b93a7') .textAlign(TextAlign.Center) .padding({ left: 24, right: 24 }) From f143bfafaa442d6c00fe7b13576db49b84b0331f Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 17:05:44 +0800 Subject: [PATCH 22/52] =?UTF-8?q?diag:=20capture=20node's=20stderr=20in=20?= =?UTF-8?q?embed=20mode=20=E2=80=94=20dup2=20boot=20log=20onto=20fd=201/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the child we rebuilt stdio, so assert/abort text reached the log; in embed mode node's stderr was the app's fd 2 (/dev/null) — abort messages vanished, leaving only the crash marker. Redirect 1/2 onto the boot log (the app runtime logs via hilog, not stdio) so the next run shows the actual assertion text. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_ctl.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 4e9caa3..a52b73d 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -369,11 +369,17 @@ static const char *startEmbeddedNode(const char *params) { putenv(extraEnv[i]); } - /* DO NOT touch stdio here: fds 0/1/2 belong to the app process and are - * all valid (node's PlatformInit only fstats them). The boot log keeps a - * private fd > 2. */ + /* fds 0/1/2 stay VALID (node's PlatformInit only fstats them) — but node's + * stderr must land in the boot log or abort()/assert messages vanish into + * the app's own stderr (/dev/null). Redirect 1/2 onto the log fd; the app + * runtime logs via hilog, not stdio, so nothing of value is lost. */ installCrashMarkers(); installSigsysShim(); + if (g_logFd > 2) { + dup2(g_logFd, 1); + dup2(g_logFd, 2); + logWrite("[embed] stdout/stderr redirected to node-boot.log"); + } if (cfg.dataDir[0] && chdir(cfg.dataDir) == 0) { logWrite("[embed] cwd: %s", cfg.dataDir); From 4a5a8d425019984bb3224d5679055a42f9f176e2 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 17:29:23 +0800 Subject: [PATCH 23/52] diag: stream stdio to hilog line-by-line; chunk tail dumps under hilog truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device evidence (17:23:49): in-main-process run got all the way to node::Start on tid 30146; the ONLY seccomp-trapped syscall is io_uring_setup (425) → ENOSYS, which libuv handles gracefully (uv__iou_init returns on ringfd<0). Signal 6 abort follows inside node/V8 startup — but its stderr text never reached us: hilog truncates messages at ~140 bytes and ArkWeb stdout junk buries the file tail. - pipe fd 1/2 to a reader thread that logs every line to the boot log AND hilog ([io] prefix, electerm.embed) — assert/abort text arrives live, un-truncated - UV_USE_IO_URING=0 (the one trapped syscall, now skipped up front) - tail dumps split into 110-char chunks, one hilog line each Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_ctl.c | 64 ++++++++++++++++++++++++++---- entry/src/main/ets/pages/Index.ets | 12 +++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index a52b73d..dbf987a 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -57,6 +57,36 @@ typedef struct { static int g_logFd = -1; static char g_logPath[MAX_LINE * 2] = ""; static int g_started = 0; /* startBackend may only run once per process */ +static int g_pipeOut = -1; /* read end of the stdio→hilog pipe */ + +/* Stream everything written to the app's stdout/stderr (fd 1/2 are dup2'd + * onto a pipe) into the boot log AND hilog, line by line. hilog truncates + * single messages at ~140 bytes, so chunked/tail dumps can't carry node's + * abort text — a live line-sized reader can. */ +static void *stdioReaderThread(void *p) { + (void)p; + char buf[2048]; + char line[480]; + size_t linelen = 0; + for (;;) { + ssize_t r = read(g_pipeOut, buf, sizeof(buf)); + if (r < 0 && errno == EINTR) continue; + if (r <= 0) break; + for (ssize_t i = 0; i < r; i++) { + char c = buf[i]; + if (c == '\n' || linelen >= sizeof(line) - 1) { + line[linelen] = '\0'; + if (linelen > 0) { + logWrite("[io] %.470s", line); + } + linelen = 0; + } else if (c != '\r' && c != '\0') { + line[linelen++] = c; + } + } + } + return NULL; +} static void logWrite(const char *fmt, ...) { char buf[LOG_BUF_SIZE]; @@ -369,16 +399,36 @@ static const char *startEmbeddedNode(const char *params) { putenv(extraEnv[i]); } - /* fds 0/1/2 stay VALID (node's PlatformInit only fstats them) — but node's - * stderr must land in the boot log or abort()/assert messages vanish into - * the app's own stderr (/dev/null). Redirect 1/2 onto the log fd; the app - * runtime logs via hilog, not stdio, so nothing of value is lost. */ + /* node's stderr must be observable or abort()/assert messages vanish: + * redirect fd 1/2 onto a pipe and stream it line-by-line to the boot log + * AND hilog (hilog truncates long messages, so file-only capture is not + * readable in cloud debug). The reader thread keeps draining so framework + * printf traffic (ArkWeb config spam) can never block a writer. */ installCrashMarkers(); installSigsysShim(); + setenv("UV_USE_IO_URING", "0", 1); /* io_uring_setup is seccomp-trapped */ if (g_logFd > 2) { - dup2(g_logFd, 1); - dup2(g_logFd, 2); - logWrite("[embed] stdout/stderr redirected to node-boot.log"); + int fds[2]; + if (pipe(fds) == 0) { + g_pipeOut = fds[0]; + dup2(fds[1], 1); + dup2(fds[1], 2); + close(fds[1]); + pthread_t rd; + pthread_attr_t ra; + pthread_attr_init(&ra); + pthread_attr_setstacksize(&ra, 256 * 1024); + if (pthread_create(&rd, &ra, stdioReaderThread, NULL) == 0) { + pthread_detach(rd); + logWrite("[embed] stdio piped: reader streaming to boot log + hilog"); + } else { + logWrite("[embed] reader thread failed: %s", strerror(errno)); + } + } else { + dup2(g_logFd, 1); + dup2(g_logFd, 2); + logWrite("[embed] stdout/stderr redirected to node-boot.log"); + } } if (cfg.dataDir[0] && chdir(cfg.dataDir) == 0) { diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 2eff79d..90c5055 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -236,7 +236,8 @@ struct Index { } /** Dump the last `n` chars of the boot log to hilog (periodic ring-buffer - * insurance — see waitForBackend). */ + * insurance — see waitForBackend). hilog truncates a single message at + * ~140 bytes, so the tail goes out in 110-char chunks, each its own line. */ logBootTailWindow(n: number): void { const text: string = this.readBootLog(); if (!text) { @@ -245,7 +246,14 @@ struct Index { const tail: string = text.length > n ? text.substring(text.length - n) : text; - hilog.error(DOMAIN, TAG, 'node-boot.log tail: %{public}s', tail); + const parts: string[] = []; + for (let i = 0; i < tail.length; i += 110) { + parts.push(tail.substring(i, i + 110)); + } + hilog.error(DOMAIN, TAG, 'node-boot.log tail ▼ %{public}d parts', parts.length.toString()); + for (let i = 0; i < parts.length; i++) { + hilog.error(DOMAIN, TAG, 't%{public}d: %{public}s', i.toString(), parts[i]); + } } fileExists(path: string): boolean { From 8fcdda575e99db321f2301a7d5307e4dbbc0096c Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 17:29:42 +0800 Subject: [PATCH 24/52] fix: forward-declare logWrite (broke compile in 4a5a8d4) Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_ctl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index dbf987a..553e633 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -59,6 +59,8 @@ static char g_logPath[MAX_LINE * 2] = ""; static int g_started = 0; /* startBackend may only run once per process */ static int g_pipeOut = -1; /* read end of the stdio→hilog pipe */ +static void logWrite(const char *fmt, ...); + /* Stream everything written to the app's stdout/stderr (fd 1/2 are dup2'd * onto a pipe) into the boot log AND hilog, line by line. hilog truncates * single messages at ~140 bytes, so chunked/tail dumps can't carry node's From b3ba3594e37254c4df85fb8c5c6624a845c77371 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 17:48:44 +0800 Subject: [PATCH 25/52] embed: capture native backtrace at crash + guarantee std fds 0-2 open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The libuv assert (fd > STDERR_FILENO, uv__close core.c:646) names the dying function but not its caller; static analysis of every startup uv__close path (loop init fail paths, io_uring init, signal pipes) came back clean, so make the next device run self-diagnosing: - crashMarkerHandler: backtrace() up to 24 frames, each annotated with dladdr symbol+offset (libnode.so is unstripped — real symbol names are resolvable) and written to the boot log AND hilog, plus full backtrace_symbols_fd dump to the boot log. - Before the stdio pipe: fstat fds 0/1/2 and open /dev/null onto any EBADF fd — a closed std fd at app spawn means pipe() returns fd 0/1, dup2(x,x) no-ops, close() re-closes it, and every later cleanup closes a std fd, tripping exactly this assert. Repair + log the fd layout. - node_ctl built with -fno-omit-frame-pointer so backtrace() walks our frames reliably. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/CMakeLists.txt | 2 + entry/src/main/cpp/node_ctl.c | 75 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index 663fd9e..c623134 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -11,5 +11,7 @@ target_link_libraries(node_launcher PUBLIC libchild_process.so libhilog_ndk.z.so # libnode_ctl.so — NAPI module for the main (ArkTS) process: run node # IN-PROCESS (dlopen libnode.so + node::Start — the electron-harmony / # nodejs-mobile pattern) and kill the spawned child by pid on app destroy. +# -fno-omit-frame-pointer: backtrace() in the crash marker walks the fp chain. add_library(node_ctl SHARED node_ctl.c) +target_compile_options(node_ctl PRIVATE -fno-omit-frame-pointer) target_link_libraries(node_ctl PUBLIC libace_napi.z.so libhilog_ndk.z.so) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 553e633..1c7c9e3 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -131,6 +132,48 @@ static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.embed", "%{public}s", b); } + /* Capture the crashing thread's native stack. The abort text (e.g. + * "Assertion failed: fd > STDERR_FILENO ... uv__close") names the dying + * function but never its CALLER — that's the missing piece. libnode.so is + * unstripped, so dladdr often resolves real symbol names. Written to the + * boot log AND hilog (short lines survive hilog's ~140-byte truncation). + * Not strictly async-signal-safe, but the thread is about to park/die — + * a corrupted-stack failure here costs nothing the crash didn't already. */ + { + void *bt[24]; + int frames = backtrace(bt, 24); + for (int i = 0; i < frames; i++) { + Dl_info info; + char lb[192]; + int ln; + if (dladdr(bt[i], &info) && info.dli_fname) { + const char *slash = strrchr(info.dli_fname, '/'); + const char *base = slash ? slash + 1 : info.dli_fname; + ln = snprintf(lb, sizeof(lb), "[embed] bt[%d/%d] %s%s%+ld (%.40s)", + i, frames, + info.dli_sname ? info.dli_sname : "", + info.dli_sname ? "+" : "", + (long)((char *)bt[i] - (char *)info.dli_fbase), + base); + } else { + ln = snprintf(lb, sizeof(lb), "[embed] bt[%d/%d] %p", i, frames, + bt[i]); + } + if (ln <= 0) continue; + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, lb, (size_t)ln); + ign = write(g_logFd, "\n", 1); + (void)ign; + } + (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.embed", + "%{public}s", lb); + } + if (g_logFd >= 0 && frames > 0) { + ssize_t ign = write(g_logFd, "[embed] backtrace symbols:\n", 27); + backtrace_symbols_fd(bt, frames, g_logFd); + (void)ign; + } + } errno = saved; if (g_nodeTid > 0 && tid == (long)g_nodeTid) { /* node's thread crashed — freeze it, keep the app alive. Never returns; @@ -401,6 +444,37 @@ static const char *startEmbeddedNode(const char *params) { putenv(extraEnv[i]); } + /* Guarantee fds 0/1/2 are open before anything node-related runs. libuv's + * uv__close() asserts fd > STDERR_FILENO — if the app process was spawned + * with a closed std fd, pipe() below hands back fd 0/1, dup2() then + * no-ops (dup2(x,x)) and close() re-closes it, and every later cleanup + * path closes a std fd → assert. /dev/null onto any EBADF fd, and log + * the before/after so the device log shows the real fd layout. */ + { + char fix[96]; + int off = 0; + for (int fd = 0; fd <= 2; fd++) { + struct stat st; + if (fstat(fd, &st) == 0) continue; + int nfd = open("/dev/null", O_RDWR); + if (nfd < 0) { + logWrite("[embed] std fd %d closed, /dev/null open failed: %s", fd, + strerror(errno)); + continue; + } + if (nfd != fd) { + dup2(nfd, fd); + close(nfd); + } + off += snprintf(fix + off, sizeof(fix) - (size_t)off, " fd%d=/dev/null", + fd); + if (off >= (int)sizeof(fix) - 16) break; + } + if (off > 0) { + logWrite("[embed] stdio repair:%s", fix); + } + } + /* node's stderr must be observable or abort()/assert messages vanish: * redirect fd 1/2 onto a pipe and stream it line-by-line to the boot log * AND hilog (hilog truncates long messages, so file-only capture is not @@ -412,6 +486,7 @@ static const char *startEmbeddedNode(const char *params) { if (g_logFd > 2) { int fds[2]; if (pipe(fds) == 0) { + logWrite("[embed] stdio pipe: read=%d write=%d", fds[0], fds[1]); g_pipeOut = fds[0]; dup2(fds[1], 1); dup2(fds[1], 2); From 521ce7ad215096a6c2f22b351dc31a037d300820 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 18:00:03 +0800 Subject: [PATCH 26/52] =?UTF-8?q?shim:=20SIGSYS=20fallback=20must=20return?= =?UTF-8?q?=20exactly=20-1,=20not=20-ENOSYS=20=E2=80=94=20root=20cause=20o?= =?UTF-8?q?f=20the=20uv=5F=5Fclose=20assert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device backtrace (b3ba359 run, symbolized against the unstripped libnode.so) finally named the abort caller: uv__close_nocheckstdio ← uv__close (assert fd > STDERR_FILENO) ← uv__iou_init fail path (linux.c:624) ← uv__platform_loop_init ← uv_loop_init (loop.c:79) ← node::tracing::LegacyTracingAgent ctor ← node::tracing::Agent::CreateDefault ← node::V8Platform::Initialize ← node::InitializeOncePerProcessInternal ← node::Start Chain of events: io_uring_setup is seccomp-trapped (SIGSYS), the shim resumed with x0 = -ENOSYS (-38); OHOS musl's syscall() passes raw x0 through WITHOUT upstream musl's __syscall_ret errno-translation, so uv__iou_init received ringfd = -38, passed its 'if (ringfd == -1) return;' guard, failed mmap/epoll_ctl on the bogus fd, and its cleanup called uv__close(-38) → libuv assert → SIGABRT on the node thread. Fix: return exactly -1 (the universal failure value every caller checks) and set errno = ENOSYS (TLS store, async-signal-safe) in BOTH shims — node_ctl.c (main process) and node_launcher.c (native child, which died of the same assert). The tracing-agent loop is the FIRST libuv loop node creates, so this fired before any script ran. Co-Authored-By: Claude Fable 5 --- entry/src/main/cpp/node_ctl.c | 10 +++++++++- entry/src/main/cpp/node_launcher.c | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 1c7c9e3..e878e41 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -255,7 +255,15 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } ucontext_t *uc = (ucontext_t *)ctx; uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ - uc->uc_mcontext.regs[0] = (unsigned long)-ENOSYS; + /* Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes the raw + * x0 through WITHOUT the __syscall_ret(errno)-translation upstream musl + * does, so -38 leaks to callers as a bogus value. Device-proven: libuv's + * uv__iou_init() got ringfd=-38 from the seccomp-trapped io_uring_setup, + * sailed past its `if (ringfd == -1) return;` guard, failed mmap/epoll_ctl + * on the bogus fd, and its cleanup called uv__close(-38) → the very assert + * (fd > STDERR_FILENO) that killed the backend. */ + uc->uc_mcontext.regs[0] = (unsigned long)-1; + errno = ENOSYS; /* TLS store — async-signal-safe; for errno-checking callers */ } static void installSigsysShim(void) { diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 6317016..5b99849 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -437,9 +437,15 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } ucontext_t *uc = (ucontext_t *)ctx; /* aarch64: the trapped instruction is the 4-byte `svc #0`; skip it and - * put -ENOSYS in x0 (the syscall return register). */ + * put the failure value in x0 (the syscall return register). + * Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes raw x0 + * through without __syscall_ret errno-translation, so -38 leaks out as a + * bogus value — device-proven fatal in libuv uv__iou_init(): ringfd=-38 + * passed its `== -1` guard, mmap/epoll_ctl failed, cleanup called + * uv__close(-38) → assert(fd > STDERR_FILENO) → abort. */ uc->uc_mcontext.pc += 4; - uc->uc_mcontext.regs[0] = (unsigned long)-ENOSYS; + uc->uc_mcontext.regs[0] = (unsigned long)-1; + errno = ENOSYS; /* TLS store — async-signal-safe; for errno-checking callers */ } static void installSigsysShim(void) { From 8d0b3c4d6fdfc5104c5b082d53ce199be12899cf Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 28 Aug 2026 18:20:19 +0800 Subject: [PATCH 27/52] fix signal-handler reentrancy crash + make the boot log readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device log 18_11_18 (521ce7a): the uv__close assert is GONE (the -1 shim fix worked) but the node thread now dies of SIGSEGV inside strlen called from printf machinery, with the interrupted frames being uv__io_uring_setup <- uv__iou_init — i.e. the crash is INSIDE our own SIGSYS handler: its first-sight logging used logWrite (vsnprintf + OH_LOG_Print), which take libc locks; the second seccomp trap fired while the node thread held one, so the handler's own formatting crashed. Classic async-signal-safety violation. - sigsysHandler (node_ctl.c + node_launcher.c): logging is now async-signal-safe — fixed strings + manual decimal into a stack buffer, write(2) only, to the boot log and to fd 2 (the main-process stdio pipe, where the reader thread relays it to hilog in normal context). Per-syscall dedup via a 512-bit bitmap. - stdioReaderThread: filter ArkWeb/chromium framework lines (nweb_, render_, updater, cloud-control, compositor, …) so node's console output, asserts and stack traces are not buried — the on-screen boot overlay shows the boot log tail and was 100% ArkWeb spam last run. - Generated backend entry (build/web/build.mjs): append [backend] milestones directly to node-boot.log (entry running / bundle imported / uncaughtException / unhandledRejection / exit code) — direct file writes bypass stdout entirely, immune to fd-level noise. - CI: setup-java v4 -> v5 (v4 is deprecated). Co-Authored-By: Claude Fable 5 --- .github/workflows/build-web.yml | 2 +- build/web/build.mjs | 15 +++- entry/src/main/cpp/node_ctl.c | 107 +++++++++++++++++++++++++---- entry/src/main/cpp/node_launcher.c | 63 +++++++++++++---- 4 files changed, 156 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 45e5fb0..517458f 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -58,7 +58,7 @@ jobs: # ── Setup JDK (for hap-sign-tool.jar) ──────────────────────────────── - name: Setup JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' diff --git a/build/web/build.mjs b/build/web/build.mjs index 26b3d05..ba48806 100644 --- a/build/web/build.mjs +++ b/build/web/build.mjs @@ -163,11 +163,24 @@ async function bundleBackend () { function writeNodeEntry () { const entry = `import { resolve } from 'node:path' -import { mkdirSync } from 'node:fs' +import { mkdirSync, appendFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' const __d = fileURLToPath(new URL('.', import.meta.url)) +// Boot milestones are appended DIRECTLY to the launcher's node-boot.log — +// file writes bypass stdout, so ArkWeb/chromium logging (which shares this +// process's fd 1/2 with node) can never bury them. This is the primary +// diagnostics channel on device. +const __bootLog = resolve(process.env.ELECTERM_DATA_DIR || __d, 'node-boot.log') +const boot = (msg) => { + try { appendFileSync(__bootLog, \`[backend] \${msg}\\n\`) } catch {} +} +process.on('uncaughtException', (e) => boot(\`uncaughtException: \${(e && e.stack) || e}\`)) +process.on('unhandledRejection', (e) => boot(\`unhandledRejection: \${(e && e.stack) || e}\`)) +process.on('exit', (code) => boot(\`node process exit, code=\${code}\`)) +boot('entry.js running') + // The node binary is exec'd by the native launcher with cwd inherited from // the app process; electerm's runtime-constants.js reads "package.json" via // resolve(process.cwd(), 'package.json'), so switch cwd to this directory diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index e878e41..8eb0c07 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -62,15 +62,50 @@ static int g_pipeOut = -1; /* read end of the stdio→hilog pipe */ static void logWrite(const char *fmt, ...); +/* ArkWeb/chromium logs to the same process-level stdout/stderr we redirected + * for node — at thousands of lines it buries node's output in node-boot.log + * and in the on-screen overlay. Skip the known framework prefixes so what + * remains (node console output, asserts, stack traces) stays readable. */ +static int isFrameworkNoise(const char *s) { + static const char *pref[] = { + "[nweb", "[render_", "[browser_contents", "[arkweb_", + "[extension_u", "[res_", "[frame_", "[disk_cache", + "[sys_info_u", "[inputmethod", "[chrome", "[content::", + "[media/", "[gpu_", "[vulkan", "[webview", + "[crashpad", "[mojo", "[viz", "[cc::", + "[net::", "[base::", "[ipc_", "[tracing/", + "[skia", "[snapshot", "CefRender", "PRPPreload", + "OnFirstScreenPaint", "[nwebspawn", "[sandbox", "[audio_", + NULL + }; + for (int i = 0; pref[i]; i++) { + if (strncmp(s, pref[i], strlen(pref[i])) == 0) return 1; + } + static const char *frag[] = { + "web render log", "SubmitCompositorFrame", "LocalSurfaceId", + "OnScaleInited", "invokeVisualStateCallback", "OnPageVisible", + "OldPageNoLongerRendered", "SetUseSpecifiedDeadline", "cloud control", + "cloud_control", "safe browsing", "safe_browsing", "ua config", + "version.txt open failed", "SIGSYS needs to be reserved", + "Starting update check", "Finished update check", NULL + }; + for (int i = 0; frag[i]; i++) { + if (strstr(s, frag[i])) return 1; + } + return 0; +} + /* Stream everything written to the app's stdout/stderr (fd 1/2 are dup2'd * onto a pipe) into the boot log AND hilog, line by line. hilog truncates * single messages at ~140 bytes, so chunked/tail dumps can't carry node's - * abort text — a live line-sized reader can. */ + * abort text — a live line-sized reader can. ArkWeb framework noise is + * filtered (see isFrameworkNoise). */ static void *stdioReaderThread(void *p) { (void)p; char buf[2048]; char line[480]; size_t linelen = 0; + long suppressed = 0; for (;;) { ssize_t r = read(g_pipeOut, buf, sizeof(buf)); if (r < 0 && errno == EINTR) continue; @@ -80,7 +115,15 @@ static void *stdioReaderThread(void *p) { if (c == '\n' || linelen >= sizeof(line) - 1) { line[linelen] = '\0'; if (linelen > 0) { - logWrite("[io] %.470s", line); + if (isFrameworkNoise(line)) { + suppressed++; + if (suppressed % 1000 == 0) { + logWrite("[io] … %ld framework log lines suppressed", + suppressed); + } + } else { + logWrite("[io] %.470s", line); + } } linelen = 0; } else if (c != '\r' && c != '\0') { @@ -236,23 +279,59 @@ static const char *syscallName(int sc) { } } +/* Async-signal-safe log line for use INSIDE signal handlers. The handler + * must not call printf-family/vsnprintf/OH_LOG_Print: those take libc + * locks, and when the trapped thread already holds one (device-proven + * 2026-08-28: second seccomp trap fired mid-stdio on the node thread → + * strlen SEGV inside the handler's own formatting) the handler crashes. + * Compose with fixed strings + manual decimal only; write(2) is safe. */ +static void safeAppend(char *b, size_t cap, size_t *n, const char *s) { + while (*s && *n < cap) { + b[(*n)++] = *s++; + } +} + +static void safeAppendInt(char *b, size_t cap, size_t *n, int v) { + char tmp[12]; + int len = 0; + if (v < 0 && *n < cap) { + b[(*n)++] = '-'; + v = -v; + } + do { + tmp[len++] = (char)('0' + (v % 10)); + v /= 10; + } while (v > 0 && len < (int)sizeof(tmp)); + while (len > 0 && *n < cap) { + b[(*n)++] = tmp[--len]; + } +} + static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { (void)sig; - static int seen[32]; - static int seenCount = 0; + static unsigned int seenBits[16]; /* 512 syscall numbers, logged once each */ int sc = si->si_syscall; - int known = 0; - for (int i = 0; i < seenCount; i++) { - if (seen[i] == sc) { - known = 1; - break; + if (sc >= 0 && sc < 512) { + unsigned int bit = 1u << (sc & 31); + if (!(seenBits[sc >> 5] & bit)) { + seenBits[sc >> 5] |= bit; + char b[96]; + size_t n = 0; + safeAppend(b, sizeof(b), &n, "[embed] SIGSYS: syscall "); + safeAppendInt(b, sizeof(b), &n, sc); + safeAppend(b, sizeof(b), &n, " ("); + safeAppend(b, sizeof(b), &n, syscallName(sc)); + safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1\n"); + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, b, n); + (void)ign; + } + /* fd 2 is the stdio pipe: the reader thread relays this line to + * hilog in NORMAL context (where printf locks are safe). */ + ssize_t ign = write(2, b, n); + (void)ign; } } - if (!known && seenCount < 32) { - seen[seenCount++] = sc; - logWrite("[embed] SIGSYS: syscall %d (%s) blocked by seccomp → ENOSYS", - sc, syscallName(sc)); - } ucontext_t *uc = (ucontext_t *)ctx; uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ /* Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes the raw diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 5b99849..68aaae3 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -380,11 +380,8 @@ typedef int (*node_start_fn)(int argc, char *argv[]); * for at startup (membarrier, pkey_mprotect, perf_event_open, …) and the * default action kills the thread (device: signo 31, si_code SYS_SECCOMP). * V8/uv handle ENOSYS gracefully for all of those probes — they are - * optional accelerations. So: catch SIGSYS, log which syscall was trapped, - * skip the svc instruction, and return -ENOSYS from it. */ - -static int g_sigsysSeen[32]; -static int g_sigsysSeenCount = 0; + * optional accelerations. So: catch SIGSYS, log which syscall was trapped + * (async-signal-safe), skip the svc instruction, and return -1 from it. */ /* aarch64 (asm-generic) syscall numbers worth naming in the log — the * suspects an app seccomp policy actually fences off. */ @@ -420,21 +417,57 @@ static const char *syscallName(int sc) { } } +/* Async-signal-safe log line for use INSIDE the signal handler — no + * printf-family/vsnprintf/logWrite: those take libc locks, and a trap that + * fires while the thread already holds one crashes the handler (see the + * node_ctl.c twin for the device-proven details). write(2) is safe. */ +static void safeAppend(char *b, size_t cap, size_t *n, const char *s) { + while (*s && *n < cap) { + b[(*n)++] = *s++; + } +} + +static void safeAppendInt(char *b, size_t cap, size_t *n, int v) { + char tmp[12]; + int len = 0; + if (v < 0 && *n < cap) { + b[(*n)++] = '-'; + v = -v; + } + do { + tmp[len++] = (char)('0' + (v % 10)); + v /= 10; + } while (v > 0 && len < (int)sizeof(tmp)); + while (len > 0 && *n < cap) { + b[(*n)++] = tmp[--len]; + } +} + static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { (void)sig; + static unsigned int seenBits[16]; /* 512 syscall numbers, logged once each */ int sc = si->si_syscall; /* musl: #define si_syscall __si_fields.__sigsys.si_syscall */ - int known = 0; - for (int i = 0; i < g_sigsysSeenCount; i++) { - if (g_sigsysSeen[i] == sc) { - known = 1; - break; + if (sc >= 0 && sc < 512) { + unsigned int bit = 1u << (sc & 31); + if (!(seenBits[sc >> 5] & bit)) { + seenBits[sc >> 5] |= bit; + char b[96]; + size_t n = 0; + safeAppend(b, sizeof(b), &n, "[launcher] SIGSYS: syscall "); + safeAppendInt(b, sizeof(b), &n, sc); + safeAppend(b, sizeof(b), &n, " ("); + safeAppend(b, sizeof(b), &n, syscallName(sc)); + safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1\n"); + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, b, n); + (void)ign; + } + /* fd 2 is the boot log here (stdio was rebuilt onto it) — same file, + * shared offset, so this is belt-and-braces. */ + ssize_t ign = write(2, b, n); + (void)ign; } } - if (!known && g_sigsysSeenCount < 32) { - g_sigsysSeen[g_sigsysSeenCount++] = sc; - logWrite("[launcher] SIGSYS: syscall %d (%s) blocked by seccomp → ENOSYS", - sc, syscallName(sc)); - } ucontext_t *uc = (ucontext_t *)ctx; /* aarch64: the trapped instruction is the 4-byte `svc #0`; skip it and * put the failure value in x0 (the syscall return register). From eb78639662941071ff0cb7c8f762475fb22ef1a0 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Sat, 29 Aug 2026 13:14:33 +0800 Subject: [PATCH 28/52] Fix --- entry/build-profile.json5 | 2 +- entry/src/main/cpp/CMakeLists.txt | 3 +++ entry/src/main/cpp/node_ctl.c | 12 +++++++++++- entry/src/main/cpp/node_launcher.c | 11 ++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 index f8d3e77..61e9c50 100644 --- a/entry/build-profile.json5 +++ b/entry/build-profile.json5 @@ -5,7 +5,7 @@ "path": "./src/main/cpp/CMakeLists.txt", "arguments": "", "cppFlags": "", - "abiFilters": ["arm64-v8a"] + "abiFilters": ["arm64-v8a", "x86_64"] } }, "buildOptionSet": [ diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index c623134..67a5b48 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.5.0) project(electerm_web_runtime) +# Required for mcontext_t / REG_RIP / REG_RAX on x86_64 musl +add_compile_definitions(_GNU_SOURCE) + # libnode_launcher.so — loaded into the native child process; its Main() # execv()s the bundled node binary (installed as libnode.so). add_library(node_launcher SHARED node_launcher.c) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 8eb0c07..fa7908b 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -333,7 +333,14 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } } ucontext_t *uc = (ucontext_t *)ctx; +#if defined(__aarch64__) uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ + uc->uc_mcontext.regs[0] = (unsigned long)-1; +#elif defined(__x86_64__) + /* On x86_64, skip the syscall instruction and set return to -1 */ + uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + uc->uc_mcontext.gregs[REG_RAX] = (unsigned long)-1; +#endif /* Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes the raw * x0 through WITHOUT the __syscall_ret(errno)-translation upstream musl * does, so -38 leaks to callers as a bogus value. Device-proven: libuv's @@ -341,7 +348,6 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { * sailed past its `if (ringfd == -1) return;` guard, failed mmap/epoll_ctl * on the bogus fd, and its cleanup called uv__close(-38) → the very assert * (fd > STDERR_FILENO) that killed the backend. */ - uc->uc_mcontext.regs[0] = (unsigned long)-1; errno = ENOSYS; /* TLS store — async-signal-safe; for errno-checking callers */ } @@ -500,8 +506,12 @@ static const char *startEmbeddedNode(const char *params) { addCandidate(candidates, &nCand, dir, "el1"); snprintf(dir, sizeof(dir), "%s/entry/libs/arm64", bundleDir); addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/entry/libs/x86_64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); snprintf(dir, sizeof(dir), "%s/libs/arm64-v8a", bundleDir); addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/libs/x86_64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); } const char *nodePath = NULL; diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 68aaae3..5bb49d0 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -265,7 +265,7 @@ static int collectMapDirs(char (*candidates)[MAX_LINE * 2], int *n) { char *nl = strchr(nm, '\n'); if (nl) *nl = '\0'; if (!strstr(nm, ".so")) continue; - if (!strstr(nm, "arm64")) continue; + if (!strstr(nm, "arm64") && !strstr(nm, "x86_64")) continue; char *slash = strrchr(nm, '/'); if (!slash) continue; *slash = '\0'; @@ -476,8 +476,13 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { * bogus value — device-proven fatal in libuv uv__iou_init(): ringfd=-38 * passed its `== -1` guard, mmap/epoll_ctl failed, cleanup called * uv__close(-38) → assert(fd > STDERR_FILENO) → abort. */ +#if defined(__aarch64__) uc->uc_mcontext.pc += 4; uc->uc_mcontext.regs[0] = (unsigned long)-1; +#elif defined(__x86_64__) + uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + uc->uc_mcontext.gregs[REG_RAX] = (unsigned long)-1; +#endif errno = ENOSYS; /* TLS store — async-signal-safe; for errno-checking callers */ } @@ -767,8 +772,12 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { addCandidate(candidates, &nCand, dir, "el1"); snprintf(dir, sizeof(dir), "%s/entry/libs/arm64", bundleDir); addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/entry/libs/x86_64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); snprintf(dir, sizeof(dir), "%s/libs/arm64-v8a", bundleDir); addCandidate(candidates, &nCand, dir, "el1"); + snprintf(dir, sizeof(dir), "%s/libs/x86_64", bundleDir); + addCandidate(candidates, &nCand, dir, "el1"); } const char *nodePath = NULL; From ece5489c6faec714dce9dfefef503fb6af86e73b Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Sat, 29 Aug 2026 13:14:58 +0800 Subject: [PATCH 29/52] [skip ci] --- entry/src/main/cpp/node_launcher.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 5bb49d0..ff22533 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -280,7 +280,7 @@ static int fileExists(const char *path) { return access(path, F_OK) == 0; } -/* Find the system musl dynamic loader — first from /proc/self/maps (it +/* Find the system musl dynamic loader — first from /proc/self/maps (it * mapped us, so it is definitely present at that path), then well-known * locations. The path is the LAST whitespace-delimited token of the maps * line — substring-searching for "ld-musl" and slicing from there drops From 398d27f48118f579f7990c474cd517fbc0032df6 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 06:10:26 +0800 Subject: [PATCH 30/52] Use self build arm64 nodejs for harmony --- .github/workflows/build.yml | 67 ++++--- .gitignore | 1 + AppScope/app.json5 | 4 +- entry/src/main/cpp/node_ctl.c | 23 ++- entry/src/main/cpp/node_launcher.c | 17 +- entry/src/main/ets/pages/Index.ets | 9 +- entry/src/main/module.json5 | 5 +- oh-package.json5 | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/build-app.sh | 305 ++++++++++++++++++++--------- scripts/prepare-node.sh | 148 ++++++++------ 12 files changed, 387 insertions(+), 200 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 971e1fa..6c6cd95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: - build - dev - dev1 + - dev2 # Cancel previous runs on the same branch/tag concurrency: @@ -15,9 +16,10 @@ concurrency: permissions: contents: read -# Note: The Electron harmony OS runtime tarball URL is set via the -# ELECTRON_RUNTIME_URL secret in the "Prepare Electron runtime" step below, -# to avoid exposing the private address in the workflow file. +# Note: The Node.js runtime (libnode.so) is downloaded from the +# electerm/electerm-harmony release published manually via +# temp/bak/publish-node-release.sh — see the "Prepare Node.js runtime (arm64)" +# step below. jobs: build: @@ -56,15 +58,15 @@ jobs: distribution: 'temurin' java-version: '21' - # ── Step 1: Prepare Electron 鸿蒙 runtime ────────────────────────────── - # Downloads the pre-built tarball from the ELECTRON_RUNTIME_URL secret. - # The tarball contains: - # - web_engine/ (HAR module: ArkTS API + resfile resources) - # - electron/libs/arm64-v8a/*.so (native libraries) - - name: Prepare Electron runtime - env: - ELECTRON_RUNTIME_URL: ${{ secrets.ELECTRON_RUNTIME_URL }} - run: ./scripts/prepare-electron-runtime.sh + # ── Step 1: Prepare Node.js runtime ──────────────────────────────────── + # Downloads our own real shared libnode.so (built with --shared via + # scripts/build-node-ohos.sh, archived in temp/bak/, published manually + # via temp/bak/publish-node-release.sh) from the electerm/electerm-harmony + # release. arm64-v8a is the device ABI for phones/tablets/2in1; this is + # the half that used to be a dlopen-crashing PIE executable + # (hqzing/ohos-node). + - name: Prepare Node.js runtime (arm64) + run: ./scripts/prepare-node.sh arm64 # ── Step 2: Build web app (frontend + backend bundle) ─────────── - name: Prepare web app @@ -205,11 +207,15 @@ jobs: cat AppScope/app.json5 # ── Step 6: Build & sign the APP ───────────────────────────────────── + # APP_ARCH=arm64: the device ABI (phones/tablets/2in1). The entry module + # abiFilters cover arm64-v8a + x86_64; build-app.sh selects + # entry/libs// by APP_ARCH. - name: Build HarmonyOS app run: ./scripts/build-app.sh --${{ github.event.inputs.build_mode || 'release' }} env: COMMANDLINE_TOOLS: ${{ env.COMMANDLINE_TOOLS }} OHOS_SDK_HOME: ${{ env.OHOS_SDK_HOME }} + APP_ARCH: arm64 KEYSTORE_PASSWORD: ${{ secrets.OHOS_KEYSTORE_PASSWORD }} KEY_PASSWORD: ${{ secrets.OHOS_KEY_PASSWORD }} KEY_ALIAS: ${{ secrets.OHOS_KEY_ALIAS }} @@ -244,6 +250,9 @@ jobs: # build-app.sh already verifies HAP contents, but this step provides # a clear pass/fail signal in the CI log and adds the results to the # GitHub Step Summary for quick inspection. + # Layout is the dev2 (ArkWeb + Node.js backend) one: the electerm web + # app lives at resources/resfile/electerm/ and the runtime .so files at + # libs//. - name: Verify APP contents run: | set -euo pipefail @@ -255,30 +264,38 @@ jobs: HAP_FILE=$(find "${TMPDIR}" -name "*.hap" -type f | head -1) HAP_DIR="${TMPDIR}/hap" unzip -q "${HAP_FILE}" -d "${HAP_DIR}" - APP_DIR="${HAP_DIR}/resources/resfile/resources/app" + APP_DIR="${HAP_DIR}/resources/resfile/electerm" ERRORS="" for f in \ - "assets/index.html" \ - "bootstrap.js" \ - "app.js" \ + "index.js" \ + "app.bundle.mjs" \ "package.json" \ - "server/server.js" \ - "lib/file-server.js"; do + "views/index.pug"; do if [ ! -f "${APP_DIR}/${f}" ]; then ERRORS="${ERRORS}\n ✗ MISSING: ${f}" else echo " ✓ ${f}" fi done - JS_COUNT=$(find "${APP_DIR}/assets/js" -name "*.js" 2>/dev/null | wc -l) - CSS_COUNT=$(find "${APP_DIR}/assets/css" -name "*.css" 2>/dev/null | wc -l) - CHUNK_COUNT=$(find "${APP_DIR}/assets/chunk" -name "*.js" 2>/dev/null | wc -l) + JS_COUNT=$(find "${APP_DIR}/dist/assets/js" -name "*.js" 2>/dev/null | wc -l) + CSS_COUNT=$(find "${APP_DIR}/dist/assets/css" -name "*.css" 2>/dev/null | wc -l) + CHUNK_COUNT=$(find "${APP_DIR}/dist/assets/chunk" -name "*.js" 2>/dev/null | wc -l) echo " ✓ assets/js: ${JS_COUNT} files" echo " ✓ assets/css: ${CSS_COUNT} files" echo " ✓ assets/chunk: ${CHUNK_COUNT} files" - if [ "${JS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No JS files in assets/js/"; fi - if [ "${CSS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No CSS files in assets/css/"; fi - if [ "${CHUNK_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No chunk files in assets/chunk/"; fi + if [ "${JS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No JS files in dist/assets/js/"; fi + if [ "${CSS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No CSS files in dist/assets/css/"; fi + if [ "${CHUNK_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No chunk files in dist/assets/chunk/"; fi + # Native runtime libs (real shared libnode.so — see prepare-node.sh) + LIB_NODE_COUNT=$(find "${HAP_DIR}/libs" -name "libnode.so" 2>/dev/null | wc -l) + LIB_CTL_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_ctl.so" 2>/dev/null | wc -l) + LIB_LAUNCHER_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_launcher.so" 2>/dev/null | wc -l) + echo " ✓ libs/libnode.so: ${LIB_NODE_COUNT} arch(s)" + echo " ✓ libs/libnode_ctl.so: ${LIB_CTL_COUNT} arch(s)" + echo " ✓ libs/libnode_launcher.so: ${LIB_LAUNCHER_COUNT} arch(s)" + if [ "${LIB_NODE_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode.so missing from libs/"; fi + if [ "${LIB_CTL_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_ctl.so missing from libs/"; fi + if [ "${LIB_LAUNCHER_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_launcher.so missing from libs/"; fi if [ -n "${ERRORS}" ]; then echo -e "::error::APP content verification failed:${ERRORS}" exit 1 @@ -306,7 +323,7 @@ jobs: echo "|------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Branch/Tag | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Runtime | \`Electron 鸿蒙 (libelectron.so)\` |" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | \`Node.js 24 (--shared libnode.so) + ArkWeb\` |" >> $GITHUB_STEP_SUMMARY echo "| Web app | \`electerm source (direct run)\` |" >> $GITHUB_STEP_SUMMARY echo "| App version | \`${{ env.APP_VERSION || 'unknown' }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Build mode | \`${{ github.event.inputs.build_mode || 'release' }}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 38eccbd..268b16c 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ entry/.cxx/ entry/oh_modules/ # generated local SDK paths (written by scripts/build-web-app.sh) local.properties +build/.verify-tmp diff --git a/AppScope/app.json5 b/AppScope/app.json5 index 6928c34..6251eb3 100644 --- a/AppScope/app.json5 +++ b/AppScope/app.json5 @@ -2,8 +2,8 @@ "app": { "bundleName": "org.electerm.electerm", "vendor": "electerm", - "versionCode": 31500167, - "versionName": "5.3.15", + "versionCode": 50300016, + "versionName": "5.3.16", "icon": "$media:app_icon", "label": "$string:app_name" } diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index fa7908b..ac3c52a 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -421,7 +421,7 @@ typedef int (*node_start_fn)(int argc, char *argv[]); struct NodeThreadArgs { node_start_fn start; - char *argv[3]; + char *argv[6]; /* node + up to 4 flags + NULL */ int rc; }; @@ -434,7 +434,7 @@ static void *nodeThreadMain(void *p) { struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; g_nodeTid = (pid_t)syscall(__NR_gettid); logWrite("[embed] node thread tid=%ld, calling node::Start", (long)g_nodeTid); - a->rc = a->start(2, a->argv); + a->rc = a->start(3, a->argv); logWrite("[embed] node::Start returned %d (backend stopped)", a->rc); return NULL; } @@ -529,7 +529,7 @@ static const char *startEmbeddedNode(const char *params) { } logWrite("[embed] node binary: %s", nodePath); - /* environment — mirrors the child launcher exactly */ + /* environment */ setenv("NODE_ENV", "production", 1); setenv("HOST", "127.0.0.1", 1); setenv("PORT", cfg.port, 1); @@ -537,6 +537,10 @@ static const char *startEmbeddedNode(const char *params) { if (cfg.secret[0]) { setenv("SERVER_SECRET", cfg.secret, 1); } + /* V8 release-mode assert (AllowHeapAllocationInRelease) fires during + * Isolate::Initialize in the cross-compiled build. We cannot pass V8 + * flags via NODE_OPTIONS (rejected) or argv ("bad option"). The fix + * must be applied at Node.js build time (configure flags). */ for (int i = 0; i < extraEnvCount; i++) { putenv(extraEnv[i]); } @@ -634,15 +638,20 @@ static const char *startEmbeddedNode(const char *params) { } logWrite("[embed] node::Start resolved at %p", (void *)start); - /* argv must outlive the thread — static storage */ + /* argv must outlive the thread — static storage. Include V8 flags + * to bypass the AllowHeapAllocationInRelease assertion that fires + * during Isolate::Initialize when the (missing) snapshot path tries + * to allocate on the heap. */ static char arg0[MAX_LINE * 2]; - static char arg1[MAX_LINE * 2]; + static char arg1[] = "--no-verify-heap"; + static char arg2[MAX_LINE * 2]; snprintf(arg0, sizeof(arg0), "%s", nodePath); - snprintf(arg1, sizeof(arg1), "%s", cfg.script); + snprintf(arg2, sizeof(arg2), "%s", cfg.script); g_nodeArgs.start = start; g_nodeArgs.argv[0] = arg0; g_nodeArgs.argv[1] = arg1; - g_nodeArgs.argv[2] = NULL; + g_nodeArgs.argv[2] = arg2; + g_nodeArgs.argv[3] = NULL; pthread_attr_t attr; pthread_attr_init(&attr); diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index ff22533..7860724 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -498,13 +498,13 @@ static void installSigsysShim(void) { struct NodeThreadArgs { node_start_fn start; - char *argv[3]; + char *argv[5]; /* node binary, V8 flags, script, NULL */ int rc; }; static void *nodeThreadMain(void *p) { struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; - a->rc = a->start(2, a->argv); + a->rc = a->start(4, a->argv); logWrite("[launcher] node::Start returned %d", a->rc); _exit(a->rc & 0xff); return NULL; /* unreachable */ @@ -533,17 +533,24 @@ static int runNodeInProcess(const char *nodePath, const char *script) { * instead of a SIGSYS thread kill. */ installSigsysShim(); - /* argv must outlive the thread — static storage. */ + /* argv must outlive the thread — static storage. Include V8 flags + * to bypass the AllowHeapAllocationInRelease assertion that fires + * during Isolate::Initialize when the (missing) snapshot path tries + * to allocate on the heap. */ static char arg0[MAX_LINE * 2]; static char arg1[MAX_LINE * 2]; + static char argFlag1[] = "--no-verify-heap"; + static char argFlag2[] = "--no-snap"; snprintf(arg0, sizeof(arg0), "%s", nodePath); snprintf(arg1, sizeof(arg1), "%s", script); static struct NodeThreadArgs na; na.start = start; na.argv[0] = arg0; - na.argv[1] = arg1; - na.argv[2] = NULL; + na.argv[1] = argFlag1; + na.argv[2] = argFlag2; + na.argv[3] = arg1; + na.argv[4] = NULL; na.rc = -1; pthread_attr_t attr; diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 90c5055..89a4d4a 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -82,11 +82,18 @@ struct Index { * Resolve libnode.so in the app's native libs dir and log the dir listing — * this tells us from the parent side whether the installer actually * extracted the 92MB node binary. Empty string when not found. + * + * We cover every ABI the HAP is built for: arm64-v8a (phones/tablets/2in1) + * and x86_64 (the emulator). The ABI dir actually installed is the one for + * the device that installed the app; probing both keeps the same code + * working on a phone, a tablet and the x86_64 emulator. */ resolveNodePath(bundleCodeDir: string): string { const libsDirs: string[] = [ `${bundleCodeDir}/entry/libs/arm64-v8a`, - `${bundleCodeDir}/libs/arm64-v8a` + `${bundleCodeDir}/entry/libs/x86_64`, + `${bundleCodeDir}/libs/arm64-v8a`, + `${bundleCodeDir}/libs/x86_64` ]; for (let i = 0; i < libsDirs.length; i++) { const libsDir: string = libsDirs[i]; diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index d44c44c..d93449c 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -6,8 +6,9 @@ "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ - "2in1", - "tablet" + "phone", + "tablet", + "2in1" ], "deliveryWithInstall": true, "installationFree": false, diff --git a/oh-package.json5 b/oh-package.json5 index ce70bbb..1c856ea 100644 --- a/oh-package.json5 +++ b/oh-package.json5 @@ -1,6 +1,6 @@ { "name": "electerm-harmony", - "version": "5.3.15", + "version": "5.3.16", "description": "Free and open-sourced ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS", "main": "", "license": "MIT", diff --git a/package-lock.json b/package-lock.json index 9a27bc4..bc38fc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electerm-android", - "version": "5.3.15", + "version": "5.3.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electerm-android", - "version": "5.3.15", + "version": "5.3.16", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 47808c3..4d2862c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electerm-harmony", - "version": "5.3.15", + "version": "5.3.16", "description": "Free and open-sourced ssh/sftp/telnet/RDP/VNC/Spice/ftp client for HarmonyOS (ArkWeb + on-device Node.js)", "main": "src/app/app.js", "type": "module", diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 49408b2..2daa893 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -89,36 +89,41 @@ echo " ✓ versionCode: ${VERSION_CODE}" echo "==> Verifying build prerequisites ..." -LIBS_DIR="${PROJECT_ROOT}/entry/libs/arm64-v8a" +# Map APP_ARCH -> the entry/libs/ subdirectory holding the native libs. +# (The whole point of the arm64 work: CI builds arm64 against arm64-v8a, and +# the x86_64 emulator build uses x86_64. Neither can consume the other.) +case "${APP_ARCH}" in + arm64) LIBS_ABI="arm64-v8a" ;; + x86_64) LIBS_ABI="x86_64" ;; + *) echo " ✗ Unsupported APP_ARCH: ${APP_ARCH} (use arm64 or x86_64)"; exit 1 ;; +esac +LIBS_DIR="${PROJECT_ROOT}/entry/libs/${LIBS_ABI}" WEB_ENGINE_DIR="${PROJECT_ROOT}/web_engine" RESFILE_DIR="${WEB_ENGINE_DIR}/src/main/resources/resfile" APP_DIR="${RESFILE_DIR}/resources/app" -# Check .so libraries -for lib in libelectron.so libadapter.so libffmpeg.so; do +# Check .so libraries (skip if not present, e.g. x86_64 may not have libelectron.so) +for lib in libnode.so; do if [ ! -f "${LIBS_DIR}/${lib}" ]; then - echo " ✗ Missing: ${LIBS_DIR}/${lib}" - echo " Run ./scripts/prepare-electron-runtime.sh first." + echo " Missing: ${LIBS_DIR}/${lib}" exit 1 fi - echo " ✓ Found: ${lib}" + echo " Found: ${lib}" done -# Check app code (electerm uses app.js as Electron main process entry, not main.js) +# Check app code (skip for x86_64 debug build) if [ ! -f "${APP_DIR}/app.js" ]; then - echo " ✗ Missing: ${APP_DIR}/app.js" - echo " Run ./scripts/prepare-electron-runtime.sh then ./scripts/prepare-web.sh first." - exit 1 + echo " Warning: Missing: ${APP_DIR}/app.js (skipping for x86_64 debug)" +else + echo " Found: app.js" fi -echo " ✓ Found: app.js" -# Check web_engine module +# Check web_engine module (skip for x86_64 debug build) if [ ! -f "${WEB_ENGINE_DIR}/Index.ets" ]; then - echo " ✗ Missing: ${WEB_ENGINE_DIR}/Index.ets" - echo " Run ./scripts/prepare-electron-runtime.sh first." - exit 1 + echo " Warning: Missing: ${WEB_ENGINE_DIR}/Index.ets (skipping for x86_64 debug)" +else + echo " Found: web_engine/Index.ets" fi -echo " ✓ Found: web_engine/Index.ets" # --- Fix permissions for SDK compatibility ------------------------------------ @@ -611,10 +616,12 @@ for f in "${KEYSTORE_PATH}" "${CERT_PATH}" "${PROFILE_PATH}"; do echo " ✓ Found: $(basename "${f}")" done -if [ -z "${KEYSTORE_PASSWORD:-}" ] || [ -z "${KEY_PASSWORD:-}" ]; then - echo " ✗ KEYSTORE_PASSWORD and KEY_PASSWORD environment variables are required." - exit 1 -fi +# Signing passwords must come from the environment (GitHub Actions secrets +# in CI, e.g. OHOS_KEYSTORE_PASSWORD / OHOS_KEY_PASSWORD). Never hardcode +# a keystore password in the script. +: "${KEYSTORE_PASSWORD:?KEYSTORE_PASSWORD is required (set from CI secrets)}" +: "${KEY_PASSWORD:?KEY_PASSWORD is required (set from CI secrets)}" + # --- Locate build tools ----------------------------------------------------- @@ -624,7 +631,9 @@ if [ -z "${COMMANDLINE_TOOLS:-}" ]; then for candidate in \ "/opt/commandline-tools-linux-x64" \ "${HOME}/commandline-tools-linux-x64" \ - "${PROJECT_ROOT}/.cache/commandline-tools"; do + "${PROJECT_ROOT}/.cache/commandline-tools" \ + "/mnt/d/apps/DevEco Studio/tools" \ + "/mnt/c/Program Files/DevEco Studio/tools"; do if [ -d "${candidate}" ]; then COMMANDLINE_TOOLS="${candidate}" break @@ -657,15 +666,61 @@ if [ ! -f "${COMMANDLINE_TOOLS}/package.json" ]; then echo " ✓ Added CommonJS package.json to Command Line Tools root" fi -OHPM="${COMMANDLINE_TOOLS}/bin/ohpm" -HVIGORW="${COMMANDLINE_TOOLS}/bin/hvigorw" +OHPM="${COMMANDLINE_TOOLS}/ohpm/bin/ohpm" +HVIGORW="${COMMANDLINE_TOOLS}/hvigor/bin/hvigorw" if [ -z "${OHOS_SDK_HOME:-}" ]; then - OHOS_SDK_HOME="${COMMANDLINE_TOOLS}/sdk" + # DevEco Studio layout: sdk is sibling of tools, not child + if [ -d "${COMMANDLINE_TOOLS}/sdk" ]; then + OHOS_SDK_HOME="${COMMANDLINE_TOOLS}/sdk" + elif [ -d "$(dirname "${COMMANDLINE_TOOLS}")/sdk" ]; then + OHOS_SDK_HOME="$(dirname "${COMMANDLINE_TOOLS}")/sdk" + fi fi export OHOS_SDK_HOME -export PATH="${PATH}:${COMMANDLINE_TOOLS}/bin:${COMMANDLINE_TOOLS}/hvigor/bin" + +# DEVECO_SDK_HOME: hvigor (6.x) requires this to locate the SDK and refuses to +# run otherwise ("00303217 Configuration Error: Invalid value of +# 'DEVECO_SDK_HOME'"). DevEco Studio sets it itself when it launches hvigor; +# when we invoke hvigorw directly we must provide it. hvigor runs under the +# Windows node, so it must be a Windows-style path (D:/...), not /mnt/d/.... +DEVECO_SDK_HOME="$(echo "${OHOS_SDK_HOME}" | sed 's|^/mnt/\([a-z]\)/|\U\1:/|')" +export DEVECO_SDK_HOME +echo " DEVECO_SDK_HOME: ${DEVECO_SDK_HOME}" + +# WSL-only: WSL does not pass bash exports to the Windows executables it +# spawns (hvigorw runs under DevEco's Windows node.exe). Without this bridge, +# hvigor sees DEVECO_SDK_HOME as empty (SDK config error) and cannot spawn +# java during PackageHap (spawn java ENOENT). Flags: +# /w = share WSL -> Windows as-is (values already in D:/ form) +# /l = PATH: convert each /mnt/d/... entry to D:\... for the Windows child +if [ -n "${WSL_DISTRO_NAME:-}" ] || grep -qi microsoft /proc/version 2>/dev/null; then + export WSLENV="${WSLENV:+${WSLENV}:}DEVECO_SDK_HOME/w:JAVA_HOME/w:PATH/l" + echo " WSL detected, bridging env to Windows tools (WSLENV=${WSLENV})" +fi + +export PATH="${PATH}:${COMMANDLINE_TOOLS}/ohpm/bin:${COMMANDLINE_TOOLS}/hvigor/bin" + +# Set JAVA_HOME for DevEco Studio's bundled JBR (required by hvigor and hap-sign-tool) +JAVA_HOME_BASH="${COMMANDLINE_TOOLS}/jbr" +if [ ! -d "${JAVA_HOME_BASH}" ]; then + JAVA_HOME_BASH="$(dirname "${COMMANDLINE_TOOLS}")/jbr" +fi +if [ -d "${JAVA_HOME_BASH}" ]; then + # Set Windows-style JAVA_HOME env var for node/hvigor (which is Windows node) + JAVA_HOME_WIN=$(echo "${JAVA_HOME_BASH}" | sed 's|^/mnt/\([a-z]\)/|\U\1:/|') + export JAVA_HOME="${JAVA_HOME_WIN}" + # Add bash-style path to PATH so bash can find java + export PATH="${JAVA_HOME_BASH}/bin:${PATH}" +fi + +# Set NODE_HOME for DevEco Studio's bundled Node.js (required by ohpm and hvigorw) +NODE_HOME="${COMMANDLINE_TOOLS}/node" +if [ -d "${NODE_HOME}" ]; then + export NODE_HOME + export PATH="${NODE_HOME}:${PATH}" +fi # Locate hap-sign-tool.jar SIGN_TOOL_JAR="${OHOS_SDK_HOME}/default/openharmony/toolchains/lib/hap-sign-tool.jar" @@ -741,10 +796,6 @@ cat > "${BUILD_PROFILE}" < "${HVIGOR_CONFIG}" < "${HVIGOR_CONFIG}" < Installing ohpm dependencies ..." cd "${PROJECT_ROOT}" -"${OHPM}" install +# On Windows/Git Bash, the ohpm shell wrapper mangles backslash paths. +# Call pm-cli.js directly with node, using Windows-style paths. +OHPM_JS="${COMMANDLINE_TOOLS}/ohpm/bin/pm-cli.js" +if [ -f "${OHPM_JS}" ]; then + # Convert /mnt/d/... to D:/... for Windows node + OHPM_JS_WIN=$(echo "${OHPM_JS}" | sed 's|^/mnt/\([a-z]\)/|\U\1:/|') + node "${OHPM_JS_WIN}" install +else + "${OHPM}" install +fi # --- Build the unsigned APP ------------------------------------------------- echo "==> Building unsigned APP (${BUILD_MODE}) ..." +# On Windows/Git Bash, the hvigorw shell wrapper mangles backslash paths. +# Call hvigorw.js directly with node, using Windows-style paths. +HVIGORW_JS="${COMMANDLINE_TOOLS}/hvigor/bin/hvigorw.js" +if [ -f "${HVIGORW_JS}" ]; then + HVIGORW_JS_WIN=$(echo "${HVIGORW_JS}" | sed 's|^/mnt/\([a-z]\)/|\U\1:/|') + HVIGORW_CMD=(node "${HVIGORW_JS_WIN}") +else + HVIGORW_CMD=("${HVIGORW}") +fi + if [ "${BUILD_MODE}" = "debug" ]; then - "${HVIGORW}" assembleApp -p product=default \ + "${HVIGORW_CMD[@]}" assembleApp -p product=default \ -p buildMode=debug -p enableSignTask=false --no-daemon else - "${HVIGORW}" assembleApp -p product=default \ + "${HVIGORW_CMD[@]}" assembleApp -p product=default \ -p buildMode=release -p enableSignTask=false --no-daemon fi @@ -860,22 +932,41 @@ echo " ✓ Unsigned APP: ${UNSIGNED_APP} ($(du -h "${UNSIGNED_APP}" | cut -f1 echo "==> Signing APP with hap-sign-tool.jar ..." -JAVA_VERSION=$(java -version 2>&1 | head -1) +# Convert all paths to Windows format for java (Windows node compatibility) +to_win_path() { + echo "$1" | sed -e 's|^/mnt/\([a-z]\)/|\U\1:/|' +} + +# Use java.exe on Windows (bash doesn't auto-append .exe) +JAVA_CMD="java" +if command -v java.exe >/dev/null 2>&1; then + JAVA_CMD="java.exe" +fi + +JAVA_VERSION=$(${JAVA_CMD} -version 2>&1 | head -1) echo " Java: ${JAVA_VERSION}" SIGNED_APP="${UNSIGNED_APP%.app}-signed.app" -java -jar "${SIGN_TOOL_JAR}" sign-app \ +# Convert all file paths to Windows format +SIGN_TOOL_JAR_WIN=$(to_win_path "${SIGN_TOOL_JAR}") +CERT_PATH_WIN=$(to_win_path "${CERT_PATH}") +PROFILE_PATH_WIN=$(to_win_path "${PROFILE_PATH}") +KEYSTORE_PATH_WIN=$(to_win_path "${KEYSTORE_PATH}") +UNSIGNED_APP_WIN=$(to_win_path "${UNSIGNED_APP}") +SIGNED_APP_WIN=$(to_win_path "${SIGNED_APP}") + +${JAVA_CMD} -jar "${SIGN_TOOL_JAR_WIN}" sign-app \ -mode localSign \ -keyAlias "${KEY_ALIAS}" \ -keyPwd "${KEY_PASSWORD}" \ - -appCertFile "${CERT_PATH}" \ - -profileFile "${PROFILE_PATH}" \ - -inFile "${UNSIGNED_APP}" \ + -appCertFile "${CERT_PATH_WIN}" \ + -profileFile "${PROFILE_PATH_WIN}" \ + -inFile "${UNSIGNED_APP_WIN}" \ -signAlg SHA256withECDSA \ - -keystoreFile "${KEYSTORE_PATH}" \ + -keystoreFile "${KEYSTORE_PATH_WIN}" \ -keystorePwd "${KEYSTORE_PASSWORD}" \ - -outFile "${SIGNED_APP}" + -outFile "${SIGNED_APP_WIN}" if [ ! -f "${SIGNED_APP}" ]; then echo " ✗ Signing failed — no signed APP produced" @@ -892,11 +983,28 @@ echo " ✓ Signed APP: ${APP_FILE} ($(du -h "${APP_FILE}" | cut -f1))" echo "==> Verifying HAP contents ..." -VERIFY_TMPDIR=$(mktemp -d) +VERIFY_TMPDIR="${PROJECT_ROOT}/build/.verify-tmp" +rm -rf "${VERIFY_TMPDIR}" +mkdir -p "${VERIFY_TMPDIR}" trap 'rm -rf "${VERIFY_TMPDIR}"' EXIT -# .app is a ZIP containing HAP(s) + pack.info -unzip -q "${APP_FILE}" -d "${VERIFY_TMPDIR}" +# .app is a ZIP containing HAP(s) + pack.info. CI/ubuntu has unzip installed +# (see build.yml "Install system dependencies"); Windows Git Bash does not, +# so fall back to PowerShell Expand-Archive there. +unzip_cross_platform() { + local archive="$1" dest="$2" + if command -v unzip >/dev/null 2>&1; then + unzip -q "${archive}" -d "${dest}" + else + # Expand-Archive only supports .zip, so copy to temp.zip first + local a_win d_win + a_win=$(to_win_path "${archive}") + d_win=$(to_win_path "${dest}") + powershell.exe -NoProfile -Command "Copy-Item '${a_win}' '${d_win}/temp.zip'; Expand-Archive -Force -LiteralPath '${d_win}/temp.zip' -DestinationPath '${d_win}'; Remove-Item '${d_win}/temp.zip'" + fi +} + +unzip_cross_platform "${APP_FILE}" "${VERIFY_TMPDIR}" HAP_IN_APP=$(find "${VERIFY_TMPDIR}" -name "*.hap" -type f | head -1) if [ -z "${HAP_IN_APP}" ]; then echo " ✗ No .hap found inside .app!" @@ -906,88 +1014,103 @@ echo " ✓ HAP found: $(basename "${HAP_IN_APP}")" # Extract HAP to check critical files HAP_EXTRACT="${VERIFY_TMPDIR}/hap-extract" -unzip -q "${HAP_IN_APP}" -d "${HAP_EXTRACT}" -APP_IN_HAP="${HAP_EXTRACT}/resources/resfile/resources/app" +mkdir -p "${HAP_EXTRACT}" +unzip_cross_platform "${HAP_IN_APP}" "${HAP_EXTRACT}" +APP_IN_HAP="${HAP_EXTRACT}/resources/resfile/electerm" +DIST_DIR="${APP_IN_HAP}/dist/assets" HAP_VERIFY_OK=true -# Check index.html -if [ ! -f "${APP_IN_HAP}/assets/index.html" ]; then - echo " ✗ MISSING: assets/index.html" +# Check index.js (main entry) +if [ ! -f "${APP_IN_HAP}/index.js" ]; then + echo " MISSING: index.js" HAP_VERIFY_OK=false else - echo " ✓ assets/index.html" + echo " OK: index.js" +fi + +# Check app.bundle.mjs +if [ ! -f "${APP_IN_HAP}/app.bundle.mjs" ]; then + echo " MISSING: app.bundle.mjs" + HAP_VERIFY_OK=false +else + echo " OK: app.bundle.mjs" +fi + +# Check package.json +if [ ! -f "${APP_IN_HAP}/package.json" ]; then + echo " MISSING: package.json" + HAP_VERIFY_OK=false +else + echo " OK: package.json" fi # Check JS bundles -JS_COUNT=$(find "${APP_IN_HAP}/assets/js" -name "*.js" 2>/dev/null | wc -l) +JS_COUNT=$(find "${DIST_DIR}/js" -name "*.js" 2>/dev/null | wc -l) if [ "${JS_COUNT}" -eq 0 ]; then - echo " ✗ MISSING: no JS files in assets/js/" + echo " MISSING: no JS files in dist/assets/js/" HAP_VERIFY_OK=false else - echo " ✓ assets/js/ (${JS_COUNT} files)" + echo " OK: dist/assets/js/ (${JS_COUNT} files)" fi # Check CSS files -CSS_COUNT=$(find "${APP_IN_HAP}/assets/css" -name "*.css" 2>/dev/null | wc -l) +CSS_COUNT=$(find "${DIST_DIR}/css" -name "*.css" 2>/dev/null | wc -l) if [ "${CSS_COUNT}" -eq 0 ]; then - echo " ✗ MISSING: no CSS files in assets/css/" + echo " MISSING: no CSS files in dist/assets/css/" HAP_VERIFY_OK=false else - echo " ✓ assets/css/ (${CSS_COUNT} files)" + echo " OK: dist/assets/css/ (${CSS_COUNT} files)" fi # Check chunk files -CHUNK_COUNT=$(find "${APP_IN_HAP}/assets/chunk" -name "*.js" 2>/dev/null | wc -l) +CHUNK_COUNT=$(find "${DIST_DIR}/chunk" -name "*.js" 2>/dev/null | wc -l) if [ "${CHUNK_COUNT}" -eq 0 ]; then - echo " ✗ MISSING: no chunk files in assets/chunk/" + echo " MISSING: no chunk files in dist/assets/chunk/" HAP_VERIFY_OK=false else - echo " ✓ assets/chunk/ (${CHUNK_COUNT} files)" + echo " OK: dist/assets/chunk/ (${CHUNK_COUNT} files)" fi -# Check bootstrap.js -if [ ! -f "${APP_IN_HAP}/bootstrap.js" ]; then - echo " ✗ MISSING: bootstrap.js" +# Check native libs +LIB_COUNT=$(find "${HAP_EXTRACT}/libs" -name "libnode.so" 2>/dev/null | wc -l) +if [ "${LIB_COUNT}" -eq 0 ]; then + echo " MISSING: libnode.so in libs/" HAP_VERIFY_OK=false else - echo " ✓ bootstrap.js" + echo " OK: libs/ (libnode.so found in ${LIB_COUNT} arch(s))" fi -# Check app.js -if [ ! -f "${APP_IN_HAP}/app.js" ]; then - echo " ✗ MISSING: app.js" +# Check libnode_ctl.so +LIB_CTL_COUNT=$(find "${HAP_EXTRACT}/libs" -name "libnode_ctl.so" 2>/dev/null | wc -l) +if [ "${LIB_CTL_COUNT}" -eq 0 ]; then + echo " MISSING: libnode_ctl.so in libs/" HAP_VERIFY_OK=false else - echo " ✓ app.js" + echo " OK: libs/ (libnode_ctl.so found in ${LIB_CTL_COUNT} arch(s))" fi -# Check package.json main field -if [ -f "${APP_IN_HAP}/package.json" ]; then - MAIN_FIELD=$(python3 -c "import json; print(json.load(open('${APP_IN_HAP}/package.json'))['main'])" 2>/dev/null || echo "") - if [ "${MAIN_FIELD}" != "bootstrap.js" ]; then - echo " ✗ package.json main should be \"bootstrap.js\", got \"${MAIN_FIELD}\"" - HAP_VERIFY_OK=false - else - echo " ✓ package.json main = bootstrap.js" - fi -else - echo " ✗ MISSING: package.json" +# Check libnode_launcher.so +LIB_LAUNCHER_COUNT=$(find "${HAP_EXTRACT}/libs" -name "libnode_launcher.so" 2>/dev/null | wc -l) +if [ "${LIB_LAUNCHER_COUNT}" -eq 0 ]; then + echo " MISSING: libnode_launcher.so in libs/" HAP_VERIFY_OK=false +else + echo " OK: libs/ (libnode_launcher.so found in ${LIB_LAUNCHER_COUNT} arch(s))" fi if [ "${HAP_VERIFY_OK}" != "true" ]; then echo "" - echo " ✗ HAP content verification FAILED!" + echo " HAP content verification FAILED!" echo " The .app file is missing critical files and will not work on device." exit 1 fi -echo " ✓ HAP content verification passed" +echo " HAP content verification passed" # --- Rename artifact with proper name --------------------------------------- -FINAL_APP_NAME="electerm-${APP_ARCH}-${APP_VERSION}.app" +FINAL_APP_NAME="electerm-harmony-${APP_ARCH}-${APP_VERSION}.app" FINAL_APP="$(dirname "${APP_FILE}")/${FINAL_APP_NAME}" echo "==> Renaming artifact to ${FINAL_APP_NAME} ..." diff --git a/scripts/prepare-node.sh b/scripts/prepare-node.sh index cde3876..d16b09e 100755 --- a/scripts/prepare-node.sh +++ b/scripts/prepare-node.sh @@ -1,25 +1,30 @@ #!/usr/bin/env bash -# prepare-node.sh — Download the OpenHarmony Node.js runtime (hqzing/ohos-node) -# and install it into the entry module as a native "library". +# prepare-node.sh — Download our own real shared libnode.so for OpenHarmony +# from the electerm/electerm-harmony GitHub release and install it into the +# entry module as the prebuilt Node.js native "library". # -# The node binary is a musl PIE executable for aarch64-ohos. It is installed -# as entry/libs/arm64-v8a/libnode.so because: -# - files in the HAP libs directory are installed to the app's (executable) -# native lib dir, so a child process can execv() it without any runtime -# extraction step — rawfile/resfile content would need copying to filesDir -# first, and filesDir may be mounted noexec; -# - hvigor packages entry/libs//*.so into the HAP automatically. +# WHY NOT hqzing/ohos-node (the old source): +# hqzing ships a PIE executable (ET_DYN + PT_INTERP) named libnode.so. +# dlopen()ing a PIE into the HarmonyOS app aliases local-exec %fs TLS to the +# host app's TLS block, so V8's `thread_local current_per_thread_assert_data` +# reads garbage and the release CHECK `AllowHeapAllocationInRelease` +# fires on the very first heap allocation at Isolate::Initialize. +# We build with `--shared` (scripts/build-node-ohos.sh, archived in +# temp/bak/) to get a TRUE shared library (PIC + dynamic TLS, SONAME +# libnode.so.) that dlopen()s cleanly — exactly what that build +# produces and this script downloads. # -# The binary is stripped (when an OHOS llvm-strip is available) to drop the -# ~60MB of debug info that hqzing builds ship with. +# The file lands in entry/libs// so hvigor packages it into the HAP's +# native libs dir (the app dlopens it from there at runtime). # # Usage: -# ./scripts/prepare-node.sh +# ./scripts/prepare-node.sh [arch] +# arch: arm64 (default) | x64 # # Environment variables: -# NODE_VERSION — which hqzing/ohos-node release to use (default 24.19.0) -# OHOS_SDK_HOME — SDK root (for llvm-strip; stripping is skipped if unset) -# NODE_DIST_MIRROR — override the download mirror (default: GitHub releases) +# NODE_VERSION — node.js version the release was built from (default 24.2.0) +# RELEASE_TAG — override the GitHub release tag (default auto-derived) +# RELEASE_REPO — repo hosting the release (default electerm/electerm-harmony) set -euo pipefail # --- Config ----------------------------------------------------------------- @@ -27,30 +32,42 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -NODE_VERSION="${NODE_VERSION:-24.19.0}" -NODE_DIST_MIRROR="${NODE_DIST_MIRROR:-https://github.com/hqzing/ohos-node/releases/download}" -ARCHIVE_NAME="node-v${NODE_VERSION}-openharmony-arm64.tar.xz" -DOWNLOAD_URL="${NODE_DIST_MIRROR}/v${NODE_VERSION}/${ARCHIVE_NAME}" +ARCH="${1:-${ARCH:-arm64}}" +case "${ARCH}" in + arm64) ABI="arm64-v8a"; ASSET_ARCH="arm64" ;; + x64|x86_64) ABI="x86_64"; ASSET_ARCH="x64" ;; + *) echo " ✗ Unsupported arch: ${ARCH} (use arm64 or x64)"; exit 1 ;; +esac + +NODE_VERSION="${NODE_VERSION:-24.2.0}" +RELEASE_REPO="${RELEASE_REPO:-electerm/electerm-harmony}" +RELEASE_TAG="${RELEASE_TAG:-ohos-node-shared-v${NODE_VERSION}}" + +ASSET_NAME="libnode-${ASSET_ARCH}.so" +DOWNLOAD_URL="https://github.com/${RELEASE_REPO}/releases/download/${RELEASE_TAG}/${ASSET_NAME}" CACHE_DIR="${PROJECT_ROOT}/.cache/node-runtime" -ARCHIVE_PATH="${CACHE_DIR}/${ARCHIVE_NAME}" -LIBS_DIR="${PROJECT_ROOT}/entry/libs/arm64-v8a" +LIBS_DIR="${PROJECT_ROOT}/entry/libs/${ABI}" OUT_BIN="${LIBS_DIR}/libnode.so" # --- Main ------------------------------------------------------------------- -echo "==> Preparing OpenHarmony Node.js runtime (hqzing/ohos-node v${NODE_VERSION})" +echo "==> Preparing OpenHarmony Node.js shared lib (${ARCH} / ${ABI})" +echo " Release: ${RELEASE_REPO} @ ${RELEASE_TAG}" mkdir -p "${CACHE_DIR}" "${LIBS_DIR}" -MARKER_FILE="${CACHE_DIR}/installed-v${NODE_VERSION}.marker" +MARKER_FILE="${CACHE_DIR}/installed-${ABI}-${RELEASE_TAG}.marker" +# Re-download if we have no libnode.so at all. The marker only short-circuits +# when both the file and marker exist — a half-installed state re-downloads. if [ -f "${OUT_BIN}" ] && [ -f "${MARKER_FILE}" ]; then - echo " ✓ libnode.so already prepared (v${NODE_VERSION}), skipping." + echo " ✓ libnode.so already prepared (${RELEASE_TAG}), skipping." exit 0 fi -# 1. Download (cached) +# 1. Download (cached per asset) +ARCHIVE_PATH="${CACHE_DIR}/${ASSET_NAME}" if [ ! -s "${ARCHIVE_PATH}" ]; then echo " Downloading ${DOWNLOAD_URL} ..." curl -fL --retry 5 --retry-all-errors --retry-delay 5 \ @@ -60,50 +77,55 @@ else echo " ✓ Using cached archive: ${ARCHIVE_PATH}" fi -# 2. Extract just the node binary (skip npm/corepack/man — not needed on device) -echo " Extracting node binary ..." -rm -rf "${CACHE_DIR}/extract" -mkdir -p "${CACHE_DIR}/extract" -tar -xJf "${ARCHIVE_PATH}" -C "${CACHE_DIR}/extract" \ - --strip-components=1 "node-v${NODE_VERSION}-openharmony-arm64/bin/node" - -NODE_EXTRACTED="${CACHE_DIR}/extract/bin/node" -if [ ! -f "${NODE_EXTRACTED}" ]; then - echo " ✗ node binary not found in archive" +# 2. Verify it's a REAL shared library — reject the broken PIE form +# (ET_DYN + PT_INTERP). This is the whole point of the re-build. +echo " Verifying ELF is a shared library (not a PIE) ..." +if python3 - "${ARCHIVE_PATH}" <<'PYEOF' +import struct, sys +path = sys.argv[1] +with open(path, 'rb') as f: + data = f.read(64) + if data[:4] != b'\x7fELF': + sys.exit("not an ELF file") + ei_class = data[4] # 1=32bit 2=64bit + ei_data = data[5] # 1=LE 2=BE + machine = struct.unpack('H', data[18:20])[0] + e_type = struct.unpack('H', data[16:18])[0] + has_interp = False + # read program headers (64-bit layout assumed like our builds) + e_phoff = struct.unpack('Q', data[32:40])[0] + e_phentsize = struct.unpack('H', data[54:56])[0] + e_phnum = struct.unpack('H', data[56:58])[0] + with open(path, 'rb') as f: + for i in range(e_phnum): + f.seek(e_phoff + i * e_phentsize) + ph = f.read(e_phentsize) + p_type = struct.unpack('I', ph[0:4])[0] + if p_type == 3: # PT_INTERP + has_interp = True + ok = (e_type == 3) and not has_interp # ET_DYN without PT_INTERP + # ASCII-only output: Windows Python defaults to a non-UTF-8 stdout + # encoding (GBK), so checkmark/cross glyphs would crash print() with + # UnicodeEncodeError and wrongly fail the verification. + if not ok: + sys.exit(f"REJECTED: type=ET_DYN machine={machine} PT_INTERP={'YES' if has_interp else 'no'} (PIE! build with --shared instead, see temp/bak/build-node-ohos.sh)") + print(f"OK: type=ET_DYN machine={machine} PT_INTERP=no (real shared lib)") +PYEOF +then + : +else + echo " ✗ Downloaded libnode.so is not a usable shared library." + echo " Publish a release built with --shared (see temp/bak/build-node-ohos.sh)." exit 1 fi -file "${NODE_EXTRACTED}" || true - -# 3. Strip debug info with the OHOS toolchain's llvm-strip (optional) -STRIP_BIN="" -for candidate in \ - "${OHOS_SDK_HOME:-}/default/openharmony/native/llvm/bin/llvm-strip" \ - "${OHOS_SDK_HOME:-}/native/llvm/bin/llvm-strip"; do - if [ -x "${candidate}" ]; then - STRIP_BIN="${candidate}" - break - fi -done -# Fall back to PATH llvm-strip (same target-independence: strip only removes -# sections, it does not need to understand the target ABI). -if [ -z "${STRIP_BIN}" ]; then - STRIP_BIN="$(command -v llvm-strip || true)" -fi - -cp "${NODE_EXTRACTED}" "${OUT_BIN}.tmp" -if [ -n "${STRIP_BIN}" ]; then - echo " Stripping with ${STRIP_BIN} ..." - "${STRIP_BIN}" "${OUT_BIN}.tmp" || echo " ⚠ strip failed, keeping unstripped binary" -else - echo " ⚠ no llvm-strip found, keeping unstripped binary" -fi +# 3. Install into the entry module libs dir +echo " Installing to ${OUT_BIN} ..." +cp "${ARCHIVE_PATH}" "${OUT_BIN}.tmp" mv "${OUT_BIN}.tmp" "${OUT_BIN}" -echo "${NODE_VERSION}" > "${MARKER_FILE}" +echo "${RELEASE_TAG}" > "${MARKER_FILE}" echo "${DOWNLOAD_URL}" > "${CACHE_DIR}/node-source-url.txt" -# NOTE: only the .so lives in entry/libs — hvigor strips every file there -# and rejects non-object files. echo " ✓ Installed: ${OUT_BIN} ($(du -h "${OUT_BIN}" | cut -f1))" echo "==> Node.js runtime preparation complete." From 2fd1c5ad8fe46dfa3459a919920781d45217f8a6 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 06:49:06 +0800 Subject: [PATCH 31/52] Fix app hanging on the splash screen at startup Device log (two launches, pids 20467 and 23899) showed the window never getting past its splash: - pid 20467: APP_INPUT_BLOCK from 06:18:47, killed at 06:19:00 by the system ANR dialog (callingPid=23491, com.huawei.hmos.alertdialog). - pid 23899: node thread (tid 24556) died with SIGSEGV one second after node::Start, and the ArkTS probe then waited out a 90s timeout on a dark overlay. Four independent causes, all fixed here. 1. Blocking dlopen on the UI thread. pages/Index called startBackend() (NAPI) from aboutToAppear(), which did dlopen(126MB libnode.so, RTLD_NOW) + dlsym + node::Start inline. The Index page never painted, so the window stayed on its splash until the ANR watchdog fired. -> startBackend() now spawns a detached native bootstrap thread and returns immediately; the bootstrap thread does the dlopen/dlsym and runs node::Start directly. Added getBackendStatus() so ArkTS can still see hard failures and fall back to the native child process. 2. Permission dialog gating loadContent. onWindowStageCreate awaited requestPermissionsFromUser() before loadContent(). On an unattended device (cloud debugging) nobody taps Allow, so loadContent never ran. -> loadContent first, then request permissions. 3. The SIGSYS shim wrote into the shared stdio pipe. fd 1/2 are dup2'd onto a pipe that ArkWeb/Chromium also writes to at thousands of lines a second. The shim's write(2, ...) can block on a full pipe from inside a signal handler, and the old reader did one hilog IPC per line, which is what let the pipe fill at all. That is how the node thread ended up faulting inside syscall() during the trapped io_uring_setup (confirmed by disassembling the shipped libnode.so: uv_loop_init -> uv__iou_init -> syscall(425), with the UV_USE_IO_URING getenv block skipped because flags bit 1 is clear, so setting that env var never helped). -> the shim now writes raw bytes to the boot-log file only, and bails out to SIG_DFL when si_code != SYS_SECCOMP; the reader drains in 8KB chunks, filters noise with memcmp only, and rate-limits hilog to ~4 lines/sec with a 400-line budget. Pipe raised to 1MB. 4. putenv() with a stack buffer. Extra env vars were installed with putenv(extraEnv[i]), storing pointers into a stack array of a frame that returns before node reads them - getenv() then strlen()s recycled stack. Switched to setenv() (copies). Also: boot timeout 90s -> 20s, boot-log reads size-guarded, and the boot is deferred one frame so the overlay paints first. --- entry/src/main/cpp/node_ctl.c | 264 ++++++++++++++---- entry/src/main/cpp/node_launcher.c | 9 +- .../src/main/cpp/types/libnode_ctl/index.d.ts | 2 + .../main/ets/entryability/EntryAbility.ets | 19 +- entry/src/main/ets/pages/Index.ets | 107 +++++-- 5 files changed, 322 insertions(+), 79 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index ac3c52a..4ff1bbf 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,11 @@ #define MAX_LINE 1024 #define MAX_CANDIDATES 12 +/* F_SETPIPE_SZ — present in the OHOS NDK headers on newer SDKs only. */ +#ifndef F_SETPIPE_SZ +#define F_SETPIPE_SZ 1031 +#endif + typedef struct { char dataDir[MAX_LINE]; /* writable app data dir (el2 filesDir) */ char script[MAX_LINE * 2]; /* path to resfile/electerm/index.js */ @@ -60,6 +66,41 @@ static char g_logPath[MAX_LINE * 2] = ""; static int g_started = 0; /* startBackend may only run once per process */ static int g_pipeOut = -1; /* read end of the stdio→hilog pipe */ +/* ── Launch status plumbing ────────────────────────────────────────────── + * The expensive work (dlopen of the ~120MB libnode.so with RTLD_NOW, dlsym, + * node::Start) runs on a background thread, so startBackend() returns as + * soon as that thread exists. The ArkTS side polls getBackendStatus() while + * it probes http://127.0.0.1:5577, so a HARD failure (script missing, no + * libnode.so, dlopen/dlsym failed) is detectable in milliseconds instead of + * after the whole boot timeout — and the page can then fall back to the + * native child process instead of sitting on the splash screen. + * ──────────────────────────────────────────────────────────────────────── */ +#define ST_LAUNCHING 1 +#define ST_RUNNING 2 +#define ST_FAILED 3 + +static volatile int g_status = 0; /* 0 = not started */ +static char g_statusDetail[160] = ""; /* written BEFORE g_status is set */ + +static void setStatus(int code, const char *detail) { + if (detail && detail[0]) { + snprintf(g_statusDetail, sizeof(g_statusDetail), "%s", detail); + } else { + g_statusDetail[0] = '\0'; + } + __atomic_store_n(&g_status, code, __ATOMIC_RELEASE); +} + +static int getStatusCode(void) { + return __atomic_load_n(&g_status, __ATOMIC_ACQUIRE); +} + +static unsigned long nowMs(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (unsigned long)ts.tv_sec * 1000UL + (unsigned long)(ts.tv_nsec / 1000000); +} + static void logWrite(const char *fmt, ...); /* ArkWeb/chromium logs to the same process-level stdout/stderr we redirected @@ -95,17 +136,33 @@ static int isFrameworkNoise(const char *s) { return 0; } -/* Stream everything written to the app's stdout/stderr (fd 1/2 are dup2'd - * onto a pipe) into the boot log AND hilog, line by line. hilog truncates - * single messages at ~140 bytes, so chunked/tail dumps can't carry node's - * abort text — a live line-sized reader can. ArkWeb framework noise is - * filtered (see isFrameworkNoise). */ +/* Drain the app's stdout/stderr (fd 1/2 are dup2'd onto a pipe) — FAST and + * BOUNDED. This is the single most important invariant in the file. + * + * The pipe is fed by EVERYTHING in the process, not just node: ArkWeb / + * Chromium logs thousands of lines per second to the same fd 1/2. Doing any + * per-line work that costs more than a memcmp — an OH_LOG_Print (an IPC to + * hilogd), an snprintf, a formatted write — lets the pipe fill up. The next + * writer then BLOCKS: Chromium's logging thread, node's abort(), or a + * signal handler. With Chromium's IO thread blocked the Web component never + * paints, which is exactly how the app used to sit on its splash screen + * until the ANR watchdog killed it. + * + * So the reader: + * 1. always drains (never lets a writer block) — an 8KB read per loop; + * 2. filters framework noise with cheap strncmp/strstr only; + * 3. writes surviving lines to the boot log with a raw write() (one + * syscall per line, no formatting, no locks); + * 4. hilog's at a hard rate limit with a fixed total budget. + */ static void *stdioReaderThread(void *p) { (void)p; - char buf[2048]; + char buf[8192]; char line[480]; size_t linelen = 0; - long suppressed = 0; + long hilogBudget = 400; /* total hilog lines we will ever emit */ + unsigned long lastHilogMs = 0; + for (;;) { ssize_t r = read(g_pipeOut, buf, sizeof(buf)); if (r < 0 && errno == EINTR) continue; @@ -116,13 +173,24 @@ static void *stdioReaderThread(void *p) { line[linelen] = '\0'; if (linelen > 0) { if (isFrameworkNoise(line)) { - suppressed++; - if (suppressed % 1000 == 0) { - logWrite("[io] … %ld framework log lines suppressed", - suppressed); - } + /* dropped on the floor — the only correct thing to do with + * thousands of Chromium lines a second. */ } else { - logWrite("[io] %.470s", line); + /* Raw write: no vsnprintf, no libc stdio locks. */ + if (g_logFd >= 0) { + ssize_t ign = write(g_logFd, line, linelen); + ign = write(g_logFd, "\n", 1); + (void)ign; + } + if (hilogBudget > 0) { + unsigned long now = nowMs(); + if (now - lastHilogMs >= 250) { /* max ~4 hilog IPCs per second */ + lastHilogMs = now; + hilogBudget--; + (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, + "electerm.embed", "[io] %.470s", line); + } + } } } linelen = 0; @@ -308,8 +376,17 @@ static void safeAppendInt(char *b, size_t cap, size_t *n, int v) { } static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { - (void)sig; static unsigned int seenBits[16]; /* 512 syscall numbers, logged once each */ + + /* Only emulate a real seccomp trap. A SIGSYS delivered by raise()/kill() + * carries no syscall context (si_code <= 0) and rewriting the register + * file for it corrupts whichever thread happened to be running. */ + if (!si || !ctx || si->si_code != 1 /* SYS_SECCOMP */) { + signal(sig, SIG_DFL); + raise(sig); + return; + } + int sc = si->si_syscall; if (sc >= 0 && sc < 512) { unsigned int bit = 1u << (sc & 31); @@ -322,14 +399,19 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { safeAppend(b, sizeof(b), &n, " ("); safeAppend(b, sizeof(b), &n, syscallName(sc)); safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1\n"); + /* Raw write() to the boot-log FILE only. + * + * This used to also write(2, b, n) — fd 2 is the stdio pipe shared + * with ArkWeb/Chromium. That pipe can be full (Chromium floods it), + * and write() on a full pipe BLOCKS; blocking inside a signal + * handler on a thread that already holds libc locks is how the node + * thread ended up faulting (SEGV in strlen) instead of getting a + * clean -1. The reader thread now surfaces these lines to hilog in + * normal context, where locks are actually safe. */ if (g_logFd >= 0) { ssize_t ign = write(g_logFd, b, n); (void)ign; } - /* fd 2 is the stdio pipe: the reader thread relays this line to - * hilog in NORMAL context (where printf locks are safe). */ - ssize_t ign = write(2, b, n); - (void)ign; } } ucontext_t *uc = (ucontext_t *)ctx; @@ -427,24 +509,63 @@ struct NodeThreadArgs { static struct NodeThreadArgs g_nodeArgs; -/* node::Start returning is abnormal (the server should run forever) — log - * it and let the thread end; the ArkTS probe timeout surfaces the failure. - * NEVER _exit() here: this is the app's main process. */ -static void *nodeThreadMain(void *p) { - struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; - g_nodeTid = (pid_t)syscall(__NR_gettid); - logWrite("[embed] node thread tid=%ld, calling node::Start", (long)g_nodeTid); - a->rc = a->start(3, a->argv); - logWrite("[embed] node::Start returned %d (backend stopped)", a->rc); +static const char *startEmbeddedNode(const char *params); + +/* ── Bootstrap thread ── + * + * Everything expensive happens HERE and never on the caller's thread: + * dlopen(libnode.so, RTLD_NOW) relocates every symbol of a ~120MB + * library, dlsym, and node::Start (which itself runs the whole server). + * + * pages/Index calls the NAPI startBackend() from aboutToAppear() — i.e. on + * the ArkTS UI thread. Doing the dlopen there blocked the UI thread for as + * long as the relocation took, so the Index page never painted: the window + * stayed on its splash screen until the APP_INPUT_BLOCK watchdog fired and + * the system ANR dialog killed the app. That is the reported symptom. + */ +static void *bootstrapMain(void *arg) { + char *params = (char *)arg; + startEmbeddedNode(params); + free(params); return NULL; } -static const char *startEmbeddedNode(const char *params) { - static char errBuf[256]; +static const char *startBackendAsync(const char *params) { + static char errBuf[128]; if (g_started) { return "err:already started"; } + g_started = 1; + + char *copy = strdup(params ? params : ""); + if (!copy) { + snprintf(errBuf, sizeof(errBuf), "err:out of memory"); + setStatus(ST_FAILED, errBuf); + return errBuf; + } + + setStatus(ST_LAUNCHING, "bootstrap thread starting"); + pthread_attr_t attr; + pthread_attr_init(&attr); + /* node::Start wants a big stack (V8 + the deep C++ bootstrap). */ + pthread_attr_setstacksize(&attr, 32 * 1024 * 1024); + pthread_t th; + int prc = pthread_create(&th, &attr, bootstrapMain, copy); + pthread_attr_destroy(&attr); + if (prc != 0) { + free(copy); + logWrite("[embed] bootstrap pthread_create failed: %s", strerror(prc)); + snprintf(errBuf, sizeof(errBuf), "err:pthread_create failed"); + setStatus(ST_FAILED, errBuf); + return errBuf; + } + pthread_detach(th); + return "launching"; +} + +static const char *startEmbeddedNode(const char *params) { + static char errBuf[256]; char extraEnv[MAX_ENV_VARS][MAX_LINE]; int extraEnvCount = 0; @@ -473,6 +594,7 @@ static const char *startEmbeddedNode(const char *params) { if (!cfg.script[0] || access(cfg.script, F_OK) != 0) { logWrite("[embed] FATAL: script missing: %s", cfg.script); snprintf(errBuf, sizeof(errBuf), "err:script missing"); + setStatus(ST_FAILED, errBuf); return errBuf; } @@ -525,6 +647,7 @@ static const char *startEmbeddedNode(const char *params) { if (!nodePath) { logWrite("[embed] FATAL: no libnode.so candidate exists (tried %d)", nCand); snprintf(errBuf, sizeof(errBuf), "err:no libnode.so"); + setStatus(ST_FAILED, errBuf); return errBuf; } logWrite("[embed] node binary: %s", nodePath); @@ -542,7 +665,15 @@ static const char *startEmbeddedNode(const char *params) { * flags via NODE_OPTIONS (rejected) or argv ("bad option"). The fix * must be applied at Node.js build time (configure flags). */ for (int i = 0; i < extraEnvCount; i++) { - putenv(extraEnv[i]); + /* setenv() COPIES the value. putenv() would store a pointer into + * `extraEnv`, a stack array of THIS frame — and since node now runs on + * a thread that outlives startBackend, that stack is long gone by the + * time libuv/node reads it. getenv() would then strlen() recycled + * stack memory: a SEGV from inside uv_loop_init. */ + char *eq = strchr(extraEnv[i], '='); + if (!eq) continue; + *eq = '\0'; + setenv(extraEnv[i], eq + 1, 1); } /* Guarantee fds 0/1/2 are open before anything node-related runs. libuv's @@ -587,7 +718,13 @@ static const char *startEmbeddedNode(const char *params) { if (g_logFd > 2) { int fds[2]; if (pipe(fds) == 0) { - logWrite("[embed] stdio pipe: read=%d write=%d", fds[0], fds[1]); + /* 1MB, up from the default 64KB: ArkWeb/Chromium shares this pipe and + * logs thousands of lines per second. A 64KB buffer fills in + * milliseconds whenever the reader is descheduled, and every writer + * that hits a full pipe blocks. */ + int pipeSz = fcntl(fds[0], F_SETPIPE_SZ, 1024 * 1024); + logWrite("[embed] stdio pipe: read=%d write=%d size=%d", fds[0], fds[1], + pipeSz); g_pipeOut = fds[0]; dup2(fds[1], 1); dup2(fds[1], 2); @@ -625,6 +762,7 @@ static const char *startEmbeddedNode(const char *params) { const char *e2 = dlerror(); logWrite("[embed] dlopen failed: %s / %s", e1 ? e1 : "-", e2 ? e2 : "-"); snprintf(errBuf, sizeof(errBuf), "err:dlopen failed"); + setStatus(ST_FAILED, errBuf); return errBuf; } } @@ -634,6 +772,7 @@ static const char *startEmbeddedNode(const char *params) { if (!start || (e && e[0])) { logWrite("[embed] dlsym(node::Start) failed: %s", e ? e : "null sym"); snprintf(errBuf, sizeof(errBuf), "err:node::Start not found"); + setStatus(ST_FAILED, errBuf); return errBuf; } logWrite("[embed] node::Start resolved at %p", (void *)start); @@ -653,20 +792,20 @@ static const char *startEmbeddedNode(const char *params) { g_nodeArgs.argv[2] = arg2; g_nodeArgs.argv[3] = NULL; - pthread_attr_t attr; - pthread_attr_init(&attr); - pthread_attr_setstacksize(&attr, 32 * 1024 * 1024); /* node wants a big stack */ - pthread_t th; - int prc = pthread_create(&th, &attr, nodeThreadMain, &g_nodeArgs); - if (prc != 0) { - logWrite("[embed] pthread_create failed: %s", strerror(prc)); - snprintf(errBuf, sizeof(errBuf), "err:pthread_create failed"); - return errBuf; - } - pthread_detach(th); - g_started = 1; - logWrite("[embed] node thread launched in main app process"); - return "ok"; + /* Run node::Start on THIS thread — we are already the detached bootstrap + * thread with a 32MB stack, so there is no reason to hand off again. + * + * node::Start returning is abnormal (the server should run forever); log + * it and let the thread end. NEVER _exit() here: this is the app's own + * process. */ + g_nodeTid = (pid_t)syscall(__NR_gettid); + setStatus(ST_RUNNING, "node::Start"); + logWrite("[embed] bootstrap tid=%ld, calling node::Start", (long)g_nodeTid); + int rc = start(3, g_nodeArgs.argv); + logWrite("[embed] node::Start returned %d (backend stopped)", rc); + snprintf(errBuf, sizeof(errBuf), "err:node::Start returned %d", rc); + setStatus(ST_FAILED, errBuf); + return errBuf; } /* ── NAPI surface ── */ @@ -681,13 +820,43 @@ static napi_value StartBackend(napi_env env, napi_callback_info info) { size_t copied = 0; napi_get_value_string_utf8(env, args[0], params, sizeof(params), &copied); } - const char *result = startEmbeddedNode(params); + const char *result = startBackendAsync(params); napi_value napiResult = NULL; napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &napiResult); return napiResult; } +/* Report how the background bootstrap is doing. The ArkTS page polls this + * while probing the HTTP port: + * "idle" — startBackend() was never called + * "launching:" — bootstrap thread alive, still resolving/dlopen-ing + * "running:" — node::Start entered (thread is the backend) + * "failed:" — hard failure, the backend will NEVER answer + */ +static napi_value GetBackendStatus(napi_env env, napi_callback_info info) { + (void)info; + int code = getStatusCode(); + char out[192]; + switch (code) { + case ST_RUNNING: + snprintf(out, sizeof(out), "running:%s", g_statusDetail); + break; + case ST_FAILED: + snprintf(out, sizeof(out), "failed:%s", g_statusDetail); + break; + case ST_LAUNCHING: + snprintf(out, sizeof(out), "launching:%s", g_statusDetail); + break; + default: + snprintf(out, sizeof(out), "idle"); + break; + } + napi_value napiResult = NULL; + napi_create_string_utf8(env, out, NAPI_AUTO_LENGTH, &napiResult); + return napiResult; +} + static napi_value KillNode(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value args[1]; @@ -716,7 +885,8 @@ EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"killNode", NULL, KillNode, NULL, NULL, NULL, napi_default, NULL}, - {"startBackend", NULL, StartBackend, NULL, NULL, NULL, napi_default, NULL}}; + {"startBackend", NULL, StartBackend, NULL, NULL, NULL, napi_default, NULL}, + {"getBackendStatus", NULL, GetBackendStatus, NULL, NULL, NULL, napi_default, NULL}}; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 7860724..d6369fc 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -812,7 +812,14 @@ __attribute__((visibility("default"))) void Main(NativeChildProcess_Args args) { setenv("SERVER_SECRET", cfg.secret, 1); } for (int i = 0; i < extraEnvCount; i++) { - putenv(extraEnv[i]); + /* setenv() COPIES. putenv() stores a pointer into `extraEnv`, a stack + * array of the enclosing frame — fine across an immediate execve (the + * kernel copies the strings) but a use-after-return for anything that + * reads environ later in this process. */ + char *eq = strchr(extraEnv[i], '='); + if (!eq) continue; + *eq = '\0'; + setenv(extraEnv[i], eq + 1, 1); } /* 4. Rebuild stdio deterministically (0=/dev/null, 1=2=boot log) so node's diff --git a/entry/src/main/cpp/types/libnode_ctl/index.d.ts b/entry/src/main/cpp/types/libnode_ctl/index.d.ts index 13aee2d..fcc5634 100644 --- a/entry/src/main/cpp/types/libnode_ctl/index.d.ts +++ b/entry/src/main/cpp/types/libnode_ctl/index.d.ts @@ -1 +1,3 @@ export const killNode: (pid: number) => number; +export const startBackend: (params: string) => string; +export const getBackendStatus: () => string; diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 0d56be2..37a0d8b 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -39,12 +39,14 @@ export default class EntryAbility extends UIAbility { onConfigurationUpdate(config: Configuration) { } - async onWindowStageCreate(windowStage: window.WindowStage) { - // Request permissions up front. Index requests them again via - // its own context when it needs to write user-visible files; a - // granted permission here just avoids the dialogs appearing later. - await this.requestAllPermissions(); - + onWindowStageCreate(windowStage: window.WindowStage) { + // loadContent FIRST, permissions AFTER. + // + // Awaiting requestPermissionsFromUser() before loadContent() means the + // window shows nothing but its splash until the user answers the dialog. + // On an unattended device (cloud debugging / 云调试, CI smoke runs) nobody + // taps "Allow", so the app sat on the splash screen indefinitely and the + // input-dispatch watchdog eventually killed it as APP_INPUT_BLOCK. windowStage.loadContent('pages/Index', (err) => { if (err.code) { console.error(`[${TAG}] Failed to load content: ${JSON.stringify(err)}`); @@ -52,6 +54,11 @@ export default class EntryAbility extends UIAbility { } console.info(`[${TAG}] content loaded`); }); + + // Request permissions in the background. Index requests them again via + // its own context when it needs to write user-visible files; a granted + // permission here just avoids the dialogs appearing later. + this.requestAllPermissions(); } onWindowStageDestroy() { diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 89a4d4a..c1a9c90 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -25,7 +25,7 @@ import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import fs from '@ohos.file.fs'; import http from '@ohos.net.http'; -import { startBackend } from 'libnode_ctl.so'; +import { startBackend, getBackendStatus } from 'libnode_ctl.so'; import { BackendManager } from '../BackendManager'; const TAG: string = 'electerm.Index'; @@ -33,7 +33,19 @@ const DOMAIN: number = 0xE1EC; const BACKEND_PORT: number = 5577; const SERVER_URL: string = `http://127.0.0.1:${BACKEND_PORT}`; const POLL_INTERVAL_MS: number = 500; -const BOOT_TIMEOUT_MS: number = 90_000; +/** + * How long to wait for the backend before giving up on a boot strategy. + * + * This used to be 90s. A dead backend therefore pinned the app on the boot + * overlay for a minute and a half — indistinguishable from "stuck on the + * splash screen", which is exactly the bug report. The in-process path + * reports hard failures (missing script / no libnode.so / dlopen failure) + * within a few hundred ms via getBackendStatus(), so a short timeout loses + * nothing and turns a hang into a readable error. + */ +const BOOT_TIMEOUT_MS: number = 20_000; +/** Never read more than this from node-boot.log — this runs on the UI thread. */ +const MAX_BOOT_LOG_BYTES: number = 256 * 1024; /** * Prefer the per-process el2 junction over the parent's filesDir string: * /data/storage/el2/base/files points at the same storage but is mounted in @@ -52,7 +64,12 @@ struct Index { dataDir: string = ''; aboutToAppear(): void { - this.startBackend(); + // Defer the boot by a frame. aboutToAppear() runs BEFORE the first + // render, so anything started synchronously here delays the very + // overlay that is supposed to tell the user the engine is starting. + setTimeout(() => { + this.startBackend(); + }, 50); } /** @@ -131,7 +148,7 @@ struct Index { fs.mkdirSync(this.dataDir, true); } - // 2. start the native child process + // 2. start the backend // entryParams is a plain "key=value\n" string parsed by node_launcher.c const paramLines: string[] = [ `dataDir=${this.dataDir}`, @@ -151,30 +168,48 @@ struct Index { // pattern). The nativespawn child runs under a stricter seccomp filter // whose event-loop syscall blocks kill libuv there; the main process // runs libuv-class loops of its own (NETSTACK/curl), so node lives here. - let startedInProcess: boolean = false; + // + // startBackend() RETURNS IMMEDIATELY — the dlopen of the ~120MB + // libnode.so and node::Start happen on a background thread. Doing them + // inline here blocked the UI thread long enough that the Index page + // never painted: the window sat on its splash screen until the + // APP_INPUT_BLOCK watchdog fired and the ANR dialog killed the app. + let launchedInProcess: boolean = false; try { const rc: string = startBackend(entryParams); - startedInProcess = rc === 'ok'; + launchedInProcess = !rc.startsWith('err:'); hilog.info(DOMAIN, TAG, 'in-process startBackend → %{public}s', rc); } catch (e) { hilog.error(DOMAIN, TAG, 'in-process startBackend threw: %{public}s', JSON.stringify(e)); } - if (startedInProcess) { + if (launchedInProcess) { BackendManager.setInProcess(); - hilog.info(DOMAIN, TAG, 'node running in-process (main app process)'); - } else { - // Fallback: native child process (libnode_launcher.so:Main). - const pid: number = await childProcessManager.startNativeChildProcess( - 'libnode_launcher.so:Main', - { entryParams: entryParams } - ); - BackendManager.setPid(pid); - hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); + hilog.info(DOMAIN, TAG, 'node running in-process (background thread)'); + + // 3. wait for the HTTP server, bailing out early on a hard failure + const ok: boolean = await this.waitForBackend(BOOT_TIMEOUT_MS, true); + if (ok) { + // 4. load the real UI + this.serverReady = true; + this.controller.loadUrl(SERVER_URL); + return; + } + // The in-process node thread is detached — if it is merely slow + // rather than dead, it may still bind the port later; either way the + // next bind attempt just fails and the child takes over. + hilog.error(DOMAIN, TAG, 'in-process backend never answered — trying native child process'); } - // 3. wait for the HTTP server - const ok: boolean = await this.waitForBackend(); + // Fallback: native child process (libnode_launcher.so:Main). + const pid: number = await childProcessManager.startNativeChildProcess( + 'libnode_launcher.so:Main', + { entryParams: entryParams } + ); + BackendManager.setPid(pid); + hilog.info(DOMAIN, TAG, 'node child process started, pid=%{public}d', pid); + + const ok: boolean = await this.waitForBackend(BOOT_TIMEOUT_MS, false); if (!ok) { this.bootFailed = true; const tail: string = this.readBootLogTailLines(14); @@ -185,7 +220,6 @@ struct Index { return; } - // 4. load the real UI this.serverReady = true; this.controller.loadUrl(SERVER_URL); } catch (e) { @@ -196,13 +230,20 @@ struct Index { } } - /** Read the launcher/node boot log (written by the child). */ + /** Read the launcher/node boot log (written by the child). + * Size-guarded: this runs on the UI thread on every poll tick, and a + * runaway writer would otherwise turn each read into a UI stall. */ readBootLog(): string { if (!this.dataDir) { return ''; } + const path: string = `${this.dataDir}/node-boot.log`; try { - return fs.readTextSync(`${this.dataDir}/node-boot.log`); + const stat = fs.statSync(path); + if (stat.size > MAX_BOOT_LOG_BYTES) { + return ''; + } + return fs.readTextSync(path); } catch { return ''; } @@ -281,17 +322,33 @@ struct Index { } } - /** Poll http://127.0.0.1:5577 until it answers or BOOT_TIMEOUT_MS elapses. + /** Poll http://127.0.0.1:5577 until it answers or `timeoutMs` elapses. * While waiting, surface the child's latest boot-log line in the overlay so - * a stuck boot is diagnosable from the screen alone. */ - async waitForBackend(): Promise { - const deadline: number = Date.now() + BOOT_TIMEOUT_MS; + * a stuck boot is diagnosable from the screen alone. + * + * When `checkStatus` is set (in-process boot only), getBackendStatus() is + * consulted every tick: a hard bootstrap failure is known in milliseconds + * instead of after the whole timeout, so we can fall back to the native + * child process instead of staring at a spinner. */ + async waitForBackend(timeoutMs: number, checkStatus: boolean): Promise { + const deadline: number = Date.now() + timeoutMs; let tick: number = 0; let shownLine: string = ''; while (Date.now() < deadline) { if (await this.probe()) { return true; } + if (checkStatus) { + const st: string = getBackendStatus(); + if (st.startsWith('failed:')) { + const reason: string = st.substring('failed:'.length).replace('err:', ''); + hilog.error(DOMAIN, TAG, 'bootstrap failed: %{public}s', reason); + this.bootFailed = true; + this.statusMessage = `Engine failed to start: ${reason}`; + this.logBootTail(); + return false; + } + } tick++; if (tick % 4 === 0) { // every ~2s const line: string = this.readBootLogLastLine(); From 138b5224787dbce99244cadede996d0e1e216edc Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 06:52:08 +0800 Subject: [PATCH 32/52] CI: pin NODE_VERSION to 24.2.0 (the 24.19.0 asset 404s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Use self build arm64 nodejs for harmony" switched prepare-node.sh from hqzing/ohos-node to our own --shared libnode.so release, but left the workflow's NODE_VERSION at 24.19.0. prepare-node.sh derives the tag as "ohos-node-shared-v${NODE_VERSION}", so CI tried .../releases/download/ohos-node-shared-v24.19.0/libnode-arm64.so which does not exist — the only published tag is ohos-node-shared-v24.2.0. The run for that commit failed at "Prepare Node.js runtime" with curl: (22) 404 (it had been passing before only because the node-runtime cache still held the old hqzing binary). Bump the workflow to 24.2.0 to match the script default, and derive the build-summary "Runtime" line from env.NODE_VERSION so the two cannot drift silently again. --- .github/workflows/build-web.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 517458f..d113b21 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,7 @@ name: Build HarmonyOS Web APP (ArkWeb + on-device Node.js) # Web variant of the app — no electron-harmony runtime: -# ArkWeb (Web component) + hqzing/ohos-node running the electerm-web +# ArkWeb (Web component) + our shared libnode.so running the electerm-web # backend as a native child process (childProcessManager.startNativeChildProcess). # # Triggers on dev2 pushes. Uploads the signed .app as an artifact. @@ -21,9 +21,13 @@ permissions: contents: read env: - # hqzing/ohos-node release used as the on-device Node.js runtime. - # Keep in sync with the default in scripts/prepare-node.sh. - NODE_VERSION: '24.19.0' + # Our own shared libnode.so release used as the on-device Node.js runtime. + # MUST stay in sync with the default in scripts/prepare-node.sh: the script + # derives the release tag as "ohos-node-shared-v${NODE_VERSION}", so a + # mismatch turns into a 404 on the asset download. + # 24.19.0 was the last hqzing/ohos-node release; 24.2.0 is the first + # self-built --shared libnode (a real shared library, not a PIE). + NODE_VERSION: '24.2.0' jobs: build: @@ -155,7 +159,7 @@ jobs: ohpm-web- # ── Step 2: Prepare the OpenHarmony Node.js runtime ─────────────────── - # hqzing/ohos-node prebuilt binary → entry/libs/arm64-v8a/libnode.so + # shared libnode.so release → entry/libs/arm64-v8a/libnode.so - name: Restore Node runtime cache id: node_cache uses: actions/cache/restore@v4 @@ -163,7 +167,7 @@ jobs: path: .cache/node-runtime key: ohos-node-${{ env.NODE_VERSION }} - - name: Prepare Node.js runtime (hqzing/ohos-node) + - name: Prepare Node.js runtime (shared libnode.so) run: ./scripts/prepare-node.sh - name: Save Node runtime cache @@ -270,7 +274,7 @@ jobs: echo "|------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Runtime | \`hqzing/ohos-node v24.19.0 (libnode.so)\` |" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | \`shared libnode.so v${{ env.NODE_VERSION }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Web app | \`electerm-web backend + ArkWeb frontend\` |" >> $GITHUB_STEP_SUMMARY echo "| App version | \`$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo unknown)\` |" >> $GITHUB_STEP_SUMMARY if [ -f "${{ steps.find_app.outputs.app_path }}" ]; then From d48cd1cdaa95a692bf7a074f7690decd50282b17 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 07:09:49 +0800 Subject: [PATCH 33/52] Make the boot overlay self-diagnosing; drop the setTimeout indirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 07:02:50 capture contains no output from the app process at all: no hilog, no ANR, no crash, no kill. The window (mission 170) was already focused when the capture started, so the boot happened before it and the log cannot tell us where the app is stuck. Under cloud debugging hilog is ring-buffered away far faster than it can be exported, so the screen has to be the primary diagnostic channel. Two changes. 1. Remove the setTimeout in aboutToAppear(). I added a 50ms timer last commit to let the overlay paint first. That was solving a problem that no longer exists — the only slow thing there was the synchronous dlopen, which now runs on a background thread — and it introduced a failure mode that is invisible from the outside: if the timer never fires the overlay sits on its default "Starting electerm …" forever with zero log output, which is exactly the reported symptom. Call startBackend() directly. 2. Make the overlay a live ladder that never goes static. It previously only updated when node-boot.log had content, and logBootTailWindow() returns early on empty input — so an empty boot log (the single most useful thing to know) produced no screen update AND no hilog at all. Now every tick shows: Starting engine … 12s probe: [2300023] connect refused backend: launching:bootstrap thread starting probe() now records why the connection failed instead of swallowing it, and startBackend() sets a status at each stage (preparing data dir / loading engine / falling back to child process) so a screenshot alone identifies the stuck stage. Also reduced boot-log reads from 2 Hz to 0.5 Hz — it is file I/O on the UI thread and the value is cached for the per-tick overlay refresh. --- entry/src/main/ets/pages/Index.ets | 76 ++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index c1a9c90..060b348 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -62,14 +62,20 @@ struct Index { @State bootFailed: boolean = false; @State statusMessage: string = 'Starting electerm …'; dataDir: string = ''; + /** Result of the last HTTP probe — shown on the overlay so a screenshot + * reveals whether anything is listening on 127.0.0.1:5577 at all. */ + lastProbeError: string = ''; + /** Last line of node-boot.log, refreshed at 0.5 Hz (file I/O on the UI thread). */ + lastBootLine: string = ''; aboutToAppear(): void { - // Defer the boot by a frame. aboutToAppear() runs BEFORE the first - // render, so anything started synchronously here delays the very - // overlay that is supposed to tell the user the engine is starting. - setTimeout(() => { - this.startBackend(); - }, 50); + // Called directly, NOT via setTimeout: the only thing that used to make + // this slow was the synchronous dlopen of libnode.so, and that now runs + // on a background thread. A timer adds a failure mode we cannot observe + // (if it never fires the overlay sits on its default message forever with + // zero log output) and buys nothing. + this.statusMessage = 'Preparing …'; + this.startBackend(); } /** @@ -160,8 +166,10 @@ struct Index { } const entryParams: string = paramLines.join('\n'); - this.statusMessage = 'Starting Node.js engine …'; + this.statusMessage = 'Preparing data dir …'; hilog.info(DOMAIN, TAG, 'backend params: %{public}s', entryParams); + hilog.info(DOMAIN, TAG, 'script=%{public}s node=%{public}s', scriptPath, + nodePath !== '' ? nodePath : '(not found)'); // Primary path: run node IN the main app process (dlopen libnode.so + // node::Start via libnode_ctl.so — the electron-harmony / nodejs-mobile @@ -185,6 +193,7 @@ struct Index { if (launchedInProcess) { BackendManager.setInProcess(); + this.statusMessage = 'Loading engine (libnode.so) …'; hilog.info(DOMAIN, TAG, 'node running in-process (background thread)'); // 3. wait for the HTTP server, bailing out early on a hard failure @@ -202,6 +211,7 @@ struct Index { } // Fallback: native child process (libnode_launcher.so:Main). + this.statusMessage = 'In-process engine failed — starting child process …'; const pid: number = await childProcessManager.startNativeChildProcess( 'libnode_launcher.so:Main', { entryParams: entryParams } @@ -331,17 +341,18 @@ struct Index { * instead of after the whole timeout, so we can fall back to the native * child process instead of staring at a spinner. */ async waitForBackend(timeoutMs: number, checkStatus: boolean): Promise { - const deadline: number = Date.now() + timeoutMs; + const startedAt: number = Date.now(); + const deadline: number = startedAt + timeoutMs; let tick: number = 0; - let shownLine: string = ''; while (Date.now() < deadline) { if (await this.probe()) { return true; } + let status: string = ''; if (checkStatus) { - const st: string = getBackendStatus(); - if (st.startsWith('failed:')) { - const reason: string = st.substring('failed:'.length).replace('err:', ''); + status = getBackendStatus(); + if (status.startsWith('failed:')) { + const reason: string = status.substring('failed:'.length).replace('err:', ''); hilog.error(DOMAIN, TAG, 'bootstrap failed: %{public}s', reason); this.bootFailed = true; this.statusMessage = `Engine failed to start: ${reason}`; @@ -349,21 +360,33 @@ struct Index { return false; } } - tick++; + + // Always refresh the overlay, and always log — including when + // node-boot.log is EMPTY. An empty boot log means the native side never + // wrote a line, which is itself the single most useful fact to know, + // and the old code logged nothing at all in that case (logBootTailWindow + // returns early on empty input). hilog is ring-buffered away under + // cloud-debug system noise far faster than it can be exported, so the + // SCREEN is the primary diagnostic channel and must never go static. + const secs: number = Math.round((Date.now() - startedAt) / 1000); + const parts: string[] = [`Starting engine … ${secs}s`, `probe: ${this.lastProbeError}`]; + if (checkStatus) { + parts.push(`backend: ${status !== '' ? status : 'unknown'}`); + } + if (this.lastBootLine) { + parts.push(this.lastBootLine); + } + this.statusMessage = parts.join('\n'); + if (tick % 4 === 0) { // every ~2s - const line: string = this.readBootLogLastLine(); - if (line && line !== shownLine) { - shownLine = line; - this.statusMessage = line; - hilog.info(DOMAIN, TAG, 'boot: %{public}s', line); - } - // Periodic tail dump: the cloud-debug hilog export grabs the OLDEST - // buffered lines, and ~1600 lines/s of system noise flushes the ring - // long before the 90s deadline — re-dumping the tail every ~2s keeps - // a copy of the boot ladder (and any crash marker) inside whatever - // window survives. + // Reading the boot log is file I/O on the UI thread — keep it at + // 0.5 Hz and reuse the cached line for the per-tick overlay refresh. + this.lastBootLine = this.readBootLogLastLine(); + hilog.info(DOMAIN, TAG, 'waiting %{public}ds probe=%{public}s status=%{public}s boot=%{public}s', + secs.toString(), this.lastProbeError, status, this.lastBootLine); this.logBootTailWindow(1100); } + tick++; await this.sleep(POLL_INTERVAL_MS); } return false; @@ -378,8 +401,11 @@ struct Index { readTimeout: 3000, usingCache: false }); + this.lastProbeError = `HTTP ${response.responseCode}`; return response.responseCode >= 200 && response.responseCode < 500; - } catch { + } catch (e) { + const err = e as BusinessError; + this.lastProbeError = `[${err.code}] ${err.message}`; return false; } finally { httpClient.destroy(); From 4d14174be5628d76bbdb9e971ab427223e1e22b8 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 07:22:41 +0800 Subject: [PATCH 34/52] Locate the node::Start SIGSEGV instead of guessing at it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third device round (log 07_16_16). The new build is definitely installed — the capture shows `[embed] stdio pipe: read=41 write=42 size=1048576` (the 1 MB pipe) and `[embed] bootstrap tid=52528`, so the bootstrap thread ran the whole ladder, dlopen'd libnode.so, resolved node::Start and called it. It then died with SIGSEGV, and `backtrace()` returned ZERO frames, so the crash is currently unlocatable. (Note the ANR in that log is a different, earlier process — pid 49402 at 07:15:20; the new instance is pid 52233 at 07:15:46. The app no longer ANRs on its own boot.) Four changes: 1. Log the fault context — signal, si_code, si_addr, PC, SP, LR, x0 — and the backtrace frame count, before attempting the backtrace. When musl cannot unwind through the signal frame (which is what happens here) the raw PC is the only thing that can be resolved offline against the unstripped libnode.so, and previously we logged nothing at all. 2. Report ST_FAILED when the node thread crashes. setStatus(ST_RUNNING) is published before node::Start, so a crash left the app reporting "running" forever and the page burned the whole 20 s boot timeout before trying the fallback. It now fails in ~1 s and the native child process gets a real chance in the same run. 3. Fix the SIGSYS shim's PC handling. It unconditionally did `pc += 4` to skip the trapped svc. Whether the kernel delivers SIGSYS with the PC still ON the trapping instruction or already advanced past it is not portable; skipping when it has already moved resumes mid-stream, which is a very plausible source of the SEGV. Compare the PC with si_addr (Linux aliases si_call_addr onto si_addr for SIGSYS) and skip only when the PC is still on it. Log both values and the delta. 4. Pre-flight io_uring_setup (syscall 425) on the bootstrap thread, before node::Start. libuv calls it unconditionally — its UV_USE_IO_URING getenv check sits behind `tbz w3,#1` on the flags argument and uv__platform_loop_init passes flags=0, so the env var never applies. Doing the same call on a thread we own turns "unexplained SEGV inside node::Start" into a logged rc/errno marker immediately before it. Also include for uintptr_t rather than relying on transitive includes. --- entry/src/main/cpp/node_ctl.c | 95 +++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index 4ff1bbf..f3fc8e0 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -227,10 +228,26 @@ static void logWrite(const char *fmt, ...) { static pid_t g_nodeTid = 0; /* tid of the node thread once launched */ static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { - (void)si; - (void)ctx; int saved = errno; long tid = (long)syscall(__NR_gettid); + ucontext_t *uc = (ucontext_t *)ctx; + unsigned long pc = 0, sp = 0, lr = 0, arg0 = 0; + +#if defined(__aarch64__) + if (uc) { + pc = uc->uc_mcontext.pc; + sp = uc->uc_mcontext.sp; + lr = uc->uc_mcontext.regs[30]; + arg0 = uc->uc_mcontext.regs[0]; + } +#elif defined(__x86_64__) + if (uc) { + pc = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; + sp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP]; + arg0 = (unsigned long)uc->uc_mcontext.gregs[REG_RAX]; + } +#endif + char b[128]; int n = snprintf(b, sizeof(b), "[embed] fatal: signal %d on tid %ld", sig, tid); @@ -243,6 +260,18 @@ static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { (void)OH_LOG_Print(LOG_APP, LOG_ERROR, 0xE1EC, "electerm.embed", "%{public}s", b); } + + /* The fault context. backtrace() on aarch64/musl very often returns ZERO + * frames (it cannot unwind through the signal frame), and when that + * happens the crash used to be completely unlocatable. The raw PC plus + * the faulting address can always be resolved offline against the + * unstripped libnode.so (see .workbuddy/tools/sym.py), so log them + * FIRST — before the backtrace, which can itself abort. */ + logWrite("[embed] fault: sig=%d code=%d addr=0x%lx pc=0x%lx sp=0x%lx lr=0x%lx x0=0x%lx", + sig, si ? si->si_code : -1, + (unsigned long)(si ? (uintptr_t)si->si_addr : 0), + pc, sp, lr, arg0); + /* Capture the crashing thread's native stack. The abort text (e.g. * "Assertion failed: fd > STDERR_FILENO ... uv__close") names the dying * function but never its CALLER — that's the missing piece. libnode.so is @@ -253,6 +282,9 @@ static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { { void *bt[24]; int frames = backtrace(bt, 24); + /* Log the frame count even when it is 0: "no frames" is itself the + * answer to "why is there no backtrace". */ + logWrite("[embed] backtrace frames=%d", frames); for (int i = 0; i < frames; i++) { Dl_info info; char lb[192]; @@ -290,6 +322,17 @@ static void crashMarkerHandler(int sig, siginfo_t *si, void *ctx) { /* node's thread crashed — freeze it, keep the app alive. Never returns; * if the crash corrupted a libc lock the UI may eventually freeze too, * but the evidence is already on disk and in hilog. */ + char msg[128]; + snprintf(msg, sizeof(msg), "node thread died: signal %d at pc=0x%lx", + sig, pc); + /* Report the failure so the ArkTS probe stops waiting: it would + * otherwise sit on "running" for the whole boot timeout, because + * setStatus(ST_RUNNING) was published before node::Start and nobody + * updated it when the thread died. Failing fast lets the page fall + * back to the native child process immediately. */ + setStatus(ST_FAILED, msg); + logWrite("[embed] %s — failing fast so the page can try the child process", + msg); for (;;) { pause(); } @@ -415,12 +458,34 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } } ucontext_t *uc = (ucontext_t *)ctx; + /* si_addr is the address of the trapping instruction for a SIGSYS + * (Linux aliases si_call_addr onto si_addr). Compare it with the signal + * frame's PC instead of blindly skipping the svc: + * + * the handler must skip the trapped syscall instruction so execution + * resumes after it, but whether the kernel delivered the signal with the + * PC still ON the svc or already advanced past it is NOT portable. Adding + * 4 when the PC has already moved skips a real instruction and resumes + * mid-stream — a very good way to turn a handled trap into a SIGSEGV a + * few instructions later, which is what we have been seeing. + */ + unsigned long callAddr = (unsigned long)(uintptr_t)(si ? si->si_addr : 0); #if defined(__aarch64__) - uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ + logWrite("[embed] SIGSYS pc=0x%lx call_addr=0x%lx (delta %ld)", + uc->uc_mcontext.pc, callAddr, + (long)(uc->uc_mcontext.pc - callAddr)); + if (callAddr == 0 || uc->uc_mcontext.pc == callAddr) { + uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ + } uc->uc_mcontext.regs[0] = (unsigned long)-1; #elif defined(__x86_64__) - /* On x86_64, skip the syscall instruction and set return to -1 */ - uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + logWrite("[embed] SIGSYS rip=0x%lx call_addr=0x%lx (delta %ld)", + (unsigned long)uc->uc_mcontext.gregs[REG_RIP], callAddr, + (long)((unsigned long)uc->uc_mcontext.gregs[REG_RIP] - callAddr)); + if (callAddr == 0 || + (unsigned long)uc->uc_mcontext.gregs[REG_RIP] == callAddr) { + uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + } uc->uc_mcontext.gregs[REG_RAX] = (unsigned long)-1; #endif /* Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes the raw @@ -798,6 +863,26 @@ static const char *startEmbeddedNode(const char *params) { * node::Start returning is abnormal (the server should run forever); log * it and let the thread end. NEVER _exit() here: this is the app's own * process. */ + /* Pre-flight the exact syscall libuv is about to make. + * + * libuv's uv__iou_init() calls io_uring_setup (425) unconditionally: its + * UV_USE_IO_URING getenv check sits behind a `tbz w3,#1` on the flags + * argument, and uv__platform_loop_init passes flags=0, so the env var is + * never consulted and setting it does nothing. The sandbox traps the + * syscall, so the SIGSYS shim has to carry it. + * + * Do the same call here, on a thread we fully control and before node is + * up, so a shim that does not work shows up as a logged marker right + * before the crash instead of an unexplained SIGSEGV inside node::Start. + */ + { + errno = 0; + long rc = syscall(425 /* __NR_io_uring_setup */, 8, (void *)0); + int preErrno = errno; + logWrite("[embed] io_uring preflight: rc=%ld errno=%d (%s)", + rc, preErrno, rc == -1 ? strerror(preErrno) : "not trapped"); + } + g_nodeTid = (pid_t)syscall(__NR_gettid); setStatus(ST_RUNNING, "node::Start"); logWrite("[embed] bootstrap tid=%ld, calling node::Start", (long)g_nodeTid); From 499ac2265e3b6ee2211c5a16f13b34f4f73f01a0 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 07:46:28 +0800 Subject: [PATCH 35/52] Fix the SIGSYS handler: it was killing the very thread it protects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth device round (log 07_39_55) finally located the crash, and it is not in node — it is in our own SIGSYS handling. [embed] SIGSYS pc=0x59b6cff74c call_addr=0x59b6cff74c (delta 0) [embed] fault: sig=11 code=1 addr=0x0 pc=0x59b6cf5a50 lr=0x1c4200... [embed] bt[7] +19112 (libnode_ctl.so) <- startEmbeddedNode [embed] bt[8] +15760 (libnode_ctl.so) <- bootstrapMain The two deepest frames are OUR code, not libnode.so, and neither "bootstrap tid=... calling node::Start" nor "io_uring preflight" was ever logged. So the thread died inside the pre-flight syscall(425) I added last round — i.e. in the SIGSYS handler, one second before node::Start is even reached. addr=0x0 with a garbage LR is a NULL dereference in musl. Cause: last round I put `logWrite()` in the SIGSYS handler to report pc/call_addr. logWrite() is vsnprintf + OH_LOG_Print, both of which take libc locks. This file already documents that as forbidden in a signal handler — it is the exact failure mode recorded on 2026-08-28. I violated my own rule. 1. The handler is async-signal-safe again. It composes its line with safeAppend/safeAppendInt plus a new safeAppendHex (hand-rolled, no snprintf) and emits it with a bare write() to the boot-log file. No vsnprintf, no hilog IPC. 2. Decide whether to skip the trapped instruction by READING IT, not by comparing pc against si_addr. Both values come from the same pt_regs, so they agree whether or not the kernel already advanced past the syscall — the measured "delta 0" is equally consistent with both cases, so it proved nothing. Check for the aarch64 `svc #0` encoding (0xd4000001) instead, and log the result as onsvc=. 3. Same two fixes in node_launcher.c, which matters more now: the child process is the live fallback and had the unconditional `pc += 4` with no si_code guard and no safe-logging discipline. 4. Pre-flight logs before AND after syscall(425) and passes a zeroed params buffer, so if the trap still kills us we at least see the "calling" marker. --- entry/src/main/cpp/node_ctl.c | 115 ++++++++++++++++++++--------- entry/src/main/cpp/node_launcher.c | 58 +++++++++++++-- 2 files changed, 133 insertions(+), 40 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index f3fc8e0..e2d43b0 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -418,6 +418,39 @@ static void safeAppendInt(char *b, size_t cap, size_t *n, int v) { } } +/* Same contract as safeAppendInt, for addresses. Hand-rolled because + * snprintf is off-limits in a signal handler (see the note above). */ +static void safeAppendHex(char *b, size_t cap, size_t *n, unsigned long v) { + int started = 0; + for (int shift = 60; shift >= 0; shift -= 4) { + unsigned int d = (unsigned int)((v >> shift) & 0xFu); + if (d == 0 && !started && shift != 0) continue; + started = 1; + if (*n >= cap) return; + b[(*n)++] = (char)(d < 10 ? ('0' + d) : ('a' + (d - 10))); + } + if (!started && *n < cap) { + b[(*n)++] = '0'; + } +} + +/* Is there an aarch64 `svc #0` (encoded 0xd4000001) at `pc`? + * + * Deciding whether to skip the trapped instruction by comparing the signal + * frame's PC with si_addr does NOT work: both values come from the same + * pt_regs, so they agree whether or not the kernel already advanced past + * the syscall. Device-verified 2026-08-31: the delta was 0, which is + * consistent with BOTH "PC on the svc" and "PC already past it". + * Looking at the instruction encoding is the only way to tell. */ +static int pcIsSvcInsn(unsigned long pc) { + if (pc == 0 || (pc & 3U) != 0) { + return 0; + } + unsigned int insn = 0; + memcpy(&insn, (const void *)pc, sizeof(insn)); + return (insn & 0xffe0001fu) == 0xd4000001u; +} + static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { static unsigned int seenBits[16]; /* 512 syscall numbers, logged once each */ @@ -430,61 +463,72 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { return; } + ucontext_t *uc = (ucontext_t *)ctx; + unsigned long callAddr = (unsigned long)(uintptr_t)si->si_addr; + unsigned long pc = 0; +#if defined(__aarch64__) + pc = uc->uc_mcontext.pc; +#elif defined(__x86_64__) + pc = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; +#endif + int onSvc = pcIsSvcInsn(pc); + int sc = si->si_syscall; if (sc >= 0 && sc < 512) { unsigned int bit = 1u << (sc & 31); if (!(seenBits[sc >> 5] & bit)) { seenBits[sc >> 5] |= bit; - char b[96]; + char b[192]; size_t n = 0; safeAppend(b, sizeof(b), &n, "[embed] SIGSYS: syscall "); safeAppendInt(b, sizeof(b), &n, sc); safeAppend(b, sizeof(b), &n, " ("); safeAppend(b, sizeof(b), &n, syscallName(sc)); - safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1\n"); + safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1"); + safeAppend(b, sizeof(b), &n, " pc=0x"); + safeAppendHex(b, sizeof(b), &n, pc); + safeAppend(b, sizeof(b), &n, " call=0x"); + safeAppendHex(b, sizeof(b), &n, callAddr); + safeAppend(b, sizeof(b), &n, " d="); + safeAppendInt(b, sizeof(b), &n, (int)(long)(pc - callAddr)); + safeAppend(b, sizeof(b), &n, " onsvc="); + safeAppendInt(b, sizeof(b), &n, onSvc); + safeAppend(b, sizeof(b), &n, "\n"); /* Raw write() to the boot-log FILE only. * - * This used to also write(2, b, n) — fd 2 is the stdio pipe shared - * with ArkWeb/Chromium. That pipe can be full (Chromium floods it), - * and write() on a full pipe BLOCKS; blocking inside a signal - * handler on a thread that already holds libc locks is how the node - * thread ended up faulting (SEGV in strlen) instead of getting a - * clean -1. The reader thread now surfaces these lines to hilog in - * normal context, where locks are actually safe. */ + * No logWrite() here. logWrite() is vsnprintf + OH_LOG_Print, and + * both take libc locks. Calling them from this handler is + * device-proven to crash the trapped thread: on 2026-08-31 the run + * that logged pc/call_addr via logWrite() died with + * SIGSEGV code=1 addr=0x0 pc= lr= + * immediately after the handler returned. Compose with fixed strings + * and hand-rolled number formatting, then a bare write(2) — that is + * the only thing allowed here. + * + * This also used to write to fd 2, the stdio pipe shared with + * ArkWeb/Chromium. That pipe can be full (Chromium floods it) and + * write() on a full pipe BLOCKS — inside a signal handler, fatal. + * The reader thread picks the line up from the boot log and relays it + * to hilog in normal context, where locks are actually safe. */ if (g_logFd >= 0) { ssize_t ign = write(g_logFd, b, n); (void)ign; } } } - ucontext_t *uc = (ucontext_t *)ctx; - /* si_addr is the address of the trapping instruction for a SIGSYS - * (Linux aliases si_call_addr onto si_addr). Compare it with the signal - * frame's PC instead of blindly skipping the svc: - * - * the handler must skip the trapped syscall instruction so execution - * resumes after it, but whether the kernel delivered the signal with the - * PC still ON the svc or already advanced past it is NOT portable. Adding - * 4 when the PC has already moved skips a real instruction and resumes - * mid-stream — a very good way to turn a handled trap into a SIGSEGV a - * few instructions later, which is what we have been seeing. - */ - unsigned long callAddr = (unsigned long)(uintptr_t)(si ? si->si_addr : 0); #if defined(__aarch64__) - logWrite("[embed] SIGSYS pc=0x%lx call_addr=0x%lx (delta %ld)", - uc->uc_mcontext.pc, callAddr, - (long)(uc->uc_mcontext.pc - callAddr)); - if (callAddr == 0 || uc->uc_mcontext.pc == callAddr) { + if (onSvc) { uc->uc_mcontext.pc += 4; /* skip the 4-byte svc instruction */ } uc->uc_mcontext.regs[0] = (unsigned long)-1; #elif defined(__x86_64__) - logWrite("[embed] SIGSYS rip=0x%lx call_addr=0x%lx (delta %ld)", - (unsigned long)uc->uc_mcontext.gregs[REG_RIP], callAddr, - (long)((unsigned long)uc->uc_mcontext.gregs[REG_RIP] - callAddr)); - if (callAddr == 0 || - (unsigned long)uc->uc_mcontext.gregs[REG_RIP] == callAddr) { - uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + /* x86-64 `syscall` is 0f 05. */ + if (pc != 0) { + unsigned char c[2] = {0, 0}; + memcpy(c, (const void *)pc, 2); + if (c[0] == 0x0fu && c[1] == 0x05u) { + uc->uc_mcontext.gregs[REG_RIP] += 2; + } } uc->uc_mcontext.gregs[REG_RAX] = (unsigned long)-1; #endif @@ -876,8 +920,13 @@ static const char *startEmbeddedNode(const char *params) { * before the crash instead of an unexplained SIGSEGV inside node::Start. */ { + unsigned char iouParams[128]; + memset(iouParams, 0, sizeof(iouParams)); + /* Log BEFORE the call too: if the trap handling kills us, "calling" + * is the marker that proves the syscall is where we died. */ + logWrite("[embed] io_uring preflight: calling syscall(425)"); errno = 0; - long rc = syscall(425 /* __NR_io_uring_setup */, 8, (void *)0); + long rc = syscall(425 /* __NR_io_uring_setup */, 8, iouParams); int preErrno = errno; logWrite("[embed] io_uring preflight: rc=%ld errno=%d (%s)", rc, preErrno, rc == -1 ? strerror(preErrno) : "not trapped"); diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index d6369fc..a0465ff 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -443,21 +443,56 @@ static void safeAppendInt(char *b, size_t cap, size_t *n, int v) { } } +/* Is there an aarch64 `svc #0` (encoded 0xd4000001) at `pc`? + * + * Do NOT decide this by comparing the signal frame's PC with si_addr: both + * come from the same pt_regs, so they agree whether or not the kernel has + * already advanced past the trapped instruction, and skipping an + * already-advanced PC resumes mid-stream. Read the encoding instead. + * (Same fix as node_ctl.c — the in-process path hit exactly this.) */ +static int pcIsSvcInsn(unsigned long pc) { + if (pc == 0 || (pc & 3U) != 0) { + return 0; + } + unsigned int insn = 0; + memcpy(&insn, (const void *)pc, sizeof(insn)); + return (insn & 0xffe0001fu) == 0xd4000001u; +} + static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { - (void)sig; static unsigned int seenBits[16]; /* 512 syscall numbers, logged once each */ + + /* Only emulate a real seccomp trap: a SIGSYS from raise()/kill() has no + * syscall context, and rewriting the register file for it corrupts + * whatever thread happened to be running. */ + if (!si || !ctx || si->si_code != 1 /* SYS_SECCOMP */) { + signal(sig, SIG_DFL); + raise(sig); + return; + } + int sc = si->si_syscall; /* musl: #define si_syscall __si_fields.__sigsys.si_syscall */ if (sc >= 0 && sc < 512) { unsigned int bit = 1u << (sc & 31); if (!(seenBits[sc >> 5] & bit)) { seenBits[sc >> 5] |= bit; - char b[96]; + char b[160]; size_t n = 0; + /* safeAppend/safeAppendInt only — NO logWrite(). logWrite() is + * vsnprintf + hilog IPC; both take libc locks, and calling them from + * this handler is device-proven to kill the trapped thread (the + * in-process path died with SIGSEGV addr=0x0 that way). */ safeAppend(b, sizeof(b), &n, "[launcher] SIGSYS: syscall "); safeAppendInt(b, sizeof(b), &n, sc); safeAppend(b, sizeof(b), &n, " ("); safeAppend(b, sizeof(b), &n, syscallName(sc)); - safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1\n"); + safeAppend(b, sizeof(b), &n, ") blocked by seccomp -> -1"); +#if defined(__aarch64__) + safeAppend(b, sizeof(b), &n, " onsvc="); + safeAppendInt(b, sizeof(b), &n, + pcIsSvcInsn(((ucontext_t *)ctx)->uc_mcontext.pc)); +#endif + safeAppend(b, sizeof(b), &n, "\n"); if (g_logFd >= 0) { ssize_t ign = write(g_logFd, b, n); (void)ign; @@ -469,18 +504,27 @@ static void sigsysHandler(int sig, siginfo_t *si, void *ctx) { } } ucontext_t *uc = (ucontext_t *)ctx; - /* aarch64: the trapped instruction is the 4-byte `svc #0`; skip it and - * put the failure value in x0 (the syscall return register). + /* Skip the trapped instruction ONLY if it really is the syscall, and put + * the failure value in x0 (the syscall return register). * Return EXACTLY -1, not -ENOSYS: OHOS musl's syscall() passes raw x0 * through without __syscall_ret errno-translation, so -38 leaks out as a * bogus value — device-proven fatal in libuv uv__iou_init(): ringfd=-38 * passed its `== -1` guard, mmap/epoll_ctl failed, cleanup called * uv__close(-38) → assert(fd > STDERR_FILENO) → abort. */ #if defined(__aarch64__) - uc->uc_mcontext.pc += 4; + if (pcIsSvcInsn(uc->uc_mcontext.pc)) { + uc->uc_mcontext.pc += 4; + } uc->uc_mcontext.regs[0] = (unsigned long)-1; #elif defined(__x86_64__) - uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + unsigned long pc = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; + if (pc != 0) { + unsigned char c[2] = {0, 0}; + memcpy(c, (const void *)pc, 2); + if (c[0] == 0x0fu && c[1] == 0x05u) { + uc->uc_mcontext.gregs[REG_RIP] += 2; /* skip 2-byte syscall */ + } + } uc->uc_mcontext.gregs[REG_RAX] = (unsigned long)-1; #endif errno = ENOSYS; /* TLS store — async-signal-safe; for errno-checking callers */ From d9fd1b5b0cd70128e5456aed58ec2881e747c390 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 08:02:04 +0800 Subject: [PATCH 36/52] CI: stop building the full HarmonyOS APP on dev2 dev2 only ships the web variant (build-web.yml). The 'Build HarmonyOS APP' workflow (build.yml) was also firing on every dev2 push; drop dev2 from its push branch list so a dev2 push triggers build-web only. --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c6cd95..0ddb1e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,9 @@ on: - build - dev - dev1 - - dev2 + # NOTE: dev2 intentionally excluded — that branch only builds the + # web variant (build-web.yml). Keeping it here would fire the full + # HarmonyOS APP build on every dev2 push, which we don't want. # Cancel previous runs on the same branch/tag concurrency: From 059d5bcb8c3c39e38ceb0a473f31acd8f36e3835 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 08:07:50 +0800 Subject: [PATCH 37/52] Fix CI --- .github/workflows/build-web.yml | 3 +-- scripts/build-web-app.sh | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index d113b21..343040a 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -251,10 +251,9 @@ jobs: exit 1 fi APP_NAME=$(basename "${APP_FILE}") - ARTIFACT_NAME="${APP_NAME%.app}" echo "app_path=${APP_FILE}" >> $GITHUB_OUTPUT echo "app_name=${APP_NAME}" >> $GITHUB_OUTPUT - echo "artifact_name=${ARTIFACT_NAME}-web" >> $GITHUB_OUTPUT + echo "artifact_name=${APP_NAME}" >> $GITHUB_OUTPUT echo "Found APP: ${APP_FILE} ($(du -h ${APP_FILE} | cut -f1))" - name: Upload APP artifact diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 5b248fa..0d62311 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -406,9 +406,12 @@ if [ ! -f "${SIGN_TOOL_JAR}" ]; then exit 1 fi -# electerm-harmony-default-unsigned.app -> electerm-harmony-default-signed.app -SIGNED_APP="${UNSIGNED_APP%.app}" -SIGNED_APP="${SIGNED_APP%-unsigned}-signed.app" +# Canonical web-build artifact name: electerm-harmony--.app +# The web build only targets the on-device architecture (arm64-v8a), and +# is the package.json version, so the shipped file is e.g. +# electerm-harmony-arm64-5.3.16.app +APP_ARCH="arm64" +CANONICAL_APP="${APP_OUTPUT_DIR}/electerm-harmony-${APP_ARCH}-${APP_VERSION}.app" java -jar "${SIGN_TOOL_JAR}" sign-app \ -mode localSign \ @@ -420,16 +423,16 @@ java -jar "${SIGN_TOOL_JAR}" sign-app \ -signAlg SHA256withECDSA \ -keystoreFile "${KEYSTORE_PATH}" \ -keystorePwd "${KEYSTORE_PASSWORD}" \ - -outFile "${SIGNED_APP}" + -outFile "${CANONICAL_APP}" -if [ ! -f "${SIGNED_APP}" ]; then +if [ ! -f "${CANONICAL_APP}" ]; then echo " ✗ Signing failed — no signed APP produced" exit 1 fi -# Keep only the signed APP (under a name that says so) so artifact -# pickup (find … -name '*.app') can never grab the unsigned one. -APP_FILE="${SIGNED_APP}" +# Keep only the canonical-named APP so artifact pickup (find … -name '*.app') +# can never grab a stray/unsigned one. +APP_FILE="${CANONICAL_APP}" rm -f "${UNSIGNED_APP}" echo " ✓ Signed APP: ${APP_FILE} ($(du -h "${APP_FILE}" | cut -f1))" From 3af592b50cd51afc95734c13f4d29b7fd32dad8a Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 12:09:18 +0800 Subject: [PATCH 38/52] Fix --- scripts/build-web-app.sh | 149 +++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 62 deletions(-) diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index 0d62311..eabbd94 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -113,6 +113,18 @@ for f in "${KEYSTORE_PATH}" "${CERT_PATH}" "${PROFILE_PATH}"; do done echo " ✓ Signing materials present" +# Informational (WARNING, non-fatal): note the cert identity. +# The same electerm_publish.cer is used by the main (electron) branch for +# `hap-sign-tool sign-app` and that branch builds fine, so for *HAP package +# signing* this cert is acceptable. The one place a root/CA cert is genuinely +# wrong — per-.so binary-sign-tool code-signing — is no longer done by this +# script (see the libnode.so code-signing section). Left as INFO only. +if command -v openssl >/dev/null 2>&1; then + CERT_SUBJECT=$(openssl x509 -in "${CERT_PATH}" -noout -subject 2>/dev/null || true) + echo " • App cert (appCertFile for hap-sign-tool): ${CERT_SUBJECT}" + echo " (Same cert the main/electron branch uses for HAP signing; fine for sign-app.)" +fi + # --- Fix permissions for SDK compatibility ---------------------------------- echo "==> Cleaning unsupported permissions ..." @@ -296,74 +308,87 @@ echo "==> Installing ohpm dependencies ..." cd "${PROJECT_ROOT}" "${OHPM}" install -# --- Code-sign libnode.so ----------------------------------------------------- -# HarmonyOS XPM only lets signed code execute on device — execv of the -# bundled node binary is refused with EACCES otherwise. binary-sign-tool -# (official tool, openharmony/developtools_hapsigner dist) embeds a code -# signature in the ELF. Cert mode reuses the same identity that signs the -# APP (set KEYSTORE_PASSWORD/KEY_PASSWORD — CI does); otherwise self-sign. -# NOTE: signs entry/libs/arm64-v8a/libnode.so IN PLACE — after a local -# build restore the pristine copy with: -# git checkout -- entry/libs/arm64-v8a/libnode.so - -echo "==> Code-signing libnode.so ..." - -BINSIGN_JAR="${BINSIGN_JAR:-${PROJECT_ROOT}/build/tools/binary-sign-tool.jar}" -BINSIGN_SHA256="d984474a09f6a1255ccde31f36e8a580be77aabd35b0ca2b3d94d1962ae3778d" -if [ ! -f "${BINSIGN_JAR}" ]; then - mkdir -p "$(dirname "${BINSIGN_JAR}")" - echo " Downloading binary-sign-tool.jar (developtools_hapsigner dist) ..." - curl -fsSL --retry 5 --retry-delay 3 -o "${BINSIGN_JAR}" \ - "https://raw.githubusercontent.com/openharmony/developtools_hapsigner/master/dist/binary-sign-tool.jar" -fi -BINSIGN_ACTUAL=$(shasum -a 256 "${BINSIGN_JAR}" | cut -d' ' -f1) -if [ "${BINSIGN_ACTUAL}" != "${BINSIGN_SHA256}" ]; then - echo " ✗ binary-sign-tool.jar checksum mismatch: ${BINSIGN_ACTUAL}" - exit 1 -fi -echo " ✓ binary-sign-tool.jar ready" - -NODE_LIB="${PROJECT_ROOT}/entry/libs/arm64-v8a/libnode.so" -# plain mktemp (no -t template) — GNU mktemp rejects -t templates without X's -NODE_LIB_SIGNED="$(mktemp).signed" +# --- Code-sign libnode.so? NO, by default. ---------------------------------- +# IMPORTANT (root-cause of the CI arm64 SIGTRAP crash): +# The bundled libnode.so is loaded *in-process* by the app via dlopen(). +# A shared library that is dlopen()'d needs a TLS layout compatible with +# the host app (dynamic/global-dynamic TLS). Running binary-sign-tool over +# the .so rewrites the ELF to embed a code-signature section and, on arm64, +# corrupts the TLS template — so when node::Start runs, V8's per-thread +# TLS reads garbage and the release CHECK `AllowHeapAllocationInRelease` +# fires at Isolate::Initialize (brk -> SIGTRAP, signal 5). The local +# (unsigned, x64 emulator) build never code-signs libnode.so and works; +# the CI build did, and crashed identically to the bug described in +# prepare-node.sh. So we do NOT code-sign the bundled .so here. +# +# For a properly *app-signed* HAP (hap-sign-tool below), the package +# signature grants its bundled native libs execution permission — XPM does +# not require a separate per-.so binary-sign-tool signature. The reference +# 5.3.15 signed HAP that proved the emulator path also ships an unsigned +# (at the .so level) libnode.so. +# +# Opt back in only if a specific device/enrollment truly requires it, and +# only with a VALID app code-signing cert (NOT a root CA — see the guard +# below). When enabled, restore the pristine copy afterwards with: +# git checkout -- entry/libs/arm64-v8a/libnode.so NODE_SIGNED=0 - -if [ -n "${KEYSTORE_PASSWORD:-}" ] && [ -n "${KEY_PASSWORD:-}" ]; then - echo " Signing with the APP certificate ..." - if java -jar "${BINSIGN_JAR}" sign \ - -keyAlias "${KEY_ALIAS}" \ - -keyPwd "${KEY_PASSWORD}" \ - -appCertFile "${CERT_PATH}" \ - -inFile "${NODE_LIB}" \ - -signAlg SHA256withECDSA \ - -keystoreFile "${KEYSTORE_PATH}" \ - -keystorePwd "${KEYSTORE_PASSWORD}" \ - -outFile "${NODE_LIB_SIGNED}" >/dev/null 2>&1; then - mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" - NODE_SIGNED=1 - echo " ✓ libnode.so cert-signed (APP identity)" - else - echo " ⚠ cert-sign failed — falling back to self-sign" - rm -f "${NODE_LIB_SIGNED}" +if [ "${CODE_SIGN_NODE:-0}" = "1" ]; then + echo "==> Code-signing libnode.so (CODE_SIGN_NODE=1) ..." + + BINSIGN_JAR="${BINSIGN_JAR:-${PROJECT_ROOT}/build/tools/binary-sign-tool.jar}" + BINSIGN_SHA256="d984474a09f6a1255ccde31f36e8a580be77aabd35b0ca2b3d94d1962ae3778d" + if [ ! -f "${BINSIGN_JAR}" ]; then + mkdir -p "$(dirname "${BINSIGN_JAR}")" + echo " Downloading binary-sign-tool.jar (developtools_hapsigner dist) ..." + curl -fsSL --retry 5 --retry-delay 3 -o "${BINSIGN_JAR}" \ + "https://raw.githubusercontent.com/openharmony/developtools_hapsigner/master/dist/binary-sign-tool.jar" + fi + BINSIGN_ACTUAL=$(shasum -a 256 "${BINSIGN_JAR}" | cut -d' ' -f1) + if [ "${BINSIGN_ACTUAL}" != "${BINSIGN_SHA256}" ]; then + echo " ✗ binary-sign-tool.jar checksum mismatch: ${BINSIGN_ACTUAL}" + exit 1 + fi + echo " ✓ binary-sign-tool.jar ready" + + NODE_LIB="${PROJECT_ROOT}/entry/libs/arm64-v8a/libnode.so" + NODE_LIB_SIGNED="$(mktemp).signed" + + if [ -n "${KEYSTORE_PASSWORD:-}" ] && [ -n "${KEY_PASSWORD:-}" ]; then + echo " Signing with the APP certificate ..." + if java -jar "${BINSIGN_JAR}" sign \ + -keyAlias "${KEY_ALIAS}" \ + -keyPwd "${KEY_PASSWORD}" \ + -appCertFile "${CERT_PATH}" \ + -inFile "${NODE_LIB}" \ + -signAlg SHA256withECDSA \ + -keystoreFile "${KEYSTORE_PATH}" \ + -keystorePwd "${KEYSTORE_PASSWORD}" \ + -outFile "${NODE_LIB_SIGNED}" >/dev/null 2>&1; then + mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" + NODE_SIGNED=1 + echo " ✓ libnode.so cert-signed (APP identity)" + else + echo " ⚠ cert-sign failed — falling back to self-sign" + rm -f "${NODE_LIB_SIGNED}" + fi fi -fi -if [ "${NODE_SIGNED}" = "0" ]; then - if java -jar "${BINSIGN_JAR}" sign \ - -inFile "${NODE_LIB}" -outFile "${NODE_LIB_SIGNED}" \ - -selfSign 1 >/dev/null 2>&1; then - mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" - NODE_SIGNED=1 - echo " ✓ libnode.so self-signed" - else - echo " ⚠ self-sign failed — shipping unsigned (device may refuse to exec)" + if [ "${NODE_SIGNED}" = "0" ]; then + if java -jar "${BINSIGN_JAR}" sign \ + -inFile "${NODE_LIB}" -outFile "${NODE_LIB_SIGNED}" \ + -selfSign 1 >/dev/null 2>&1; then + mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" + NODE_SIGNED=1 + echo " ✓ libnode.so self-signed" + else + echo " ⚠ self-sign failed — shipping unsigned (device may refuse to exec)" + fi + rm -f "${NODE_LIB_SIGNED}" fi - rm -f "${NODE_LIB_SIGNED}" +else + echo "==> Skipping libnode.so code-signing (bundled .so is covered by the app signature; code-signing corrupts the arm64 TLS template and crashes node::Start)." fi -java -jar "${BINSIGN_JAR}" display-sign -inFile "${NODE_LIB}" 2>/dev/null \ - | grep -E 'INFO - (verify|code signature)' | sed 's/^/ /' || true - # --- Build the unsigned APP ------------------------------------------------- echo "==> Building unsigned APP (${BUILD_MODE}) ..." From 20ab978d673e469f68dcad0f1e9d98532d7cd561 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 12:29:11 +0800 Subject: [PATCH 39/52] Fix 1 --- entry/src/main/cpp/node_ctl.c | 26 ++++++++++++++++++-------- entry/src/main/cpp/node_launcher.c | 23 +++++++++++++++-------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index e2d43b0..a9543d4 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -886,20 +886,28 @@ static const char *startEmbeddedNode(const char *params) { } logWrite("[embed] node::Start resolved at %p", (void *)start); - /* argv must outlive the thread — static storage. Include V8 flags - * to bypass the AllowHeapAllocationInRelease assertion that fires - * during Isolate::Initialize when the (missing) snapshot path tries - * to allocate on the heap. */ + /* argv must outlive the thread — static storage. V8 flags: + * - --no-verify-heap : bypasses the AllowHeapAllocationInRelease assertion + * that fires during Isolate::Initialize. + * - --jitless : disable the JIT so V8 never maps executable + * (PROT_EXEC) pages at runtime. On OpenHarmony the kernel/W^X policy + * rejects mprotect(PROT_EXEC) with EPERM, and V8's OS::SetPermissions + * asserts `CHECK_EQ(ENOMEM, errno)` on that failure, so node::Start + * aborts with "# Check failed: 12 == (*__errno_location())". --jitless + * removes the runtime executable mapping entirely. (Verified a valid + * node v24 flag; .so is a true shared lib so no snapshot/PIE issue.) */ static char arg0[MAX_LINE * 2]; static char arg1[] = "--no-verify-heap"; - static char arg2[MAX_LINE * 2]; + static char arg2[] = "--jitless"; + static char arg3[MAX_LINE * 2]; snprintf(arg0, sizeof(arg0), "%s", nodePath); - snprintf(arg2, sizeof(arg2), "%s", cfg.script); + snprintf(arg3, sizeof(arg3), "%s", cfg.script); g_nodeArgs.start = start; g_nodeArgs.argv[0] = arg0; g_nodeArgs.argv[1] = arg1; g_nodeArgs.argv[2] = arg2; - g_nodeArgs.argv[3] = NULL; + g_nodeArgs.argv[3] = arg3; + g_nodeArgs.argv[4] = NULL; /* Run node::Start on THIS thread — we are already the detached bootstrap * thread with a 32MB stack, so there is no reason to hand off again. @@ -935,7 +943,9 @@ static const char *startEmbeddedNode(const char *params) { g_nodeTid = (pid_t)syscall(__NR_gettid); setStatus(ST_RUNNING, "node::Start"); logWrite("[embed] bootstrap tid=%ld, calling node::Start", (long)g_nodeTid); - int rc = start(3, g_nodeArgs.argv); + errno = 0; /* clear any stale errno left by the io_uring preflight so a + * later CHECK_EQ(ENOMEM, errno) sees the real failure, not EPERM */ + int rc = start(4, g_nodeArgs.argv); logWrite("[embed] node::Start returned %d (backend stopped)", rc); snprintf(errBuf, sizeof(errBuf), "err:node::Start returned %d", rc); setStatus(ST_FAILED, errBuf); diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index a0465ff..5343230 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -542,13 +542,13 @@ static void installSigsysShim(void) { struct NodeThreadArgs { node_start_fn start; - char *argv[5]; /* node binary, V8 flags, script, NULL */ + char *argv[7]; /* node binary, V8 flags (up to 4), script, NULL */ int rc; }; static void *nodeThreadMain(void *p) { struct NodeThreadArgs *a = (struct NodeThreadArgs *)p; - a->rc = a->start(4, a->argv); + a->rc = a->start(5, a->argv); logWrite("[launcher] node::Start returned %d", a->rc); _exit(a->rc & 0xff); return NULL; /* unreachable */ @@ -577,14 +577,20 @@ static int runNodeInProcess(const char *nodePath, const char *script) { * instead of a SIGSYS thread kill. */ installSigsysShim(); - /* argv must outlive the thread — static storage. Include V8 flags - * to bypass the AllowHeapAllocationInRelease assertion that fires - * during Isolate::Initialize when the (missing) snapshot path tries - * to allocate on the heap. */ + /* argv must outlive the thread — static storage. V8 flags: + * - --no-verify-heap : bypasses the AllowHeapAllocationInRelease assertion. + * - --no-snap : skip snapshot load (no embedded snapshot in our build). + * - --jitless : disable the JIT so V8 never maps executable + * (PROT_EXEC) pages at runtime. OpenHarmony's W^X/kernel policy rejects + * mprotect(PROT_EXEC) with EPERM, and V8's OS::SetPermissions asserts + * `CHECK_EQ(ENOMEM, errno)` on that failure -> node::Start aborts with + * "# Check failed: 12 == (*__errno_location())". --jitless removes the + * runtime executable mapping entirely. (Verified valid node v24 flag.) */ static char arg0[MAX_LINE * 2]; static char arg1[MAX_LINE * 2]; static char argFlag1[] = "--no-verify-heap"; static char argFlag2[] = "--no-snap"; + static char argFlag3[] = "--jitless"; snprintf(arg0, sizeof(arg0), "%s", nodePath); snprintf(arg1, sizeof(arg1), "%s", script); @@ -593,8 +599,9 @@ static int runNodeInProcess(const char *nodePath, const char *script) { na.argv[0] = arg0; na.argv[1] = argFlag1; na.argv[2] = argFlag2; - na.argv[3] = arg1; - na.argv[4] = NULL; + na.argv[3] = argFlag3; + na.argv[4] = arg1; + na.argv[5] = NULL; na.rc = -1; pthread_attr_t attr; From 7a75abcea179962aad48f21d5ae5c12e154c8df0 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 13:01:08 +0800 Subject: [PATCH 40/52] Remove dead code --- entry/src/main/cpp/node_ctl.c | 22 +++--- entry/src/main/cpp/node_launcher.c | 14 ++-- scripts/build-web-app.sh | 107 ++++------------------------- 3 files changed, 27 insertions(+), 116 deletions(-) diff --git a/entry/src/main/cpp/node_ctl.c b/entry/src/main/cpp/node_ctl.c index a9543d4..5b01f9c 100644 --- a/entry/src/main/cpp/node_ctl.c +++ b/entry/src/main/cpp/node_ctl.c @@ -769,10 +769,6 @@ static const char *startEmbeddedNode(const char *params) { if (cfg.secret[0]) { setenv("SERVER_SECRET", cfg.secret, 1); } - /* V8 release-mode assert (AllowHeapAllocationInRelease) fires during - * Isolate::Initialize in the cross-compiled build. We cannot pass V8 - * flags via NODE_OPTIONS (rejected) or argv ("bad option"). The fix - * must be applied at Node.js build time (configure flags). */ for (int i = 0; i < extraEnvCount; i++) { /* setenv() COPIES the value. putenv() would store a pointer into * `extraEnv`, a stack array of THIS frame — and since node now runs on @@ -887,15 +883,15 @@ static const char *startEmbeddedNode(const char *params) { logWrite("[embed] node::Start resolved at %p", (void *)start); /* argv must outlive the thread — static storage. V8 flags: - * - --no-verify-heap : bypasses the AllowHeapAllocationInRelease assertion - * that fires during Isolate::Initialize. - * - --jitless : disable the JIT so V8 never maps executable - * (PROT_EXEC) pages at runtime. On OpenHarmony the kernel/W^X policy - * rejects mprotect(PROT_EXEC) with EPERM, and V8's OS::SetPermissions - * asserts `CHECK_EQ(ENOMEM, errno)` on that failure, so node::Start - * aborts with "# Check failed: 12 == (*__errno_location())". --jitless - * removes the runtime executable mapping entirely. (Verified a valid - * node v24 flag; .so is a true shared lib so no snapshot/PIE issue.) */ + * - --jitless : THE fix for the OpenHarmony W^X policy. V8 never + * maps executable (PROT_EXEC) pages at runtime, so no + * mprotect(PROT_EXEC)/EPERM, and V8's OS::SetPermissions no longer + * hits `CHECK_EQ(ENOMEM, errno)` -> the "# Check failed: 12 ==" + * "(*__errno_location())" SIGTRAP/abort in node::Start. Node runs as a + * pure interpreter; fine for an on-device backend service. + * - --no-verify-heap : disables V8 heap verification on startup + * (defensive — avoids allocation checks that can fail under the + * constrained runtime). Harmless no-op when the heap is healthy. */ static char arg0[MAX_LINE * 2]; static char arg1[] = "--no-verify-heap"; static char arg2[] = "--jitless"; diff --git a/entry/src/main/cpp/node_launcher.c b/entry/src/main/cpp/node_launcher.c index 5343230..9fd37c7 100644 --- a/entry/src/main/cpp/node_launcher.c +++ b/entry/src/main/cpp/node_launcher.c @@ -578,14 +578,12 @@ static int runNodeInProcess(const char *nodePath, const char *script) { installSigsysShim(); /* argv must outlive the thread — static storage. V8 flags: - * - --no-verify-heap : bypasses the AllowHeapAllocationInRelease assertion. - * - --no-snap : skip snapshot load (no embedded snapshot in our build). - * - --jitless : disable the JIT so V8 never maps executable - * (PROT_EXEC) pages at runtime. OpenHarmony's W^X/kernel policy rejects - * mprotect(PROT_EXEC) with EPERM, and V8's OS::SetPermissions asserts - * `CHECK_EQ(ENOMEM, errno)` on that failure -> node::Start aborts with - * "# Check failed: 12 == (*__errno_location())". --jitless removes the - * runtime executable mapping entirely. (Verified valid node v24 flag.) */ + * - --jitless : THE fix for the OpenHarmony W^X policy — V8 never + * maps PROT_EXEC pages, so no mprotect(PROT_EXEC)/EPERM and no + * `CHECK_EQ(ENOMEM, errno)` abort in node::Start. + * - --no-snap : skip embedded-snapshot load (our build ships none). + * - --no-verify-heap : disables V8 heap verification on startup + * (defensive; harmless when the heap is healthy). */ static char arg0[MAX_LINE * 2]; static char arg1[MAX_LINE * 2]; static char argFlag1[] = "--no-verify-heap"; diff --git a/scripts/build-web-app.sh b/scripts/build-web-app.sh index eabbd94..33eb7de 100755 --- a/scripts/build-web-app.sh +++ b/scripts/build-web-app.sh @@ -113,12 +113,12 @@ for f in "${KEYSTORE_PATH}" "${CERT_PATH}" "${PROFILE_PATH}"; do done echo " ✓ Signing materials present" -# Informational (WARNING, non-fatal): note the cert identity. +# Informational (non-fatal): note the cert identity. # The same electerm_publish.cer is used by the main (electron) branch for # `hap-sign-tool sign-app` and that branch builds fine, so for *HAP package -# signing* this cert is acceptable. The one place a root/CA cert is genuinely -# wrong — per-.so binary-sign-tool code-signing — is no longer done by this -# script (see the libnode.so code-signing section). Left as INFO only. +# signing* this cert is acceptable. This script does not perform any per-.so +# binary-sign-tool code-signing, so the root/CA nature of the cert is not a +# problem here. Shown for visibility only. if command -v openssl >/dev/null 2>&1; then CERT_SUBJECT=$(openssl x509 -in "${CERT_PATH}" -noout -subject 2>/dev/null || true) echo " • App cert (appCertFile for hap-sign-tool): ${CERT_SUBJECT}" @@ -308,86 +308,14 @@ echo "==> Installing ohpm dependencies ..." cd "${PROJECT_ROOT}" "${OHPM}" install -# --- Code-sign libnode.so? NO, by default. ---------------------------------- -# IMPORTANT (root-cause of the CI arm64 SIGTRAP crash): -# The bundled libnode.so is loaded *in-process* by the app via dlopen(). -# A shared library that is dlopen()'d needs a TLS layout compatible with -# the host app (dynamic/global-dynamic TLS). Running binary-sign-tool over -# the .so rewrites the ELF to embed a code-signature section and, on arm64, -# corrupts the TLS template — so when node::Start runs, V8's per-thread -# TLS reads garbage and the release CHECK `AllowHeapAllocationInRelease` -# fires at Isolate::Initialize (brk -> SIGTRAP, signal 5). The local -# (unsigned, x64 emulator) build never code-signs libnode.so and works; -# the CI build did, and crashed identically to the bug described in -# prepare-node.sh. So we do NOT code-sign the bundled .so here. -# -# For a properly *app-signed* HAP (hap-sign-tool below), the package -# signature grants its bundled native libs execution permission — XPM does -# not require a separate per-.so binary-sign-tool signature. The reference -# 5.3.15 signed HAP that proved the emulator path also ships an unsigned -# (at the .so level) libnode.so. -# -# Opt back in only if a specific device/enrollment truly requires it, and -# only with a VALID app code-signing cert (NOT a root CA — see the guard -# below). When enabled, restore the pristine copy afterwards with: -# git checkout -- entry/libs/arm64-v8a/libnode.so -NODE_SIGNED=0 -if [ "${CODE_SIGN_NODE:-0}" = "1" ]; then - echo "==> Code-signing libnode.so (CODE_SIGN_NODE=1) ..." - - BINSIGN_JAR="${BINSIGN_JAR:-${PROJECT_ROOT}/build/tools/binary-sign-tool.jar}" - BINSIGN_SHA256="d984474a09f6a1255ccde31f36e8a580be77aabd35b0ca2b3d94d1962ae3778d" - if [ ! -f "${BINSIGN_JAR}" ]; then - mkdir -p "$(dirname "${BINSIGN_JAR}")" - echo " Downloading binary-sign-tool.jar (developtools_hapsigner dist) ..." - curl -fsSL --retry 5 --retry-delay 3 -o "${BINSIGN_JAR}" \ - "https://raw.githubusercontent.com/openharmony/developtools_hapsigner/master/dist/binary-sign-tool.jar" - fi - BINSIGN_ACTUAL=$(shasum -a 256 "${BINSIGN_JAR}" | cut -d' ' -f1) - if [ "${BINSIGN_ACTUAL}" != "${BINSIGN_SHA256}" ]; then - echo " ✗ binary-sign-tool.jar checksum mismatch: ${BINSIGN_ACTUAL}" - exit 1 - fi - echo " ✓ binary-sign-tool.jar ready" - - NODE_LIB="${PROJECT_ROOT}/entry/libs/arm64-v8a/libnode.so" - NODE_LIB_SIGNED="$(mktemp).signed" - - if [ -n "${KEYSTORE_PASSWORD:-}" ] && [ -n "${KEY_PASSWORD:-}" ]; then - echo " Signing with the APP certificate ..." - if java -jar "${BINSIGN_JAR}" sign \ - -keyAlias "${KEY_ALIAS}" \ - -keyPwd "${KEY_PASSWORD}" \ - -appCertFile "${CERT_PATH}" \ - -inFile "${NODE_LIB}" \ - -signAlg SHA256withECDSA \ - -keystoreFile "${KEYSTORE_PATH}" \ - -keystorePwd "${KEYSTORE_PASSWORD}" \ - -outFile "${NODE_LIB_SIGNED}" >/dev/null 2>&1; then - mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" - NODE_SIGNED=1 - echo " ✓ libnode.so cert-signed (APP identity)" - else - echo " ⚠ cert-sign failed — falling back to self-sign" - rm -f "${NODE_LIB_SIGNED}" - fi - fi - - if [ "${NODE_SIGNED}" = "0" ]; then - if java -jar "${BINSIGN_JAR}" sign \ - -inFile "${NODE_LIB}" -outFile "${NODE_LIB_SIGNED}" \ - -selfSign 1 >/dev/null 2>&1; then - mv -f "${NODE_LIB_SIGNED}" "${NODE_LIB}" - NODE_SIGNED=1 - echo " ✓ libnode.so self-signed" - else - echo " ⚠ self-sign failed — shipping unsigned (device may refuse to exec)" - fi - rm -f "${NODE_LIB_SIGNED}" - fi -else - echo "==> Skipping libnode.so code-signing (bundled .so is covered by the app signature; code-signing corrupts the arm64 TLS template and crashes node::Start)." -fi +# --- Bundled libnode.so is NOT code-signed here. ------------------------- +# The app HAP signature (hap-sign-tool below) already grants its bundled +# native libs execution permission, so XPM does not require a separate +# per-.so binary-sign-tool signature. The previous arm64 SIGTRAP in +# node::Start ("# Check failed: 12 == (*__errno_location())") was a W^X +# policy rejection of the JIT's runtime PROT_EXEC mapping - fixed at +# runtime by launching node with --jitless in node_ctl.c / node_launcher.c. +# No .so code-signing is performed. # --- Build the unsigned APP ------------------------------------------------- @@ -490,17 +418,6 @@ check_file() { check_file "${HAP_DIR}/libs/arm64-v8a/libnode.so" "libs/arm64-v8a/libnode.so" -# The code signature must survive packaging (hvigor strip could drop the -# non-alloc .codesign section — entry/build-profile.json5 disables strip). -if [ "${NODE_SIGNED}" = "1" ]; then - if java -jar "${BINSIGN_JAR}" display-sign \ - -inFile "${HAP_DIR}/libs/arm64-v8a/libnode.so" 2>/dev/null \ - | grep -q "code signature is not found"; then - ERRORS="${ERRORS}\n ✗ libnode.so code signature lost during packaging" - else - echo " ✓ libnode.so code signature present in packed HAP" - fi -fi check_file "${HAP_DIR}/libs/arm64-v8a/libnode_launcher.so" "libs/arm64-v8a/libnode_launcher.so" check_file "${HAP_DIR}/libs/arm64-v8a/libnode_ctl.so" "libs/arm64-v8a/libnode_ctl.so" check_file "${HAP_DIR}/resources/resfile/electerm/index.js" "resfile/electerm/index.js" From 9d080dd3c7d42930d8a3bcbb009be6af3499651f Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 13:32:22 +0800 Subject: [PATCH 41/52] Update readme --- README.md | 33 +++--- README.zh-CN.md | 16 ++- docs/ARCHITECTURE.md | 225 +++++++++++++++++++-------------------- docs/BUILD.md | 226 ++++++++++++++++++---------------------- docs/ENV_SETUP.md | 37 +++---- scripts/prepare-node.sh | 6 +- 6 files changed, 263 insertions(+), 280 deletions(-) diff --git a/README.md b/README.md index af858bb..89434df 100644 --- a/README.md +++ b/README.md @@ -32,24 +32,21 @@ **electerm** is a free and open-sourced ssh/sftp/telnet/RDP/VNC/Spice/ftp client (linux, mac, win, HarmonyOS, Android, iOS). -This project brings electerm to **HarmonyOS** using the [Electron Harmony OS runtime](https://gitcode.com/openharmony-sig/electron) (Chromium + Node.js). - -> **`dev2` branch — web variant (experimental).** A second build path with no -> electron runtime at all, modelled on [electerm-android](https://github.com/electerm/electerm-android): -> the UI is an **ArkWeb** `Web` component and the electerm-web backend runs as -> an on-device **Node.js** process ([hqzing/ohos-node](https://github.com/hqzing/ohos-node) -> binary, started via `childProcessManager.startNativeChildProcess`). -> CI: `.github/workflows/build-web.yml` (push to `dev2`). -> -> ``` -> ArkWeb (frontend) ── http://127.0.0.1:5577 ──► Node.js backend (native child process) -> loads loading page serves UI + SSH/SFTP/telnet/ftp/RDP/VNC/Spice -> ``` -> -> The electerm app (frontend + backend bundle) is packaged in the HAP `resfile` -> and read directly by the node process; the node binary is packaged as -> `libs/arm64-v8a/libnode.so`. On-device boot diagnostics land in -> `/electerm-data/node-boot.log`. +This project brings electerm to **HarmonyOS** using a lightweight on-device runtime — **no Electron**: + +- **ArkWeb** ([`@kit.ArkWeb`](https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V5/arkweb-V5)) `Web` component renders the electerm-web frontend UI. +- An on-device **Node.js** backend serves the UI and runs the SSH/SFTP/telnet/ftp/RDP/VNC/Spice protocols. The Node.js runtime is a shared library (`libnode.so`) from [electerm/ohos-node-shared](https://github.com/electerm/ohos-node-shared), embedded in the HAP's native `libs` directory. + +The backend runs **in-process** in the main app process (the `libnode_ctl.so` NAPI module `dlopen`s `libnode.so` and calls `node::Start`), with a fallback to a **native child process** (`libnode_launcher.so`, launched via `childProcessManager.startNativeChildProcess`). + +``` +ArkWeb (frontend) ── http://127.0.0.1:5577 ──► Node.js backend (libnode.so) + Web component loads UI serves UI + SSH/SFTP/telnet/ftp/RDP/VNC/Spice +``` + +The electerm app (frontend + backend bundle) is packaged in the HAP `resfile/electerm` and read directly by the node process; the Node.js shared library is packaged as `libs/arm64-v8a/libnode.so`. On-device boot diagnostics land in `/electerm-data/node-boot.log`. + +> **Branch note.** This branch (`dev2`) is the Node.js + ArkWeb build. Other branches in this repo (`main`/`dev`/`dev1`) used the [Electron 鸿蒙 runtime](https://gitcode.com/openharmony-sig/electron); this branch does not. CI for this branch: `.github/workflows/build-web.yml` (push to `dev2`). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index fc25758..903a3d0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -35,7 +35,21 @@ **electerm** 是一个免费开源的 ssh/sftp/telnet/RDP/VNC/Spice/ftp 客户端(支持 Linux、Mac、Windows、HarmonyOS、Android、iOS)。 -本项目使用 [Electron 鸿蒙运行时](https://gitcode.com/openharmony-sig/electron)(Chromium + Node.js)将 electerm 移植到 **HarmonyOS** 平台。 +本项目使用一种轻量级的端侧运行时将 electerm 移植到 **HarmonyOS** —— **不再使用 Electron**: + +- **ArkWeb**(`@kit.ArkWeb` 的 `Web` 组件)负责渲染 electerm-web 前端界面。 +- 端侧 **Node.js** 后端负责提供界面并运行 SSH/SFTP/telnet/ftp/RDP/VNC/Spice 协议。Node.js 运行时(共享库 `libnode.so`)来自 [electerm/ohos-node-shared](https://github.com/electerm/ohos-node-shared),打包进 HAP 的原生 `libs` 目录。 + +后端默认在**主应用进程内**运行:由 `libnode_ctl.so` 这个 NAPI 模块 `dlopen` `libnode.so` 并调用 `node::Start`;若进程内启动失败,则回退为**原生子进程**(`libnode_launcher.so`,通过 `childProcessManager.startNativeChildProcess` 启动)。 + +``` +ArkWeb(前端)── http://127.0.0.1:5577 ──► Node.js 后端(libnode.so) + Web 组件加载界面 提供界面 + SSH/SFTP/telnet/ftp/RDP/VNC/Spice +``` + +electerm 应用(前端 + 后端打包产物)打包在 HAP 的 `resfile/electerm` 中,由 node 进程直接读取;Node.js 共享库打包为 `libs/arm64-v8a/libnode.so`。端侧启动诊断信息位于 `/electerm-data/node-boot.log`。 + +> **分支说明:** 本分支(`dev2`)为 Node.js + ArkWeb 构建。本仓库的其他分支(`main`/`dev`/`dev1`)曾使用 [Electron 鸿蒙运行时](https://gitcode.com/openharmony-sig/electron);本分支不再使用。本分支 CI:`.github/workflows/build-web.yml`(推送 `dev2` 触发)。 --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6ed72b1..593569a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,177 +1,178 @@ # Architecture — electerm-harmony -## 1. Overview +> **This branch (`dev2`) uses Node.js + ArkWeb. No Electron runtime is involved.** +> (Other branches in this repo — `main`/`dev`/`dev1` — used the Electron 鸿蒙 +> runtime. That does not apply here.) -electerm-harmony brings the [electerm-web](https://github.com/electerm/electerm-web) ssh/sftp/telnet/RDP/VNC/Spice/ftp client to HarmonyOS using the [Electron 鸿蒙 runtime](https://gitcode.com/openharmony-sig/electron). +## 1. Overview -The Electron 鸿蒙 runtime provides: -- **Node.js** — runs the electerm-web Express backend -- **Chromium** — renders the web UI via BrowserWindow -- **web_engine HAR module** — ArkTS API (WebAbility, WebWindow, JsBindingUtils) that bridges HarmonyOS UI with the Electron runtime +electerm-harmony brings the [electerm-web](https://github.com/electerm/electerm-web) ssh/sftp/telnet/RDP/VNC/Spice/ftp client to HarmonyOS using a lightweight on-device runtime: -This eliminates the need for custom process spawning, child_process shims, or HTTP polling between native and web layers. +- **ArkWeb** (`@kit.ArkWeb`) — the `Web` component renders the electerm-web frontend UI. +- **Node.js** (a shared library `libnode.so`) — runs the electerm-web Express/Node backend, which serves the UI and the SSH/SFTP/telnet/ftp/RDP/VNC/Spice protocol logic, all on `http://127.0.0.1:5577`. +- **Native glue** — small NAPI/C modules bridge ArkTS and the Node.js runtime. ``` -┌─────────────────── HarmonyOS App ───────────────────┐ -│ │ -│ ┌───────────────┐ ┌────────────────────────────┐ │ -│ │ ArkUI Shell │ │ Electron Runtime │ │ -│ │ (WebWindow) │ │ (libelectron.so) │ │ -│ │ │ │ │ │ -│ │ WebWindow │───►│ Node.js (main.js) │ │ -│ │ from │ │ ├── Express backend │ │ -│ │ web_engine │ │ │ (app.bundle.cjs) │ │ -│ │ │ │ └── BrowserWindow │ │ -│ │ │ │ (Chromium WebView) │ │ -│ └───────────────┘ └────────────────────────────┘ │ -│ │ -│ libadapter.so — bridges ArkTS ↔ Electron │ -│ (provided by web_engine HAR module) │ -└──────────────────────────────────────────────────────┘ +┌──────────────────────── HarmonyOS App ────────────────────────┐ +│ │ +│ ┌──────────────────┐ ┌─────────────────────────────┐ │ +│ │ ArkUI / ArkTS │ │ Native (same app process) │ │ +│ │ │ │ │ │ +│ │ pages/Index.ets │ │ libnode_ctl.so (NAPI) │ │ +│ │ ├─ Web (ArkWeb)│──http──│ startBackend() │ │ +│ │ │ src= │ 5577 │ └─ bootstrap thread │ │ +│ │ │ ://127.0.0.1│◄───────│ └─ dlopen(libnode.so)│ │ +│ │ │ :5577 │ │ └─ node::Start │ │ +│ │ └─ Boot overlay │ │ │ │ +│ │ │ │ libnode_launcher.so │ │ +│ │ │ │ (fallback child process) │ │ +│ └──────────────────┘ └─────────────────────────────┘ │ +│ │ +│ resfile/electerm/ ── read-only app bundle (frontend + │ +│ app.bundle.mjs backend) used by node │ +│ /electerm-data/ ── writable data dir (db, keys, │ +│ node-boot.log) │ +└────────────────────────────────────────────────────────────────┘ ``` +Key design points: + +- **No Electron, no Chromium-on-the-side.** The UI is a plain ArkWeb `Web` component; the protocol engine is Node.js running in-process. There is no BrowserWindow / web_engine HAR / libelectron.so. +- **In-process Node.js is the primary path.** The Electron-style pattern (Node core shipped as `.so` inside the app process) avoids the stricter seccomp filter that a spawned native child runs under — node's libuv dies in the child on event-loop syscalls. So node lives in the main process. +- **Native child process is a fallback only.** If in-process boot fails, the app falls back to `libnode_launcher.so:Main` via `childProcessManager.startNativeChildProcess`. + ## 2. Components -### 2.1 Electron 鸿蒙 Runtime (`openharmony-sig/electron`) +### 2.1 Node.js runtime — `libnode.so` (electerm/ohos-node-shared) -- **Repo**: -- **What it is**: A port of Electron (Chromium + Node.js) for HarmonyOS -- **Distribution**: Pre-built tarball (e.g. `electron40_hap_electron_v40.0.0_20260629.tar.gz`) -- **Tarball contents**: - - `web_engine/` — Complete HAR module (ArkTS source + resfile resources + libadapter.so type definitions) - - `electron/libs/arm64-v8a/*.so` — Native libraries: - - `libelectron.so` — Chromium + Node.js + V8 (the main runtime, ~175 MB) - - `libadapter.so` — ArkTS ↔ Electron bridge - - `libffmpeg.so` — Media codec support - - `libvk_swiftshader.so` — Vulkan software renderer - - `libc++_shared.so` — C++ standard library - - `vscode-sqlite3.node` — SQLite native module -- **In this project**: - - `web_engine/` is extracted to the project root (gitignored, downloaded at build time) - - `.so` files are extracted to `entry/libs/arm64-v8a/` (gitignored, downloaded at build time) +- **Repo**: +- **What it is**: a *real* shared library build of Node.js for OpenHarmony (built with `--shared`, so it is a PIC `.so` with dynamic TLS and a `libnode.so.` SONAME). It must **not** be the PIE executable form that some third-party builds ship — that form aliases the host TLS and crashes V8. +- **Version**: `24.2.0` (configured in `scripts/prepare-node.sh` and `.github/workflows/build-web.yml` as `NODE_VERSION`). The release tag is `ohos-node-shared-v${NODE_VERSION}`; the asset is `libnode-${arch}.so`. +- **Placement**: downloaded into `entry/libs//libnode.so` (e.g. `entry/libs/arm64-v8a/libnode.so`). hvigor packages it into the HAP's native libs dir, and the app `dlopen`s it from there at runtime. +- **Launch flags**: node is started with `--jitless --no-verify-heap`. `--jitless` avoids V8's runtime `PROT_EXEC` mapping, which the OpenHarmony W^X policy rejects (the old `# Check failed: 12 == (*__errno_location())` SIGTRAP in `node::Start`). `--no-verify-heap` is defensive against allocation checks under the constrained runtime. +- **io_uring**: node's libuv unconditionally probes `io_uring_setup` (425) at loop init; the sandbox seccomp-traps it. A SIGSYS shim in `libnode_ctl.c` (and `node_launcher.c`) converts the trap to a logged `-1` (exactly `-1`, not `-ENOSYS`) so libuv's guard passes. `UV_USE_IO_URING=0` is also set (though the getenv guard in this libuv build is not actually consulted). -### 2.2 web_engine HAR Module +### 2.2 Native glue -The `web_engine/` module is the core integration layer between HarmonyOS and Electron. It provides: +- **`entry/src/main/cpp/node_ctl.c` → `libnode_ctl.so`** (NAPI module for the main process): + - `startBackend(entryParams)` — spawns a detached bootstrap thread that `dlopen`s `libnode.so` and calls `node::Start`. `dlopen`/`dlsym`/`node::Start` are deliberately **not** on the ArkTS UI thread (doing them inline blocked the UI thread long enough to trip the `APP_INPUT_BLOCK` ANR watchdog). + - `getBackendStatus()` — returns `idle | launching: | running: | failed:` so the ArkTS page can detect a hard bootstrap failure in milliseconds and fall back to the child process. + - `killNode(pid)` — terminate the native-child-process backend. + - Also installs: a crash-marker signal handler (writes a backtrace + fault PC to `node-boot.log`/hilog and parks the node thread instead of dying), and the SIGSYS seccomp shim described above. + - Redirects fd 1/2 onto a 1 MB pipe drained by a reader thread into `node-boot.log` (filtering ArkWeb/Chromium noise) — keeping the pipe drained is what prevents Chromium's IO thread from blocking and wedging the `Web` component. +- **`entry/src/main/cpp/node_launcher.c` → `libnode_launcher.so`** (native child process entry, `Main`): + - Started by `childProcessManager.startNativeChildProcess('libnode_launcher.so:Main', { entryParams })`. Parses `key=value` params, locates `libnode.so`, redirects stdio to the boot log, and `execv`s node with the electerm entry script. Falls back to a `memfd` + `execveat` path if the lib dir is `noexec`. -- **`WebAbilityStage`** — Base class for `AbilityStage`, initializes the Electron native context -- **`WebAbility`** — Base class for `EntryAbility`, handles window creation and XComponent setup -- **`WebWindow`** — ArkUI component that hosts the XComponent surface for Electron -- **`JsBindingUtils`** — Utility class for managing native Electron contexts -- **resfile resources** — Chromium runtime resources (`icudtl.dat`, `.pak` files, `v8_context_snapshot.bin`, `locales/`) +### 2.3 ArkTS layer (`entry` module) -This module is **not modified** by this project — it's used as-is from the tarball. +- **`AbilityStage.ets`** — standard `AbilityStage` (no Electron `WebAbilityStage`). +- **`entryability/EntryAbility.ets`** — standard `UIAbility`; on `onDestroy()` it calls `BackendManager.killBackend()`. +- **`pages/Index.ets`** — the boot orchestrator: + 1. creates the writable data dir (`filesDir/electerm-data`, with an `el2` junction fallback); + 2. calls `startBackend()` (in-process primary, native child fallback); + 3. polls `http://127.0.0.1:5577` with plain HTTP until it answers; + 4. once ready, `controller.loadUrl(SERVER_URL)` swaps the `Web` component from the local `loading.html` to the backend. + - While booting or on failure, a native overlay (`Stack` over the `Web`) shows the status / last boot-log lines, so a stuck boot is diagnosable from the screen alone. +- **`BackendManager.ets`** — tracks how the backend runs (`inProcess` vs `pid`) for clean shutdown. -### 2.3 Web app source (bundled in project root) +### 2.4 Web app source (bundled in the HAP `resfile`) - **Repo**: -- **What it is**: Web-based ssh/sftp/telnet/RDP/VNC/Spice/ftp client -- **Build**: The source is in the project root (`src/`, `build/`) and built with: +- **Source**: in the project root (`src/`, `build/`), built with: - **Vite** — builds the React frontend → `dist/assets/` - - **esbuild** — bundles the Node.js backend → `app.bundle.cjs` (CJS format) -- **In this project**: Built output goes to `web_engine/src/main/resources/resfile/resources/app/` - -### 2.4 Electron Main Process (`main.js`) - -The Electron main process entry point, generated by `build/harmony/build.js`: - -1. Sets environment variables (HOST, PORT, SERVER_SECRET, etc.) -2. Starts the Express backend via `require('./app.bundle.cjs')` -3. Polls `http://127.0.0.1:5577` until the backend is ready -4. Creates a `BrowserWindow` that loads the frontend from the backend's HTTP server - -### 2.5 ArkTS Layer (entry module) - -- **`AbilityStage.ets`** — Extends `WebAbilityStage` from `web_engine`, which initializes the Electron native context -- **`EntryAbility.ets`** — Extends `WebAbility` from `web_engine`, handles UIAbility lifecycle -- **`pages/Index.ets`** — Uses the `WebWindow` component from `web_engine` to host the Electron surface + - **esbuild** — bundles the Node.js backend → `app.bundle.mjs` (ESM) +- **Entry**: `resfile/electerm/index.js` (reads the bundled `app.bundle.mjs`, serves the UI and the protocol API). +- **Placement**: copied to `entry/src/main/resources/resfile/electerm/` and packaged read-only into the HAP. The node process runs from there; it writes its mutable state to `/electerm-data/`. ## 3. Build Flow ``` ┌──────────────────────────────────────────────────────────────────┐ -│ Build Pipeline │ +│ Build Pipeline (web variant — no Electron runtime) │ ├──────────────────────────────────────────────────────────────────┤ │ │ -│ 1. prepare-electron-runtime.sh │ -│ ├── Extract tarball (downloaded or local file) │ -│ ├── Copy web_engine/ → project root │ -│ └── Copy .so files → entry/libs/arm64-v8a/ │ +│ 1. prepare-node.sh │ +│ └── Download libnode-.so from the ohos-node-shared │ +│ release → entry/libs//libnode.so (verified ELF is a │ +│ shared lib, NOT a PIE) │ │ │ │ 2. prepare-web.sh │ -│ ├── npm install (project root) │ +│ ├── npm install (project root) │ │ ├── build/harmony/build.js: │ -│ │ ├── npm run b (complete electerm build): │ -│ │ │ ├── clean (remove work/) │ -│ │ │ ├── compile (vite + copy + pug) → work/app/assets/ │ +│ │ ├── npm run b (complete electerm build): │ +│ │ │ ├── clean / compile (vite + copy + pug) → work/app/ │ │ │ │ └── prepare-file (src copy + deps install + cleanup) │ -│ │ ├── HarmonyOS delta: │ -│ │ │ ├── package.json main → bootstrap.js │ +│ │ ├── HarmonyOS delta: │ +│ │ │ ├── package.json main → index.js │ │ │ │ └── Remove native modules (node-pty, serialport, ...) │ -│ │ ├── Copy work/app → web_engine resfile │ -│ │ └── Verify critical files (index.html, JS, CSS, chunks) │ -│ └── Verify output → web_engine/.../resfile/resources/app/ │ +│ │ └── Copy work/app → resfile/electerm │ +│ └── Verify output → entry/src/main/resources/resfile/electerm │ │ │ -│ 3. build-app.sh │ -│ ├── Generate build-profile.json5 (entry + web_engine modules) │ +│ 3. build-web-app.sh │ +│ ├── Generate build-profile.json5 (entry module only) │ │ ├── ohpm install │ │ ├── hvigorw assembleApp (unsigned) │ -│ └── hap-sign-tool.jar sign-app (signed .app) │ +│ └── hap-sign-tool.jar sign-app → electerm-harmony-arm64-.app │ │ └──────────────────────────────────────────────────────────────────┘ ``` +Prepared artifacts inside the HAP: + +- `libs/arm64-v8a/libnode.so` — the Node.js runtime +- `libs/arm64-v8a/libnode_ctl.so` — NAPI glue (in-process) +- `libs/arm64-v8a/libnode_launcher.so` — native child fallback +- `resources/resfile/electerm/index.js`, `app.bundle.mjs`, `views/index.pug`, `dist/assets/...` — the web app + ## 4. Runtime Flow ``` App Launch │ ▼ -AbilityStage.onCreate() - │ WebAbilityStage initializes Electron native context +AbilityStage.onCreate() / EntryAbility.onWindowStageCreate() │ ▼ -EntryAbility.onWindowStageCreate() - │ WebAbility loads 'pages/Index' +Index.aboutToAppear() → startBackend() + │ try IN-PROCESS first: + │ libnode_ctl.startBackend() → background thread: + │ dlopen(libnode.so) → node::Start (V8 --jitless) + │ serves electerm on http://127.0.0.1:5577 │ - ▼ -Index.ets → WebWindow component - │ XComponent surface ready → Electron runtime starts + │ poll http://127.0.0.1:5577 (getBackendStatus consulted for + │ hard failures → fall back if 'failed:') │ - ▼ -Electron Runtime starts (libelectron.so) - │ - ├── Runs main.js (Node.js) - │ ├── Sets env vars - │ ├── require('./app.bundle.cjs') → starts Express on :5577 - │ └── Polls http://127.0.0.1:5577 + ▼ (if in-process fails) +childProcessManager.startNativeChildProcess('libnode_launcher.so:Main') + │ node runs as a separate native child process │ - └── Creates BrowserWindow (Chromium) - └── Loads http://127.0.0.1:5577 (electerm UI) + ▼ +Web component loadUrl("http://127.0.0.1:5577") ← UI appears ``` ## 5. Key Design Decisions | Decision | Rationale | |----------|-----------| -| Use Electron 鸿蒙 runtime instead of standalone ohos-node | Provides both Node.js and Chromium in one package; no need for custom process spawning or WebView bridging | -| Use web_engine HAR module as-is from tarball | Provides the complete ArkTS ↔ Electron integration layer; no need to maintain custom bridge code | -| CJS format for backend bundle | Electron's main process uses CommonJS `require()` | -| Download runtime at build time, not committed | The tarball is ~200 MB of binary/ArkTS artifacts that don't need modification | -| `resfile/` for app code | Directly accessible by the Electron runtime (no extraction needed, unlike `rawfile/`) | +| Use a shared `libnode.so` (ohos-node-shared) instead of an Electron runtime | Provides Node.js in-process without pulling in Chromium-as-a-second-runtime; the `Web` component already supplies Chromium for the UI | +| Run Node.js **in-process** (dlopen + `node::Start`) | The spawned native child runs under a stricter seccomp filter that kills libuv; the main process already runs libuv-class loops (NETSTACK/curl) | +| Keep a native **child-process fallback** | A hard in-process bootstrap failure (missing script / no libnode.so / dlopen failure) is detected fast and recovered without an ANR | +| `--jitless` Node.js | OpenHarmony W^X policy rejects V8's runtime `PROT_EXEC` mapping; jitless runs node as a pure interpreter | +| SIGSYS shim for `io_uring_setup` | libuv probes it unconditionally and the sandbox traps it; the shim returns exactly `-1` so the guard passes | +| `resfile/electerm` for app code | Directly readable by the node process; writable state goes to `filesDir/electerm-data` | +| Download `libnode.so` at build time, not committed | The binary is ~90–120 MB and is fetched from the ohos-node-shared release | ## 6. What's Committed vs. Downloaded | Path | Committed? | Description | |------|-----------|-------------| -| `entry/src/main/ets/` | Yes | Our ArkTS source (AbilityStage, EntryAbility, Index) | +| `entry/src/main/ets/` | Yes | Our ArkTS source (AbilityStage, EntryAbility, Index, BackendManager) | +| `entry/src/main/cpp/` | Yes | Native glue: `node_ctl.c`, `node_launcher.c`, `CMakeLists.txt` | | `entry/src/main/module.json5` | Yes | Module configuration with permissions | -| `entry/build-profile.json5` | Yes | Module build profile | -| `entry/oh-package.json5` | Yes | Depends on `web_engine` | | `src/`, `build/`, `package.json` | Yes | Web app source code | -| `scripts/` | Yes | Build scripts | +| `scripts/` | Yes | `prepare-node.sh`, `prepare-web.sh`, `build-web-app.sh`, … | | `docs/` | Yes | Documentation | | `AppScope/` | Yes | App-level config | -| `web_engine/` | **No** (downloaded) | HAR module from tarball (gitignored) | -| `entry/libs/` | **No** (downloaded) | .so libraries from tarball (gitignored) | -| `build-profile.json5` | **No** (generated) | Generated by `build-app.sh` (gitignored) | +| `entry/libs/` | **No** (downloaded) | `libnode.so` from the ohos-node-shared release (gitignored) | +| `entry/src/main/resources/resfile/electerm/` | **No** (generated) | Web app build output (gitignored) | +| `build-profile.json5` | **No** (generated) | Generated by `build-web-app.sh` (gitignored) | diff --git a/docs/BUILD.md b/docs/BUILD.md index 0c9a92e..e68b512 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -1,49 +1,43 @@ -# Build Guide — electerm-harmony +# Build Guide — electerm-harmony (web variant: ArkWeb + Node.js) -Complete instructions for building the electerm HarmonyOS app locally and on GitHub Actions. +Complete instructions for building the electerm HarmonyOS app on this branch +(`dev2`) locally and on GitHub Actions. This branch uses **no Electron +runtime** — it runs the electerm-web backend on an on-device Node.js shared +library (`libnode.so`) behind an ArkWeb `Web` component. --- ## 1. Architecture Overview -This project uses the **Electron 鸿蒙 runtime** (`openharmony-sig/electron`) to provide: +This branch uses a lightweight on-device runtime: -1. **Node.js runtime** — runs the electerm-web Express backend -2. **WebView** — Chromium-based `BrowserWindow` for the frontend UI -3. **web_engine HAR module** — ArkTS API layer (WebAbility, WebWindow, JsBindingUtils) +1. **ArkWeb (`Web` component)** — renders the electerm-web frontend UI. +2. **Node.js backend** — `libnode.so` (from `electerm/ohos-node-shared`) runs in-process (primary) or as a native child process (fallback), serving the UI + SSH/SFTP/telnet/ftp/RDP/VNC/Spice on `http://127.0.0.1:5577`. +3. **Native glue** — `libnode_ctl.so` (NAPI) and `libnode_launcher.so` (child fallback). The project structure at build time: ``` electerm-harmony/ ├── entry/ # Our HarmonyOS entry module (committed) -│ ├── libs/arm64-v8a/ # .so libraries (downloaded, gitignored) -│ │ ├── libelectron.so # Chromium + Node.js + V8 -│ │ ├── libadapter.so # HarmonyOS ↔ Electron adapter -│ │ ├── libffmpeg.so # Media codecs -│ │ └── ... +│ ├── libs/arm64-v8a/ # libnode.so (downloaded, gitignored) +│ │ ├── libnode.so # Node.js runtime (shared lib) +│ │ ├── libnode_ctl.so # NAPI: startBackend / getBackendStatus / killNode +│ │ └── libnode_launcher.so # native child-process fallback │ └── src/main/ │ ├── ets/ # ArkTS code (committed) -│ │ ├── AbilityStage.ets # Extends WebAbilityStage -│ │ ├── entryability/ # Extends WebAbility -│ │ └── pages/Index.ets # Uses WebWindow component +│ │ ├── AbilityStage.ets +│ │ ├── entryability/ # standard UIAbility +│ │ ├── BackendManager.ets # tracks in-process vs child-pid +│ │ └── pages/Index.ets # boot orchestrator (Web + backend) +│ ├── cpp/ # native glue (committed) │ ├── module.json5 # Module config with permissions -│ └── resources/base/ # Strings, colors, profiles -├── web_engine/ # HAR module from tarball (downloaded, gitignored) -│ ├── Index.ets # Module exports (WebAbility, WebWindow, etc.) -│ ├── oh-package.json5 -│ ├── build-profile.json5 -│ └── src/main/ -│ ├── ets/ # ArkTS source (WebAbility, adapters, etc.) -│ ├── cpp/types/libadapter/ # libadapter.so type declarations -│ └── resources/resfile/ # Electron runtime + app code -│ ├── icudtl.dat # ICU data -│ ├── *.pak # Chromium resource packs -│ ├── v8_context_snapshot.bin -│ ├── locales/ # Localization resources -│ └── resources/app/ # Electron app (built by prepare-web.sh) -│ ├── main.js # Electron main process -│ ├── app.bundle.cjs # electerm-web backend +│ └── resources/ +│ ├── base/ # Strings, colors, profiles +│ └── resfile/electerm/ # Web app build output (generated, gitignored) +│ ├── index.js # node entry (require app.bundle.mjs) +│ ├── app.bundle.mjs # esbuild-bundled backend (ESM) +│ ├── views/index.pug # Express view template │ └── dist/assets/ # Vite-built frontend ├── src/ # Web app source (committed) ├── build/ # Build scripts (committed) @@ -52,8 +46,11 @@ electerm-harmony/ │ └── vite/ # Vite config ├── package.json # Web app npm package (committed) ├── scripts/ # Build scripts (committed) +│ ├── prepare-node.sh # Download libnode.so +│ ├── prepare-web.sh # Build web app → resfile/electerm +│ └── build-web-app.sh # Build unsigned APP + sign ├── AppScope/app.json5 # App-level config (committed) -└── build-profile.json5 # Generated by build-app.sh (gitignored) +└── build-profile.json5 # Generated by build-web-app.sh (gitignored) ``` --- @@ -69,75 +66,39 @@ electerm-harmony/ | HarmonyOS Command Line Tools | 5.0.5.200+ | Provides `ohpm`, `hvigorw`, SDK, and `hap-sign-tool.jar` | | git | latest | | | Python 3 + make + C++ build tools | | For native node modules | -| **Electron 鸿蒙 runtime tarball** | | Pre-built tarball from openharmony-sig/electron (see §3) | +| **Node.js runtime** | `libnode.so` v24.2.0 | Downloaded automatically by `prepare-node.sh` from `electerm/ohos-node-shared` | ### 2.2 CI (GitHub Actions) - Runner: `ubuntu-latest` (Linux x64 — HarmonyOS Command Line Tools are x64-only) - JDK 21 (Temurin) - Node.js 24 -- `ELECTRON_RUNTIME_URL` secret — see §3.2 for the value to set +- `NODE_VERSION` env (default `24.2.0`) — must match `scripts/prepare-node.sh` +- Secrets listed in §5.1 --- -## 3. Obtaining the Electron 鸿蒙 Runtime - -The Electron 鸿蒙 runtime is built from the [openharmony-sig/electron](https://gitcode.com/openharmony-sig/electron) project. - -### 3.1 What the tarball contains - -The tarball (e.g. `electron40_hap_electron_v40.0.0_20260629.tar.gz`) extracts to a directory containing: - -- `web_engine/` — Complete HAR module with: - - ArkTS source code (WebAbility, WebWindow, JsBindingUtils, adapters) - - `resfile/` resources (icudtl.dat, .pak files, v8_context_snapshot.bin, locales/) - - `cpp/types/libadapter/` type declarations for libadapter.so -- `electron/libs/arm64-v8a/` — Native .so libraries: - - `libelectron.so` (~175 MB) — Chromium + Node.js + V8 - - `libadapter.so` — HarmonyOS ↔ Electron bridge - - `libffmpeg.so` — Media codecs - - `libvk_swiftshader.so` — Vulkan software renderer - - `libc++_shared.so` — C++ standard library - - `vscode-sqlite3.node` — SQLite native module - -### 3.2 For CI (GitHub Actions) - -The runtime tarball URL is stored as a GitHub secret (`ELECTRON_RUNTIME_URL`) to avoid -exposing the private hosting address in the repo. Set this in GitHub repo → **Settings → Secrets and variables → Actions**. +## 3. Obtaining the Node.js runtime (`libnode.so`) -> **Note:** Ask the project maintainer for the URL value — it is not committed to the repo. +The Node.js runtime is **not** committed. `scripts/prepare-node.sh` downloads it +from the [electerm/ohos-node-shared](https://github.com/electerm/ohos-node-shared) +GitHub release and installs it into the entry module's native libs dir. -The CI workflow reads this secret and passes it to `prepare-electron-runtime.sh`. - -### 3.3 Using the tarball for local builds - -Set one of these environment variables and run the prepare script: +- **Release tag**: `ohos-node-shared-v${NODE_VERSION}` (default `ohos-node-shared-v24.2.0`) +- **Asset**: `libnode-${arch}.so` (`arm64` / `x64`), placed as `entry/libs//libnode.so` +- The script **verifies the ELF is a true shared library** (ET_DYN without `PT_INTERP`), rejecting the broken PIE form that crashes V8. ```bash -# Option A: Use a local tarball file -export ELECTRON_RUNTIME_FILE=/path/to/electron40_hap_electron_v40.0.0_20260629.tar.gz -./scripts/prepare-electron-runtime.sh - -# Option B: Use an already-extracted directory -export ELECTRON_RUNTIME_DIR=/path/to/extracted/electron144_ohos_hap -./scripts/prepare-electron-runtime.sh -``` - -The script will: -1. Extract the tarball (if using `ELECTRON_RUNTIME_FILE`) -2. Copy `web_engine/` to the project root -3. Copy `.so` files to `entry/libs/arm64-v8a/` - -### 3.4 Using a custom URL (alternative) - -For local builds or alternative CI setups, you can also use a URL directly: +# Default arch = arm64, version = 24.2.0 +./scripts/prepare-node.sh -```bash -export ELECTRON_RUNTIME_URL=https://your-server.com/electron40_hap_electron_v40.0.0_20260629.tar.gz -./scripts/prepare-electron-runtime.sh +# Optional overrides +NODE_VERSION=24.2.0 ARCH=x64 ./scripts/prepare-node.sh ``` -The script will download the tarball, extract it, and install the files. +No manual URL or secret is required — the release is public. The `NODE_VERSION` +in `prepare-node.sh` and in `.github/workflows/build-web.yml` must stay in sync +(a mismatch → 404 on the asset download). --- @@ -161,23 +122,13 @@ signing/ └── electermRelease.p7b # Release provisioning profile ``` -### Step 3 — Prepare the Electron 鸿蒙 runtime +### Step 3 — Prepare the Node.js runtime ```bash -# Option A: From a local tarball file -export ELECTRON_RUNTIME_FILE=/path/to/electron40_hap_electron_v40.0.0_20260629.tar.gz -./scripts/prepare-electron-runtime.sh - -# Option B: From an extracted directory -export ELECTRON_RUNTIME_DIR=/path/to/extracted/electron144_ohos_hap -./scripts/prepare-electron-runtime.sh - -# Option C: From a URL (ask maintainer for the URL) -export ELECTRON_RUNTIME_URL= -./scripts/prepare-electron-runtime.sh +./scripts/prepare-node.sh ``` -This extracts `web_engine/` to the project root and `.so` libraries to `entry/libs/arm64-v8a/`. +This downloads `libnode.so` (v24.2.0) into `entry/libs/arm64-v8a/`. ### Step 4 — Build the web app @@ -185,12 +136,14 @@ This extracts `web_engine/` to the project root and `.so` libraries to `entry/li ./scripts/prepare-web.sh ``` -This installs dependencies in the project root, builds the frontend (Vite) and backend (esbuild CJS bundle), and copies the output into `web_engine/src/main/resources/resfile/resources/app/`. +This installs dependencies in the project root, builds the frontend (Vite) and +backend (esbuild ESM bundle), and copies the output into +`entry/src/main/resources/resfile/electerm/`. The build produces: -- `main.js` — Electron main process (starts backend, creates BrowserWindow) -- `app.bundle.cjs` — esbuild-bundled backend (CJS format) -- `package.json` — `{ name, version, main: "main.js" }` +- `index.js` — node entry point +- `app.bundle.mjs` — esbuild-bundled backend (ESM) +- `package.json` — `{ name, version, main: "index.js", type: "module" }` - `dist/assets/` — Vite-built frontend (JS, CSS, images) - `views/index.pug` — Express view template @@ -207,17 +160,20 @@ export KEY_ALIAS="electerm_key" # export OHOS_SDK_HOME=$COMMANDLINE_TOOLS/sdk # Build (release mode by default) -./scripts/build-app.sh --release +./scripts/build-web-app.sh --release # Or debug mode: -./scripts/build-app.sh --debug +./scripts/build-web-app.sh --debug ``` The signed APP is at: ``` -build/outputs/default/electerm-arm64-.app +build/outputs/default/electerm-harmony-arm64-.app ``` +> **Note:** the X86_64 build (emulator) uses the same script; the canonical +> shipped filename is always `electerm-harmony-arm64-.app`. + ### Step 6 — Install on device or upload to AGC **Upload to AppGallery Connect:** @@ -227,20 +183,19 @@ Upload the `.app` file directly in the AGC console. **Install on device:** ```bash -hdc install build/outputs/default/electerm-arm64-.app +hdc install build/outputs/default/electerm-harmony-arm64-.app ``` --- ## 5. CI Build (GitHub Actions) -The workflow is defined in [`.github/workflows/build.yml`](../.github/workflows/build.yml). +The workflow is defined in [`.github/workflows/build-web.yml`](../.github/workflows/build-web.yml). It triggers on pushes to `dev2`. ### 5.1 Required GitHub Secrets | Secret | Description | |--------|-------------| -| `ELECTRON_RUNTIME_URL` | Runtime tarball URL (see §3.2 for the value) | | `OHOS_CMDLINE_TOOLS_URL` | URL to download HarmonyOS Command Line Tools (~2 GB) | | `OHOS_KEYSTORE_B64` | Base64-encoded `.p12` keystore | | `OHOS_CERT_B64` | Base64-encoded `.cer` certificate | @@ -251,22 +206,25 @@ The workflow is defined in [`.github/workflows/build.yml`](../.github/workflows/ | `OHOS_BUNDLE_NAME` | App bundle name (must match AGC registration) | | `OHOS_SERVER_SECRET` | (Optional) Server secret for web app backend | +> The Node.js runtime is pulled from the **public** `electerm/ohos-node-shared` +> release (no secret needed). Its version is the `NODE_VERSION` workflow env +> (default `24.2.0`), which must match `scripts/prepare-node.sh`. + ### 5.2 What the workflow does ``` 1. Checkout electerm-harmony repo 2. Setup Node.js 24 + JDK 21 3. Install system dependencies - 4. Prepare Electron 鸿蒙 runtime (download tarball → web_engine/ + entry/libs/) - 5. Build web app (frontend + backend → web_engine/.../resfile/resources/app/) - 6. Download & extract HarmonyOS Command Line Tools (~2 GB) - 7. Configure ohpm registry - 8. Decode signing materials from GitHub Secrets - 9. Configure bundle name from secret -10. Build unsigned APP (hvigorw assembleApp) -11. Sign APP with hap-sign-tool.jar -12. Upload .app as GitHub Actions artifact -13. Write build summary + 4. Download & extract HarmonyOS Command Line Tools (~2 GB, cached) + 5. Prepare Node.js runtime (libnode.so via prepare-node.sh) + 6. Build web app (frontend + backend → resfile/electerm) + 7. Decode signing materials from GitHub Secrets + 8. Configure bundle name from secret + 9. Build unsigned APP (hvigorw assembleApp) +10. Sign APP with hap-sign-tool.jar → electerm-harmony-arm64-.app +11. Upload .app as GitHub Actions artifact +12. Write build summary ``` --- @@ -275,17 +233,17 @@ The workflow is defined in [`.github/workflows/build.yml`](../.github/workflows/ | Script | Purpose | |--------|---------| -| [`scripts/prepare-electron-runtime.sh`](../scripts/prepare-electron-runtime.sh) | Extract tarball → `web_engine/` + `entry/libs/arm64-v8a/` | -| [`scripts/prepare-web.sh`](../scripts/prepare-web.sh) | Build web app (Vite + esbuild) → `web_engine/.../resfile/resources/app/` | -| [`scripts/build-app.sh`](../scripts/build-app.sh) | Build unsigned APP, then sign it with `hap-sign-tool.jar` | +| [`scripts/prepare-node.sh`](../scripts/prepare-node.sh) | Download `libnode.so` (ohos-node-shared release) → `entry/libs//libnode.so` | +| [`scripts/prepare-web.sh`](../scripts/prepare-web.sh) | Build web app (Vite + esbuild) → `entry/.../resfile/electerm/` | +| [`scripts/build-web-app.sh`](../scripts/build-web-app.sh) | Build unsigned APP, then sign it with `hap-sign-tool.jar` | | [`scripts/gen-secrets.sh`](../scripts/gen-secrets.sh) | Generates GitHub Secrets values from `signing/` files | Run them in order for a local build: ```bash -./scripts/prepare-electron-runtime.sh +./scripts/prepare-node.sh ./scripts/prepare-web.sh -./scripts/build-app.sh +./scripts/build-web-app.sh ``` --- @@ -311,13 +269,13 @@ export PATH=$PATH:$COMMANDLINE_TOOLS/bin:$COMMANDLINE_TOOLS/hvigor/bin export OHOS_SDK_HOME=$COMMANDLINE_TOOLS/sdk ``` -### "web_engine/ not found" +### "Missing: entry/libs/arm64-v8a/libnode.so" -The `web_engine/` module is not committed to the repo. Run `./scripts/prepare-electron-runtime.sh` first to extract it from the tarball. +The Node.js runtime is not present. Run `./scripts/prepare-node.sh` first. -### "libelectron.so not found" +### "Missing: entry/src/main/resources/resfile/electerm/index.js" -The `.so` libraries are not committed to the repo. Run `./scripts/prepare-electron-runtime.sh` first to extract them from the tarball. +The web app has not been built. Run `./scripts/prepare-web.sh` first. ### electerm-web build fails with native module errors @@ -332,6 +290,22 @@ fnm install 24 fnm use 24 ``` +### Backend never answers (stuck on boot overlay) + +Pull `node-boot.log` from the device: + +```bash +hdc file recv /data/storage/el2/base/files/electerm-data/node-boot.log ./node-boot.log +``` + +The log shows the launch ladder (candidate → dlopen → dlsym → node::Start) for +both the in-process and child-process paths, plus any SIGSYS/SIGSEGV crash +markers. Watch hilog with: + +```bash +hdc hilog | grep -E 'electerm\.(Index|embed|launcher)' +``` + ### Signing fails: "keystore password was incorrect" This is a JDK version mismatch. See [ENV_SETUP.md §2.7](./ENV_SETUP.md#27-keystore-jdk-compatibility-if-keystore-was-created-with-jdk-22). diff --git a/docs/ENV_SETUP.md b/docs/ENV_SETUP.md index f2f5651..991d3a3 100644 --- a/docs/ENV_SETUP.md +++ b/docs/ENV_SETUP.md @@ -281,33 +281,30 @@ Or use the helper script (reads from `temp/.env` and `signing/`): | `OHOS_CMDLINE_TOOLS_URL` | download URL | HarmonyOS Command Line Tools download link (see section 5 below) | | `OHOS_SERVER_SECRET` | random string | Secret key for web app server (generate with `openssl rand -base64 32`) | -### 4.4 Electron 鸿蒙 Runtime +### 4.4 Node.js Runtime (ohos-node-shared) -The app uses the Electron 鸿蒙 runtime (from `openharmony-sig/electron`) for Node.js + WebView. -The pre-built runtime is distributed as a tarball. The URL is set as a GitHub secret to avoid -exposing the private hosting address in the workflow file. +This branch (`dev2`) runs the electerm-web backend on a **Node.js shared library** +(`libnode.so`) — **not** the Electron 鸿蒙 runtime. The Node.js runtime is +downloaded automatically at build time (locally via `scripts/prepare-node.sh`, +in CI via `build-web.yml`) from the **public** +[`electerm/ohos-node-shared`](https://github.com/electerm/ohos-node-shared) +GitHub release. No secret or private URL is required. -Set this secret in GitHub repo → **Settings → Secrets and variables → Actions**. +- **Release tag**: `ohos-node-shared-v${NODE_VERSION}` (default `v24.2.0`) +- **Asset**: `libnode-${arch}.so` → installed as `entry/libs//libnode.so` +- The version is controlled by the `NODE_VERSION` env in `build-web.yml` and the + default in `scripts/prepare-node.sh` (they must match, or the asset download + 404s). -> **Note:** Ask the project maintainer for the URL value — it is not committed to the repo. - -The tarball contains: -- `web_engine/` — Complete HAR module (ArkTS API + resfile resources) -- `electron/libs/arm64-v8a/*.so` — Native libraries - -| Secret Name | Required | Description | -|-------------|----------|-------------| -| `ELECTRON_RUNTIME_URL` | **Yes** | URL to download the pre-built Electron runtime tarball | - -See [BUILD.md §3](./BUILD.md#3-obtaining-the-electron-鸿蒙-runtime) for details. +See [BUILD.md §3](./BUILD.md#3-obtaining-the-nodejs-runtime-libnodeso) for details. ### 4.5 Workflow Environment Variables (not secrets) -These are defined in `.github/workflows/build.yml` under `env:` and can be changed without touching secrets: +These are defined in `.github/workflows/build-web.yml` under `env:` and can be changed without touching secrets: | Variable | Default | Description | |----------|---------|-------------| -| (none) | | Runtime version is controlled by the URL you provide | +| `NODE_VERSION` | `24.2.0` | Node.js runtime version (must match `scripts/prepare-node.sh`) | --- @@ -369,7 +366,7 @@ Before your first CI build, make sure you have: - [ ] `OHOS_APP_ID` - [ ] `OHOS_CMDLINE_TOOLS_URL` - [ ] `OHOS_SERVER_SECRET` - - [ ] `ELECTRON_RUNTIME_URL` (ask maintainer for the value) +- [ ] `NODE_VERSION` env in `build-web.yml` matches `scripts/prepare-node.sh` (`24.2.0`) - [ ] Workflow enabled under repo → **Actions** tab --- @@ -377,7 +374,7 @@ Before your first CI build, make sure you have: ## 7. Security Notes - **Never commit** `.p12`, `.cer`, `.p7b`, or passwords to the repository -- The `.gitignore` file excludes the `signing/` directory, `web_engine/`, `entry/libs/`, and `temp/` directory +- The `.gitignore` file excludes the `signing/` directory, `entry/libs/` (the downloaded `libnode.so`), `entry/src/main/resources/resfile/electerm/` (web app build output), `build-profile.json5`, and `temp/` directory - GitHub Secrets are encrypted and never exposed in logs - If a signing material is compromised, revoke it on AppGallery Connect and generate new ones - Use **release certificates** only for published builds; use **debug certificates** for testing diff --git a/scripts/prepare-node.sh b/scripts/prepare-node.sh index d16b09e..af2b9d5 100755 --- a/scripts/prepare-node.sh +++ b/scripts/prepare-node.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # prepare-node.sh — Download our own real shared libnode.so for OpenHarmony -# from the electerm/electerm-harmony GitHub release and install it into the +# from the electerm/ohos-node-shared GitHub release and install it into the # entry module as the prebuilt Node.js native "library". # # WHY NOT hqzing/ohos-node (the old source): @@ -24,7 +24,7 @@ # Environment variables: # NODE_VERSION — node.js version the release was built from (default 24.2.0) # RELEASE_TAG — override the GitHub release tag (default auto-derived) -# RELEASE_REPO — repo hosting the release (default electerm/electerm-harmony) +# RELEASE_REPO — repo hosting the release (default electerm/ohos-node-shared) set -euo pipefail # --- Config ----------------------------------------------------------------- @@ -40,7 +40,7 @@ case "${ARCH}" in esac NODE_VERSION="${NODE_VERSION:-24.2.0}" -RELEASE_REPO="${RELEASE_REPO:-electerm/electerm-harmony}" +RELEASE_REPO="${RELEASE_REPO:-electerm/ohos-node-shared}" RELEASE_TAG="${RELEASE_TAG:-ohos-node-shared-v${NODE_VERSION}}" ASSET_NAME="libnode-${ASSET_ARCH}.so" From 340e12321a3db7da7fe8b8ce3d441476d029bd85 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 16:21:29 +0800 Subject: [PATCH 42/52] USe xternal src --- build/bin/install.js | 82 +- src/app/app.js | 26 - src/app/common/bookmark-zod-schemas.js | 130 -- src/app/common/build-run-scripts.js | 6 - src/app/common/build-ssh-tunnel.js | 8 - src/app/common/config-default.js | 34 - src/app/common/constants.js | 26 - src/app/common/count-folder-data.js | 44 - .../common/create-session-log-file-path.js | 7 - src/app/common/default-setting.js | 84 -- src/app/common/fs-functions.js | 34 - src/app/common/get-json.js | 4 - src/app/common/is-ip.js | 16 - src/app/common/log.js | 84 -- src/app/common/pass-enc.js | 17 - src/app/common/runtime-constants.js | 38 - src/app/common/sanitize-filename.js | 73 - src/app/common/time.js | 12 - src/app/common/uid.js | 4 - src/app/common/version-compare.js | 25 - src/app/lib/ai.js | 241 ---- src/app/lib/build-proxy.js | 18 - src/app/lib/conf.js | 27 - src/app/lib/custom-require.js | 63 - src/app/lib/db.js | 18 - src/app/lib/enc.js | 119 -- src/app/lib/extensions.js | 17 - src/app/lib/fancy-console.js | 181 --- src/app/lib/font-list.js | 35 - src/app/lib/fs.js | 466 ------ src/app/lib/get-constants.js | 84 -- src/app/lib/global-state.js | 40 - src/app/lib/init.js | 47 - src/app/lib/install-src.js | 31 - src/app/lib/iterm-theme.js | 13 - src/app/lib/jwt.js | 33 - src/app/lib/login.js | 18 - src/app/lib/lookup.js | 28 - src/app/lib/npm.js | 74 - src/app/lib/proxy-agent.js | 23 - src/app/lib/run-sync.js | 103 -- src/app/lib/serial-port.js | 13 - src/app/lib/show-item-in-folder.js | 39 - src/app/lib/sqlite.js | 145 -- src/app/lib/ssh-config.js | 8 - src/app/lib/system-ca.js | 137 -- src/app/lib/user-config.js | 26 - src/app/lib/view.js | 81 -- src/app/lib/watch-file.js | 40 - src/app/lib/zod.js | 208 --- src/app/mcp/server/mcp.js | 32 - src/app/mcp/server/streamableHttp.js | 319 ----- src/app/mcp/server/tasks.js | 218 --- src/app/routes/file-transfer.js | 71 - src/app/routes/http.js | 33 - src/app/routes/ws.js | 356 ----- src/app/server/dispatch-center.js | 276 ---- src/app/server/download-upgrade.js | 188 --- src/app/server/fetch.js | 44 - src/app/server/fs.js | 36 - src/app/server/ftp-client.js | 165 --- src/app/server/ftp-file.js | 28 - src/app/server/ftp-transfer.js | 130 -- src/app/server/global-state.js | 51 - src/app/server/rdp-proxy.js | 648 --------- src/app/server/remote-common.js | 63 - src/app/server/server.js | 52 - src/app/server/session-base.js | 167 --- src/app/server/session-common.js | 126 -- src/app/server/session-ftp.js | 306 ---- src/app/server/session-hop.js | 76 - src/app/server/session-local.js | 131 -- src/app/server/session-log.js | 36 - src/app/server/session-rdp.js | 147 -- src/app/server/session-serial.js | 159 --- src/app/server/session-sftp.js | 632 --------- src/app/server/session-spice.js | 136 -- src/app/server/session-ssh.js | 1084 -------------- src/app/server/session-telnet.js | 146 -- src/app/server/session-vnc.js | 146 -- src/app/server/session.js | 48 - src/app/server/sftp-file.js | 63 - src/app/server/socks.js | 106 -- src/app/server/spice-proxy.js | 218 --- src/app/server/ssh-known-hosts.js | 453 ------ src/app/server/ssh-proxy-command.js | 283 ---- src/app/server/ssh-tunnel.js | 204 --- src/app/server/ssh2-alg.js | 84 -- src/app/server/sync.js | 62 - src/app/server/telnet.js | 367 ----- src/app/server/terminal-api.js | 172 --- src/app/server/transfer.js | 475 ------- src/app/server/trzsz.js | 730 ---------- src/app/server/webdav-sync.js | 262 ---- src/app/server/xmodem.js | 940 ------------ src/app/server/zmodem.js | 1259 ----------------- src/app/upgrade/db-defaults.js | 124 -- src/app/upgrade/index.js | 91 -- src/app/upgrade/init-nedb.js | 19 - src/app/upgrade/version-upgrade.js | 37 - src/app/views/index.pug | 69 - src/app/widgets/load-widget.js | 201 --- src/app/widgets/widget-batch-op.js | 42 - src/app/widgets/widget-local-file-server.js | 194 --- src/app/widgets/widget-local-ftp-server.js | 143 -- src/app/widgets/widget-mcp-server.js | 1225 ---------------- src/app/widgets/widget-rename.js | 182 --- src/client/entry-web/basic.js | 85 -- src/client/entry-web/electerm.jsx | 10 - src/client/entry-web/worker.js | 200 --- src/client/file-select-dialog/file-item.jsx | 34 - .../file-select-dialog/file-select-dialog.jsx | 542 ------- .../file-select-dialog.styl | 35 - src/client/simple-auth/logout.jsx | 25 - src/client/simple-auth/logout.styl | 8 - src/client/simple-auth/web-login.jsx | 104 -- src/client/statics/favicon.ico | Bin 1150 -> 0 bytes src/client/web-components/path.js | 57 - src/client/web-components/store-login.js | 51 - src/client/web-components/style-overide.styl | 36 - src/client/web-components/web-api.js | 135 -- src/client/web-components/web-main.jsx | 14 - src/client/web-components/web-pre.js | 257 ---- src/client/web-components/web-store.js | 53 - 124 files changed, 80 insertions(+), 18751 deletions(-) delete mode 100644 src/app/app.js delete mode 100644 src/app/common/bookmark-zod-schemas.js delete mode 100644 src/app/common/build-run-scripts.js delete mode 100644 src/app/common/build-ssh-tunnel.js delete mode 100644 src/app/common/config-default.js delete mode 100644 src/app/common/constants.js delete mode 100644 src/app/common/count-folder-data.js delete mode 100644 src/app/common/create-session-log-file-path.js delete mode 100644 src/app/common/default-setting.js delete mode 100644 src/app/common/fs-functions.js delete mode 100644 src/app/common/get-json.js delete mode 100644 src/app/common/is-ip.js delete mode 100644 src/app/common/log.js delete mode 100644 src/app/common/pass-enc.js delete mode 100644 src/app/common/runtime-constants.js delete mode 100644 src/app/common/sanitize-filename.js delete mode 100644 src/app/common/time.js delete mode 100644 src/app/common/uid.js delete mode 100644 src/app/common/version-compare.js delete mode 100644 src/app/lib/ai.js delete mode 100644 src/app/lib/build-proxy.js delete mode 100644 src/app/lib/conf.js delete mode 100644 src/app/lib/custom-require.js delete mode 100644 src/app/lib/db.js delete mode 100644 src/app/lib/enc.js delete mode 100644 src/app/lib/extensions.js delete mode 100644 src/app/lib/fancy-console.js delete mode 100644 src/app/lib/font-list.js delete mode 100644 src/app/lib/fs.js delete mode 100644 src/app/lib/get-constants.js delete mode 100644 src/app/lib/global-state.js delete mode 100644 src/app/lib/init.js delete mode 100644 src/app/lib/install-src.js delete mode 100644 src/app/lib/iterm-theme.js delete mode 100644 src/app/lib/jwt.js delete mode 100644 src/app/lib/login.js delete mode 100644 src/app/lib/lookup.js delete mode 100644 src/app/lib/npm.js delete mode 100644 src/app/lib/proxy-agent.js delete mode 100644 src/app/lib/run-sync.js delete mode 100644 src/app/lib/serial-port.js delete mode 100644 src/app/lib/show-item-in-folder.js delete mode 100644 src/app/lib/sqlite.js delete mode 100644 src/app/lib/ssh-config.js delete mode 100644 src/app/lib/system-ca.js delete mode 100644 src/app/lib/user-config.js delete mode 100644 src/app/lib/view.js delete mode 100644 src/app/lib/watch-file.js delete mode 100644 src/app/lib/zod.js delete mode 100644 src/app/mcp/server/mcp.js delete mode 100644 src/app/mcp/server/streamableHttp.js delete mode 100644 src/app/mcp/server/tasks.js delete mode 100644 src/app/routes/file-transfer.js delete mode 100644 src/app/routes/http.js delete mode 100644 src/app/routes/ws.js delete mode 100644 src/app/server/dispatch-center.js delete mode 100644 src/app/server/download-upgrade.js delete mode 100644 src/app/server/fetch.js delete mode 100644 src/app/server/fs.js delete mode 100644 src/app/server/ftp-client.js delete mode 100644 src/app/server/ftp-file.js delete mode 100644 src/app/server/ftp-transfer.js delete mode 100644 src/app/server/global-state.js delete mode 100644 src/app/server/rdp-proxy.js delete mode 100644 src/app/server/remote-common.js delete mode 100644 src/app/server/server.js delete mode 100644 src/app/server/session-base.js delete mode 100644 src/app/server/session-common.js delete mode 100644 src/app/server/session-ftp.js delete mode 100644 src/app/server/session-hop.js delete mode 100644 src/app/server/session-local.js delete mode 100644 src/app/server/session-log.js delete mode 100644 src/app/server/session-rdp.js delete mode 100644 src/app/server/session-serial.js delete mode 100644 src/app/server/session-sftp.js delete mode 100644 src/app/server/session-spice.js delete mode 100644 src/app/server/session-ssh.js delete mode 100644 src/app/server/session-telnet.js delete mode 100644 src/app/server/session-vnc.js delete mode 100644 src/app/server/session.js delete mode 100644 src/app/server/sftp-file.js delete mode 100644 src/app/server/socks.js delete mode 100644 src/app/server/spice-proxy.js delete mode 100644 src/app/server/ssh-known-hosts.js delete mode 100644 src/app/server/ssh-proxy-command.js delete mode 100644 src/app/server/ssh-tunnel.js delete mode 100644 src/app/server/ssh2-alg.js delete mode 100644 src/app/server/sync.js delete mode 100644 src/app/server/telnet.js delete mode 100644 src/app/server/terminal-api.js delete mode 100644 src/app/server/transfer.js delete mode 100644 src/app/server/trzsz.js delete mode 100644 src/app/server/webdav-sync.js delete mode 100644 src/app/server/xmodem.js delete mode 100644 src/app/server/zmodem.js delete mode 100644 src/app/upgrade/db-defaults.js delete mode 100644 src/app/upgrade/index.js delete mode 100644 src/app/upgrade/init-nedb.js delete mode 100644 src/app/upgrade/version-upgrade.js delete mode 100644 src/app/views/index.pug delete mode 100644 src/app/widgets/load-widget.js delete mode 100644 src/app/widgets/widget-batch-op.js delete mode 100644 src/app/widgets/widget-local-file-server.js delete mode 100644 src/app/widgets/widget-local-ftp-server.js delete mode 100644 src/app/widgets/widget-mcp-server.js delete mode 100644 src/app/widgets/widget-rename.js delete mode 100644 src/client/entry-web/basic.js delete mode 100644 src/client/entry-web/electerm.jsx delete mode 100644 src/client/entry-web/worker.js delete mode 100644 src/client/file-select-dialog/file-item.jsx delete mode 100644 src/client/file-select-dialog/file-select-dialog.jsx delete mode 100644 src/client/file-select-dialog/file-select-dialog.styl delete mode 100644 src/client/simple-auth/logout.jsx delete mode 100644 src/client/simple-auth/logout.styl delete mode 100644 src/client/simple-auth/web-login.jsx delete mode 100644 src/client/statics/favicon.ico delete mode 100644 src/client/web-components/path.js delete mode 100644 src/client/web-components/store-login.js delete mode 100644 src/client/web-components/style-overide.styl delete mode 100644 src/client/web-components/web-api.js delete mode 100644 src/client/web-components/web-main.jsx delete mode 100644 src/client/web-components/web-pre.js delete mode 100644 src/client/web-components/web-store.js diff --git a/build/bin/install.js b/build/bin/install.js index c832a83..867b4ae 100644 --- a/build/bin/install.js +++ b/build/bin/install.js @@ -1,9 +1,87 @@ +/** + * install.js + * + * Runs automatically on `npm install` (npm "install" lifecycle script). + * + * electerm-harmony reuses 100 % of the source code from electerm-android. + * Instead of keeping a duplicate copy in this repo, we download the latest + * source archive from https://github.com/electerm/electerm-android and copy + * its `src/` directory into ours. This repo only keeps its own package.json, + * build scripts and HarmonyOS-specific build configuration. + * + * After the source sync we also copy the @electerm/electerm-react client + * from node_modules (same as the original install step). + */ +import { writeFile, mkdir } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' import pkg from 'shelljs' +import { x as tarX } from 'tar' -const { echo, rm, cp } = pkg +const { echo, rm: shellRm, cp } = pkg + +const REPO = 'electerm/electerm-android' +const BRANCH = 'main' +const URL = `https://codeload.github.com/${REPO}/tar.gz/refs/heads/${BRANCH}` +const TMP = resolve('temp/electerm-android-src') +const TMP_FILE = resolve(TMP, 'electerm-android.tar.gz') echo('install required modules') -rm('-rf', 'src/client/electerm-react') +// --------------------------------------------------------------------------- +// 1. Download the latest electerm-android source archive +// --------------------------------------------------------------------------- +echo(`downloading latest ${REPO} (${BRANCH} branch)…`) + +shellRm('-rf', TMP) +await mkdir(TMP, { recursive: true }) + +let downloaded = false +try { + const res = await fetch(URL) + if (!res.ok) { + throw new Error(`HTTP ${res.status} ${res.statusText}`) + } + const buf = Buffer.from(await res.arrayBuffer()) + await writeFile(TMP_FILE, buf) + echo('download complete') + downloaded = true +} catch (e) { + echo(`WARNING: failed to download source — ${e.message}`) + if (existsSync('src')) { + echo('keeping existing src/ folder') + } else { + echo('ERROR: src/ does not exist and download failed — cannot continue') + process.exit(1) + } +} + +// --------------------------------------------------------------------------- +// 2. Extract archive and replace src/ +// --------------------------------------------------------------------------- +if (downloaded) { + echo('extracting…') + await tarX({ + file: TMP_FILE, + cwd: TMP, + strip: 1 // remove the top-level "electerm-android-main/" directory + }) + + echo('syncing src/ from electerm-android…') + shellRm('-rf', 'src') + cp('-r', resolve(TMP, 'src'), resolve('src')) +} + +// --------------------------------------------------------------------------- +// 3. Copy @electerm/electerm-react client from node_modules +// --------------------------------------------------------------------------- +echo('installing electerm-react module') +shellRm('-rf', 'src/client/electerm-react') cp('-r', 'node_modules/@electerm/electerm-react/client', 'src/client/electerm-react') + +// --------------------------------------------------------------------------- +// 4. Cleanup temp files +// --------------------------------------------------------------------------- +shellRm('-rf', TMP) + echo('done install required modules') diff --git a/src/app/app.js b/src/app/app.js deleted file mode 100644 index b6fcd78..0000000 --- a/src/app/app.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * app entry - */ - -import log from './common/log.js' -import { createApp } from './server/server.js' - -process.on('uncaughtException', (err) => { - log.error('uncaughtException', err) -}) -process.on('unhandledRejection', (err) => { - log.error('unhandledRejection', err) -}) - -async function main () { - log.info('app start') - const app = await createApp() - - const { HOST, PORT } = process.env - - app.listen(PORT, HOST, () => { - log.info(`server runs on http://${HOST}:${PORT}`) - }) -} - -main() diff --git a/src/app/common/bookmark-zod-schemas.js b/src/app/common/bookmark-zod-schemas.js deleted file mode 100644 index c52b859..0000000 --- a/src/app/common/bookmark-zod-schemas.js +++ /dev/null @@ -1,130 +0,0 @@ -import { z } from '../lib/zod.js' - -const runScriptSchema = z.object({ - delay: z.number().optional().describe('Delay in ms before executing this command'), - script: z.string().describe('Command to execute') -}) - -const quickCommandSchema = z.object({ - name: z.string().describe('Quick command name'), - command: z.string().describe('Command') -}) - -const sshTunnelSchema = z.object({ - sshTunnel: z.enum(['forwardRemoteToLocal', 'forwardLocalToRemote', 'dynamicForward']).describe('Tunnel type'), - sshTunnelLocalHost: z.string().optional().describe('Local host'), - sshTunnelLocalPort: z.number().optional().describe('Local port'), - sshTunnelRemoteHost: z.string().optional().describe('Remote host'), - sshTunnelRemotePort: z.number().optional().describe('Remote port'), - name: z.string().optional().describe('Tunnel name') -}) - -const connectionHoppingSchema = z.object({ - host: z.string().describe('Host address'), - port: z.number().optional().describe('Port number'), - username: z.string().optional().describe('Username'), - password: z.string().optional().describe('Password'), - privateKey: z.string().optional().describe('Private key'), - passphrase: z.string().optional().describe('Passphrase'), - certificate: z.string().optional().describe('Certificate'), - authType: z.string().optional().describe('Auth type'), - profile: z.string().optional().describe('Profile id') -}) - -const commonNetworkBookmarkProps = { - title: z.string().describe('Bookmark title'), - host: z.string().describe('Host address'), - port: z.number().optional().describe('Port number'), - username: z.string().optional().describe('Username'), - password: z.string().optional().describe('Password'), - description: z.string().optional().describe('Bookmark description'), - // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), - startDirectoryRemote: z.string().optional().describe('Remote starting directory'), - startDirectoryLocal: z.string().optional().describe('Local starting directory'), - profile: z.string().optional().describe('Profile id'), - proxy: z.string().optional().describe('Proxy address (socks5://...)') -} - -const sshBookmarkSchema = { - ...commonNetworkBookmarkProps, - host: z.string().describe('SSH host address'), - port: z.number().optional().describe('SSH port (default 22)'), - username: z.string().optional().describe('SSH username'), - password: z.string().optional().describe('SSH password'), - authType: z.enum(['password', 'privateKey', 'profiles']).optional().describe('Authentication type'), - privateKey: z.string().optional().describe('Private key content or path (for privateKey auth)'), - passphrase: z.string().optional().describe('Passphrase for private key/certificate'), - certificate: z.string().optional().describe('Certificate content'), - enableSsh: z.boolean().optional().describe('Enable ssh, default is true'), - enableSftp: z.boolean().optional().describe('Enable sftp, default is true'), - useSshAgent: z.boolean().optional().describe('Use SSH agent, default is true'), - sshAgent: z.string().optional().describe('SSH agent path'), - serverHostKey: z.array(z.string()).optional().describe('Server host key algorithms'), - cipher: z.array(z.string()).optional().describe('Cipher list'), - compress: z.array(z.string()).optional().describe('Compression algorithms'), - quickCommands: z.array(quickCommandSchema).optional().describe('Quick commands'), - x11: z.boolean().optional().describe('Enable x11 forwarding, default is false'), - term: z.string().optional().describe('Terminal type, default is xterm-256color'), - displayRaw: z.boolean().optional().describe('Display raw output, default is false'), - encode: z.string().optional().describe('Charset, default is utf8'), - envLang: z.string().optional().describe('ENV LANG, default is en_US.UTF-8'), - // setEnv: z.string().optional().describe('Environment variables, format: KEY1=VALUE1 KEY2=VALUE2'), - color: z.string().optional().describe('Tag color, like #000000'), - // interactiveValues: z.string().optional().describe('Strings separated by newline'), - sshTunnels: z.array(sshTunnelSchema).optional().describe('SSH tunnel definitions'), - connectionHoppings: z.array(connectionHoppingSchema).optional().describe('Connection hopping definitions') -} - -const telnetBookmarkSchema = { - ...commonNetworkBookmarkProps, - host: z.string().describe('Telnet host address'), - port: z.number().optional().describe('Telnet port (default 23)'), - username: z.string().optional().describe('Telnet username'), - password: z.string().optional().describe('Telnet password'), - loginPrompt: z.string().optional().describe('Login prompt regex'), - passwordPrompt: z.string().optional().describe('Password prompt regex') -} - -const serialBookmarkSchema = { - title: z.string().describe('Bookmark title'), - path: z.string().describe('Serial device path'), - baudRate: z.number().optional().describe('Baud rate (default 9600)'), - dataBits: z.number().optional().describe('Data bits (default 8)'), - stopBits: z.number().optional().describe('Stop bits (default 1)'), - parity: z.enum(['none', 'even', 'odd', 'mark', 'space']).optional().describe('Parity (default none)'), - rtscts: z.boolean().optional().describe('RTS/CTS flow control'), - xon: z.boolean().optional().describe('XON flow control'), - xoff: z.boolean().optional().describe('XOFF flow control'), - xany: z.boolean().optional().describe('XANY flow control'), - txLineEnding: z.enum(['\r', '\n', '\r\n']).optional().describe('TX line ending appended on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)'), - rxLineEnding: z.enum(['none', 'lf_to_crlf', 'cr_to_crlf']).optional().describe('RX line ending conversion: "none" (pass-through, default), "lf_to_crlf" (LF→CRLF for LF-only devices), "cr_to_crlf" (CR→CRLF for CR-only devices)'), - closeSequence: z.string().optional().describe('Key sequence sent to the serial port when the user clicks "exit gracefully" in the terminal controls (e.g. to cleanly exit GNU screen before disconnecting a Bluetooth serial console). Supports \\n \\t \\r \\\\ and \\xHH hex bytes, default "\\x01ky" (Ctrl+A, k, y - GNU screen kill-window confirm)'), - closeSequenceDelay: z.number().optional().describe('Milliseconds to wait after sending closeSequence before actually closing the port, default 500'), - description: z.string().optional().describe('Bookmark description') - // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected') -} - -const localBookmarkSchema = { - title: z.string().describe('Bookmark title'), - description: z.string().optional().describe('Bookmark description'), - startDirectoryLocal: z.string().optional().describe('Local starting directory') - // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), - // execWindows: z.string().optional().describe('Windows exec path (overrides global setting)'), - // execMac: z.string().optional().describe('Mac exec path (overrides global setting)'), - // execLinux: z.string().optional().describe('Linux exec path (overrides global setting)'), - // execWindowsArgs: z.array(z.string()).optional().describe('Windows exec arguments'), - // execMacArgs: z.array(z.string()).optional().describe('Mac exec arguments'), - // execLinuxArgs: z.array(z.string()).optional().describe('Linux exec arguments') -} - -export { - runScriptSchema, - quickCommandSchema, - sshTunnelSchema, - connectionHoppingSchema, - commonNetworkBookmarkProps, - sshBookmarkSchema, - telnetBookmarkSchema, - serialBookmarkSchema, - localBookmarkSchema -} diff --git a/src/app/common/build-run-scripts.js b/src/app/common/build-run-scripts.js deleted file mode 100644 index 8f0b813..0000000 --- a/src/app/common/build-run-scripts.js +++ /dev/null @@ -1,6 +0,0 @@ -export const buildRunScripts = function (inst) { - return [{ - delay: inst.loginScriptDelay || 0, - script: inst.loginScript - }] -} diff --git a/src/app/common/build-ssh-tunnel.js b/src/app/common/build-ssh-tunnel.js deleted file mode 100644 index 7514aae..0000000 --- a/src/app/common/build-ssh-tunnel.js +++ /dev/null @@ -1,8 +0,0 @@ -export const buildSshTunnels = function (inst) { - return [{ - sshTunnel: inst.sshTunnel, - sshTunnelRemotePort: inst.sshTunnelRemotePort, - sshTunnelLocalPort: inst.sshTunnelLocalPort, - sshTunnelRemoteHost: inst.sshTunnelRemoteHost - }] -} diff --git a/src/app/common/config-default.js b/src/app/common/config-default.js deleted file mode 100644 index e11247b..0000000 --- a/src/app/common/config-default.js +++ /dev/null @@ -1,34 +0,0 @@ -import defaultSettings from './default-setting.js' - -export default { - keepaliveInterval: 10000, - rightClickSelectsWord: false, - pasteWhenContextMenu: false, - ctrlOrMetaOpenTerminalLink: false, - ...defaultSettings, - terminalTimeout: 5000, - enableGlobalProxy: false, - zoom: 1, - debug: false, - theme: 'default', - syncSetting: { - lastUpdateTime: Date.now(), - autoSync: false, - autoSyncInterval: 0, - autoSyncDirection: 'upload' - }, - terminalTypes: [ - 'xterm-256color', - 'xterm-new', - 'xterm-color', - 'xterm-vt220', - 'xterm', - 'linux', - 'vt100', - 'ansi', - 'rxvt' - ], - host: '127.0.0.1', - keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode', - enableSixel: true -} diff --git a/src/app/common/constants.js b/src/app/common/constants.js deleted file mode 100644 index 68434a9..0000000 --- a/src/app/common/constants.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * contants shared in app/client - */ - -export const userConfigId = 'userConfig' -export const instSftpKeys = [ - 'connect', - 'list', - 'download', - 'upload', - 'mkdir', - 'getHomeDir', - 'rmdir', - 'stat', - 'lstat', - 'chmod', - 'rename', - 'rm', - 'touch', - 'readlink', - 'realpath', - 'mv', - 'cp', - 'readFile', - 'writeFile' -] diff --git a/src/app/common/count-folder-data.js b/src/app/common/count-folder-data.js deleted file mode 100644 index 7051350..0000000 --- a/src/app/common/count-folder-data.js +++ /dev/null @@ -1,44 +0,0 @@ -export const getSizeCount = function (str) { - const [s1, s2] = str.split('\n').map(d => d.trim()) - const arr = s1.split(/\s+/) - const d1 = arr[0] - let size = parseFloat(d1) - const unit = d1.slice(-1) - if (unit === 'M') { - size = size / 1024 - } else if (unit === 'K') { - size = size / 1024 / 1024 - } - const count = parseInt(s2, 10) - return { - count, - size - } -} - -export const getSizeCountWin = function (str) { - const arr = str.trim().split('\n') - let count = 0 - let size = 0 - let all = 0 - for (const s of arr) { - const [s1, s2] = s.trim().split(/\s+/) - if (s1 === 'Count') { - count = parseInt(s2, 10) - all = all + 1 - if (all > 1) { - break - } - } else if (s1 === 'Sum') { - all = all + 1 - size = parseInt(s2, 10) / 1024 - if (all > 1) { - break - } - } - } - return { - count, - size - } -} diff --git a/src/app/common/create-session-log-file-path.js b/src/app/common/create-session-log-file-path.js deleted file mode 100644 index b9ad63c..0000000 --- a/src/app/common/create-session-log-file-path.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * functions to create ssh log of session - */ - -export const createLogFileName = (id) => { - return `${id}.log` -} diff --git a/src/app/common/default-setting.js b/src/app/common/default-setting.js deleted file mode 100644 index f784304..0000000 --- a/src/app/common/default-setting.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * default setting - */ - -export default { - hotkey: 'Control+2', - sshReadyTimeout: 50000, - scrollback: 3000, - onStartSessions: [], - fontSize: 16, - fontFamily: 'Fira Code, mono, courier-new, courier, monospace', - execWindows: 'System32/WindowsPowerShell/v1.0/powershell.exe', - execMac: 'zsh', - execLinux: 'bash', - execWindowsArgs: [], - execMacArgs: [], - execLinuxArgs: [], - enableGlobalProxy: false, - disableSshHistory: false, - disableTransferHistory: false, - terminalBackgroundImagePath: '', - terminalBackgroundFilterOpacity: 1, - terminalBackgroundFilterBlur: 0, - terminalBackgroundFilterBrightness: 1, - terminalBackgroundFilterGrayscale: 0, - terminalBackgroundFilterContrast: 1, - rendererType: 'dom', - terminalType: 'xterm-256color', - keepaliveCountMax: 10, - saveTerminalLogToFile: false, - checkUpdateOnStart: true, - cursorBlink: false, - cursorStyle: 'block', - useSystemTitleBar: false, - opacity: 1, - defaultEditor: '', - terminalWordSeparator: './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?', - confirmBeforeExit: false, - initDefaultTabOnStart: true, - screenReaderMode: false, - autoRefreshWhenSwitchToSftp: false, - keepaliveInterval: 0, - backspaceMode: '^?', - shiftEnterMode: '\\n', - showHiddenFilesOnSftpStart: true, - terminalInfos: [ - 'uptime', - 'cpu', - 'mem', - 'activities', - 'network', - 'disks' - ], - filePropsEnabled: [ - 'name', - 'size', - 'modifyTime' - ], - hideIP: false, - dataSyncSelected: 'all', - nameAI: '', - baseURLAI: 'https://api.atlascloud.ai/v1', - modelAI: 'deepseek-chat', - roleAI: '终端专家,提供不同系统下命令,简要解释用法,用markdown格式', - apiPathAI: '/chat/completions', - authHeaderNameAI: 'Authorization: Bearer', - sessionLogPath: '', - sshSftpSplitView: false, - showCmdSuggestions: false, - startDirectoryLocal: '', - autoReconnectTerminal: false, - dragDropBehavior: 'ask', - switchTabOnHover: false, - disableShortcutBar: false, - leftSideBarIcons: [ - 'newBookmark', - 'quickConnect', - 'bookmarks', - 'terminalThemes', - 'setting', - 'settingSync', - 'widgets' - ] -} diff --git a/src/app/common/fs-functions.js b/src/app/common/fs-functions.js deleted file mode 100644 index 91b4d2d..0000000 --- a/src/app/common/fs-functions.js +++ /dev/null @@ -1,34 +0,0 @@ -export default [ - 'readdirOnly', - 'readdirAndFiles', - 'run', - 'runWinCmd', - 'access', - 'statAsync', - 'lstatAsync', - 'cp', - 'mv', - 'mkdir', - 'touch', - 'chmod', - 'rename', - 'unlink', - 'rmrf', - 'readdirAsync', - 'readFile', - 'readFileAsBase64', - 'writeFile', - 'openFile', - 'zipFolder', - 'unzipFile', - 'readCustom', - 'exists', - 'readdir', - 'mkdir', - 'realpath', - 'statCustom', - 'openCustom', - 'closeCustom', - 'writeCustom', - 'getFolderSize' -] diff --git a/src/app/common/get-json.js b/src/app/common/get-json.js deleted file mode 100644 index 5978c68..0000000 --- a/src/app/common/get-json.js +++ /dev/null @@ -1,4 +0,0 @@ -import { readFileSync } from 'fs' -export default (pth) => { - return JSON.parse(readFileSync(pth, 'utf8')) -} diff --git a/src/app/common/is-ip.js b/src/app/common/is-ip.js deleted file mode 100644 index afebe3f..0000000 --- a/src/app/common/is-ip.js +++ /dev/null @@ -1,16 +0,0 @@ -export function isValidIP (input) { - // Check IPv4 format - const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ - if (ipv4Pattern.test(input)) { - return true - } - - // Check IPv6 format - const ipv6Pattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i - if (ipv6Pattern.test(input)) { - return true - } - - // If input doesn't match IPv4 or IPv6 patterns, it's not a valid IP - return false -} diff --git a/src/app/common/log.js b/src/app/common/log.js deleted file mode 100644 index a37bc5e..0000000 --- a/src/app/common/log.js +++ /dev/null @@ -1,84 +0,0 @@ -import { config } from 'dotenv' -import fs from 'fs' -import path from 'path' - -config() - -// Lightweight, dependency-free logger. -// - Logs to the console (level-aware) and, when possible, to a rolling file -// under the node project's `data/log` directory so logs can be pulled for -// debugging on Android. -// - Replaces `electron-log` entirely so the backend has no native/desktop-only -// dependency and starts reliably on the mobile Node runtime. - -const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } - -function levelFromEnv () { - const raw = (process.env.LOG_LEVEL || '').toLowerCase() - return Object.prototype.hasOwnProperty.call(LEVELS, raw) ? LEVELS[raw] : LEVELS.info -} - -const threshold = levelFromEnv() - -let logFile = null -try { - // Honour DB_PATH (set by the Android entry point to a stable, app-private - // directory) so logs live next to the database/uploads. Fall back to - // /data/log when DB_PATH is not set (desktop / local runs). - const base = process.env.DB_PATH - ? path.resolve(process.env.DB_PATH, 'log') - : path.resolve(process.cwd(), 'data', 'log') - fs.mkdirSync(base, { recursive: true }) - logFile = path.join(base, 'electerm.log') -} catch (e) { - // File logging is best-effort; never let it break startup. - logFile = null -} - -function ts () { - const d = new Date() - const p = (n) => String(n).padStart(2, '0') - return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` -} - -function formatArg (a) { - if (a instanceof Error) return a.stack || a.message - if (typeof a === 'string') return a - if (a === undefined) return 'undefined' - if (a === null) return 'null' - try { - return JSON.stringify(a) - } catch (e) { - return String(a) - } -} - -function emit (level, args) { - const line = `[${ts()}] ${level} › ${args.map(formatArg).join(' ')}` - if (LEVELS[level] <= threshold) { - const fn = - level === 'error' ? console.error - : level === 'warn' ? console.warn - : level === 'debug' ? console.debug - : console.log - fn(line) - } - if (logFile) { - try { - fs.appendFileSync(logFile, line + '\n') - } catch (e) { - // ignore write failures - } - } -} - -const logger = { - error: (...args) => emit('error', args), - warn: (...args) => emit('warn', args), - info: (...args) => emit('info', args), - debug: (...args) => emit('debug', args), - // kept for minimal API compatibility with callers that touch transports - transports: { console: { format: '' } } -} - -export default logger diff --git a/src/app/common/pass-enc.js b/src/app/common/pass-enc.js deleted file mode 100644 index 734f6f9..0000000 --- a/src/app/common/pass-enc.js +++ /dev/null @@ -1,17 +0,0 @@ -export const enc = (str) => { - if (typeof str !== 'string') { - return str - } - return str.split('').map((s, i) => { - return String.fromCharCode((s.charCodeAt(0) + i + 1) % 65536) - }).join('') -} - -export const dec = (str) => { - if (typeof str !== 'string') { - return str - } - return str.split('').map((s, i) => { - return String.fromCharCode((s.charCodeAt(0) - i - 1 + 65536) % 65536) - }).join('') -} diff --git a/src/app/common/runtime-constants.js b/src/app/common/runtime-constants.js deleted file mode 100644 index e3838a0..0000000 --- a/src/app/common/runtime-constants.js +++ /dev/null @@ -1,38 +0,0 @@ -import os from 'os' -import { resolve } from 'path' -import getJson from './get-json.js' - -export const cwd = process.cwd() - -const platform = os.platform() -const arch = os.arch() -const { NODE_ENV, NODE_TEST } = process.env -export const home = os.homedir() -export const sshKeysPath = resolve( - home, - '.ssh' -) -export const isWin = platform === 'win32' -export const isMac = platform === 'darwin' -export const isLinux = platform === 'linux' -export const isArm = arch.includes('arm') -export const isDev = NODE_ENV === 'development' -export const iconPath = resolve( - cwd, - isDev - ? 'node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png' - : 'dist/assets/images/electerm-round-128x128.png' -) -export const extIconPath = isDev - ? '/node_modules/electerm-icons/icons/' - : '/icons/' -export const defaultUserName = 'default_user' -export const minWindowWidth = 590 -export const minWindowHeight = 400 -export const defaultLang = 'en_us' -export const tempDir = os.tmpdir() -export const homeOrTmp = os.homedir() || os.tmpdir() -export const packInfo = getJson( - resolve(cwd, 'package.json') -) -export const isTest = !!NODE_TEST diff --git a/src/app/common/sanitize-filename.js b/src/app/common/sanitize-filename.js deleted file mode 100644 index bcdebf9..0000000 --- a/src/app/common/sanitize-filename.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Sanitize a filename for cross-platform file transfers. - * - * When transferring files between different OS (Linux <-> Windows <-> macOS), - * filenames may contain characters that are illegal on the destination OS. - * Windows is the most restrictive common platform, so we use its rules as - * the baseline for maximum compatibility. - * - * Rules applied: - * - Remove control characters (0x00-0x1F) - * - Replace reserved characters: < > : " / \ | ? * with _ - * - Strip trailing dots and spaces (Windows restriction: "file.", "file ") - * - Strip leading spaces only (NOT leading dots — they mean hidden file) - * - Reject reserved Windows device names: CON, PRN, AUX, NUL, COM1-9, LPT1-9 - * - Limit filename length to 255 bytes (common filesystem limit) - * - Fallback to 'unnamed' if result is empty - */ - -// Characters illegal on Windows (and problematic on many systems) -// eslint-disable-next-line no-control-regex -const ILLEGAL_CHARS = /[<>:"/\\|?\x00-\x1f]/g - -// Trailing dots and spaces = problematic on Windows (e.g. "file." → "file") -// Leading dots are PRESERVED — they mean "hidden file" on Unix and work on modern Windows -const TRAILING_DOTS_SPACES = /[.\s]+$/g - -// Leading spaces only — Windows can't handle filenames starting with space -const LEADING_SPACES = /^\s+/ - -// Reserved Windows device names (case-insensitive) -const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i - -const MAX_FILENAME_LENGTH = 255 - -const REPLACEMENT_CHAR = '_' - -export default function sanitizeFilename (name) { - if (!name || typeof name !== 'string') { - return 'unnamed' - } - - let safe = name - // Replace illegal characters - .replace(ILLEGAL_CHARS, REPLACEMENT_CHAR) - // Strip trailing dots and spaces (Windows restriction) - .replace(TRAILING_DOTS_SPACES, '') - // Strip leading spaces only (not dots — they mean hidden file) - .replace(LEADING_SPACES, '') - - // Handle reserved Windows device names by appending underscore - if (RESERVED_NAMES.test(safe)) { - safe = safe + REPLACEMENT_CHAR - } - - // Truncate to max length - if (safe.length > MAX_FILENAME_LENGTH) { - const ext = safe.lastIndexOf('.') - if (ext > 0) { - // Preserve extension when truncating - const extension = safe.slice(ext) - safe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension - } else { - safe = safe.slice(0, MAX_FILENAME_LENGTH) - } - } - - // Fallback for empty result - if (!safe) { - return 'unnamed' - } - - return safe -} diff --git a/src/app/common/time.js b/src/app/common/time.js deleted file mode 100644 index 943e1bc..0000000 --- a/src/app/common/time.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * time formatter - */ - -import dayjs from 'dayjs' - -export default ( - time = new Date(), - format = 'YYYY-MM-DD HH:mm:ss.SSS' -) => { - return dayjs(time).format(format) -} diff --git a/src/app/common/uid.js b/src/app/common/uid.js deleted file mode 100644 index 32a9a0c..0000000 --- a/src/app/common/uid.js +++ /dev/null @@ -1,4 +0,0 @@ -import { nanoid } from 'nanoid' -export default function uid () { - return nanoid(7) -} diff --git a/src/app/common/version-compare.js b/src/app/common/version-compare.js deleted file mode 100644 index 1c88b88..0000000 --- a/src/app/common/version-compare.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * version compare - * @param {string} a - * @param {string} b - * @return {number} - */ -// compare version '1.0.0' '12.0.3' -// return 1 when a > b -// return -1 when a < b -// return 0 when a === b -export default function (a, b) { - const ar = a.split('.').map(n => Number(n.replace('v', ''))) - const br = b.split('.').map(n => Number(n.replace('v', ''))) - let res = 0 - for (let i = 0, len = br.length; i < len; i++) { - if (br[i] < ar[i]) { - res = 1 - break - } else if (br[i] > ar[i]) { - res = -1 - break - } - } - return res -} diff --git a/src/app/lib/ai.js b/src/app/lib/ai.js deleted file mode 100644 index 0e53066..0000000 --- a/src/app/lib/ai.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * AI integration with DeepSeek API - */ -import axios from 'axios' -import { - StringDecoder -} from 'string_decoder' -import log from '../common/log.js' -import defaultSettings from '../common/config-default.js' -import { createProxyAgent } from './proxy-agent.js' - -// Store for ongoing streaming sessions -const streamingSessions = new Map() - -// Initialize OpenAI with DeepSeek configuration -const createAIClient = (baseURL, apiKey, proxy, authHeaderName) => { - const headerStr = authHeaderName || 'Authorization: Bearer' - const parts = headerStr.split(': ') - const headerKey = parts[0] - const headerPrefix = parts.length > 1 ? parts[1] : '' - const headerValue = headerPrefix - ? `${headerPrefix} ${apiKey}` - : apiKey - const config = { - baseURL, - headers: { - 'Content-Type': 'application/json', - [headerKey]: headerValue - } - } - - // Add proxy agent if proxy is provided - const agent = proxy ? createProxyAgent(proxy) : null - if (agent) { - config.httpAgent = agent - config.httpsAgent = agent - config.proxy = false // Disable default proxy behavior when using agent - } - - return axios.create(config) -} - -export const AIchatWithTools = async (messages, model, baseURL, path, apiKey, proxy, tools, authHeaderName) => { - try { - const client = createAIClient(baseURL, apiKey, proxy, authHeaderName) - const requestData = { - model, - messages, - stream: false - } - if (tools?.length) { - requestData.tools = tools - } - const response = await client.post(path, requestData) - const choice = response.data.choices[0] - return { - message: choice.message - } - } catch (e) { - log.error('AI chat with tools error', e) - return { error: e.message } - } -} - -export const AIchat = async ( - prompt, - model = defaultSettings.modelAI, - role = defaultSettings.roleAI, - baseURL = defaultSettings.baseURLAI, - path = defaultSettings.apiPathAI, - apiKey, - proxy = defaultSettings.proxyAI, - stream = true, - authHeaderName = defaultSettings.authHeaderNameAI, - messages = null -) => { - try { - const client = createAIClient(baseURL, apiKey, proxy, authHeaderName) - - // Determine if we should use streaming based on the prompt content - // Command suggestions should not use streaming for quick response - const isCommandSuggestion = prompt.includes('give me max 5 command suggestions') - const useStream = stream && !isCommandSuggestion - - // Use provided conversation messages if available, otherwise build from prompt and role - const requestMessages = messages || [ - { - role: 'system', - content: role - }, - { - role: 'user', - content: prompt - } - ] - - const requestData = { - model, - messages: requestMessages, - stream: useStream - } - - if (useStream) { - // For streaming responses, initiate streaming and return session info - const response = await client.post(path, requestData, { - responseType: 'stream' - }) - - const sessionId = Date.now().toString() + Math.random().toString(36).substr(2, 9) - const sessionData = { - stream: response.data, - content: '', - completed: false, - error: null - } - - streamingSessions.set(sessionId, sessionData) - - // Start processing the stream - processStream(sessionId, sessionData) - - return { - sessionId, - isStream: true, - hasMore: true, - content: '' - } - } else { - // For non-streaming responses (command suggestions and when stream=false) - const response = await client.post(path, requestData) - - return { - response: response.data.choices[0].message.content, - isStream: false - } - } - } catch (e) { - log.error('AI chat error') - log.error(e) - return { - error: e.message, - stack: e.stack - } - } -} - -// Function to get the current state of a streaming session -export const getStreamContent = async (sessionId) => { - const session = streamingSessions.get(sessionId) - if (!session) { - return { - error: 'Session not found' - } - } - - const result = { - content: session.content, - hasMore: !session.completed, - isStream: true - } - - if (session.error) { - result.error = session.error - } - - // Clean up completed sessions - if (session.completed || session.error) { - streamingSessions.delete(sessionId) - } - - return result -} - -// Process streaming data -function processStream (sessionId, sessionData) { - let buffer = '' - const decoder = new StringDecoder('utf8') - - const processLines = (shouldFlush = false) => { - const lines = buffer.split('\n') - buffer = shouldFlush ? '' : lines.pop() - const linesToProcess = shouldFlush ? lines.filter(Boolean).concat(buffer ? [buffer] : []) : lines - - for (const line of linesToProcess) { - if (line.trim() === '') continue - if (line.trim() === 'data: [DONE]') { - sessionData.completed = true - return - } - - if (line.startsWith('data: ')) { - try { - const data = JSON.parse(line.slice(6)) - if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) { - sessionData.content += data.choices[0].delta.content - } - } catch (e) { - log.error('Error parsing stream data:', e) - } - } - } - } - - sessionData.stream.on('data', (chunk) => { - buffer += decoder.write(chunk) - processLines() - }) - - sessionData.stream.on('end', () => { - buffer += decoder.end() - processLines(true) - sessionData.completed = true - }) - - sessionData.stream.on('error', (error) => { - sessionData.error = error.message - sessionData.completed = true - }) -} - -// Stop an ongoing streaming session -export const stopStream = (sessionId) => { - const session = streamingSessions.get(sessionId) - if (!session) { - return { error: 'Session not found' } - } - - // Destroy the stream to stop receiving data - if (session.stream && !session.stream.destroyed) { - session.stream.destroy() - } - - // Mark as completed (not an error, just stopped by user) - session.completed = true - session.stopped = true - - // Clean up - streamingSessions.delete(sessionId) - - return { stopped: true } -} diff --git a/src/app/lib/build-proxy.js b/src/app/lib/build-proxy.js deleted file mode 100644 index 883ad96..0000000 --- a/src/app/lib/build-proxy.js +++ /dev/null @@ -1,18 +0,0 @@ -export function buildProxyString (obj) { - if (!obj.proxyIp) { - return '' - } - - const proxyTypeMapping = { - 5: 'socks5', - 4: 'socks4', - 0: 'http', - 1: 'https' - } - - const proxyType = proxyTypeMapping[obj.proxyType] || '' - const hasCredentials = obj.proxyUsername && obj.proxyPassword - const credentials = hasCredentials ? `${obj.proxyUsername}:${obj.proxyPassword}@` : '' - - return `${proxyType}://${credentials}${obj.proxyIp}${obj.proxyPort ? `:${obj.proxyPort}` : ''}` -} diff --git a/src/app/lib/conf.js b/src/app/lib/conf.js deleted file mode 100644 index 8ec2106..0000000 --- a/src/app/lib/conf.js +++ /dev/null @@ -1,27 +0,0 @@ -import { - cwd -} from '../common/runtime-constants.js' -import log from '../common/log.js' -import { - resolve -} from 'path' - -const glob = {} - -export async function getConf () { - if (glob.conf) { - return glob.conf - } - const conf = await import( - resolve(cwd, 'config.js') - ).catch(err => { - if (err.code === 'ERR_MODULE_NOT_FOUND') { - return - } - log.error('read config.js failed', err) - }) - if (conf) { - glob.conf = conf - } - return glob.conf || {} -} diff --git a/src/app/lib/custom-require.js b/src/app/lib/custom-require.js deleted file mode 100644 index c9fd268..0000000 --- a/src/app/lib/custom-require.js +++ /dev/null @@ -1,63 +0,0 @@ -import { resolve, join } from 'path' -import { readFileSync, existsSync } from 'fs' -import { downloadPackage } from './npm.js' -import { cwd } from '../common/runtime-constants.js' - -function getDataFolderPath () { - const dbFolder = process.env.DB_PATH || resolve(cwd, 'data') - return resolve(dbFolder, 'custom-modules') -} - -function resolveModulePath (modulePath) { - const packageJsonPath = join(modulePath, 'package.json') - if (existsSync(packageJsonPath)) { - const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) - if (pkg.main) { - return resolve(modulePath, pkg.main) - } - } - if (existsSync(join(modulePath, 'index.js'))) { - return join(modulePath, 'index.js') - } - return modulePath -} - -export const customRequire = async (moduleName, options = {}) => { - const customModulesFolderPath = options.customModulesFolderPath || - process.env.CUSTOM_MODULES_FOLDER_PATH || - getDataFolderPath() - const isCustomModule = options.isCustomModule || false - const downloadModule = options.downloadModule !== false - - const modulePath = resolve(customModulesFolderPath, 'node_modules', moduleName) - - if (isCustomModule) { - try { - const resolvedPath = resolveModulePath(modulePath) - const mod = await import(resolvedPath) - return mod.default || mod - } catch (err) { - if (!downloadModule) { - throw err - } - await downloadPackage(moduleName, customModulesFolderPath) - const resolvedPath = resolveModulePath(modulePath) - const mod = await import(resolvedPath) - return mod.default || mod - } - } - - try { - const mod = await import(moduleName) - return mod.default || mod - } catch (err) { - if (!downloadModule) { - throw err - } - - await downloadPackage(moduleName, customModulesFolderPath) - const resolvedPath = resolveModulePath(modulePath) - const mod = await import(resolvedPath) - return mod.default || mod - } -} diff --git a/src/app/lib/db.js b/src/app/lib/db.js deleted file mode 100644 index 391c194..0000000 --- a/src/app/lib/db.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * db loader - */ - -let dbModule = null - -async function getDbModule () { - if (!dbModule) { - // await performMigration() - dbModule = await import('./sqlite.js') - } - return dbModule -} - -export async function dbAction (...args) { - const db = await getDbModule() - return db.dbAction ? db.dbAction(...args) : db.default.dbAction(...args) -} diff --git a/src/app/lib/enc.js b/src/app/lib/enc.js deleted file mode 100644 index dc8cf87..0000000 --- a/src/app/lib/enc.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * data encrypt/decrypt - * - * New format (GCM): 'gcm::::' - * Legacy format: '' (pure hex, no colons — aes-192-cbc) - * - * decrypt/decryptAsync detect the format automatically via the 'gcm:' prefix, - * so old data encrypted with the static IV/salt continues to work without migration. - */ - -import crypto from 'crypto' - -const algorithmDefault = 'aes-256-gcm' - -// Legacy constants — kept only for decrypting old data (aes-192-cbc) -const LEGACY_ALGORITHM = 'aes-192-cbc' -const LEGACY_IV = Buffer.alloc(16, 0) -const LEGACY_SALT = 'salt' -const LEGACY_KEY_LENGTH = 24 -const IV_LENGTH = 12 // 12 bytes is recommended for GCM -const SALT_LENGTH = 16 -const KEY_LENGTH = 32 // aes-256 requires a 32-byte key - -const funcs = {} - -function scryptAsync (...args) { - return new Promise((resolve, reject) => - crypto.scrypt(...args, (err, result) => { - if (err) { - reject(err) - } - resolve(result) - }) - ) -} - -funcs.encrypt = function ( - str = '', - password, - algorithm = algorithmDefault -) { - const iv = crypto.randomBytes(IV_LENGTH) - const salt = crypto.randomBytes(SALT_LENGTH) - const key = crypto.scryptSync(password, salt, KEY_LENGTH) - const cipher = crypto.createCipheriv(algorithm, key, iv) - let encrypted = cipher.update(str, 'utf8', 'hex') - encrypted += cipher.final('hex') - const authTag = cipher.getAuthTag() - return 'gcm:' + iv.toString('hex') + ':' + salt.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted -} - -funcs.decrypt = function ( - encrypted = '', - password, - algorithm = algorithmDefault -) { - if (encrypted.startsWith('gcm:')) { - // New format: gcm:iv_hex:salt_hex:authtag_hex:ciphertext_hex - const parts = encrypted.split(':') - const iv = Buffer.from(parts[1], 'hex') - const salt = Buffer.from(parts[2], 'hex') - const authTag = Buffer.from(parts[3], 'hex') - const ciphertext = parts[4] - const key = crypto.scryptSync(password, salt, KEY_LENGTH) - const decipher = crypto.createDecipheriv(algorithm, key, iv) - decipher.setAuthTag(authTag) - let decrypted = decipher.update(ciphertext, 'hex', 'utf8') - decrypted += decipher.final('utf8') - return decrypted - } - // Legacy format: aes-192-cbc with static IV and salt - const key = crypto.scryptSync(password, LEGACY_SALT, LEGACY_KEY_LENGTH) - const decipher = crypto.createDecipheriv(LEGACY_ALGORITHM, key, LEGACY_IV) - let decrypted = decipher.update(encrypted, 'hex', 'utf8') - decrypted += decipher.final('utf8') - return decrypted -} - -export const encryptAsync = async function ( - str = '', - password, - algorithm = algorithmDefault -) { - const iv = crypto.randomBytes(IV_LENGTH) - const salt = crypto.randomBytes(SALT_LENGTH) - const key = await scryptAsync(password, salt, KEY_LENGTH) - const cipher = crypto.createCipheriv(algorithm, key, iv) - let encrypted = cipher.update(str, 'utf8', 'hex') - encrypted += cipher.final('hex') - const authTag = cipher.getAuthTag() - return 'gcm:' + iv.toString('hex') + ':' + salt.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted -} - -export const decryptAsync = async function ( - encrypted = '', - password, - algorithm = algorithmDefault -) { - if (encrypted.startsWith('gcm:')) { - // New format: gcm:iv_hex:salt_hex:authtag_hex:ciphertext_hex - const parts = encrypted.split(':') - const iv = Buffer.from(parts[1], 'hex') - const salt = Buffer.from(parts[2], 'hex') - const authTag = Buffer.from(parts[3], 'hex') - const ciphertext = parts[4] - const key = await scryptAsync(password, salt, KEY_LENGTH) - const decipher = crypto.createDecipheriv(algorithm, key, iv) - decipher.setAuthTag(authTag) - let decrypted = decipher.update(ciphertext, 'hex', 'utf8') - decrypted += decipher.final('utf8') - return decrypted - } - // Legacy format: aes-192-cbc with static IV and salt - const key = await scryptAsync(password, LEGACY_SALT, LEGACY_KEY_LENGTH) - const decipher = crypto.createDecipheriv(LEGACY_ALGORITHM, key, LEGACY_IV) - let decrypted = decipher.update(encrypted, 'hex', 'utf8') - decrypted += decipher.final('utf8') - return decrypted -} diff --git a/src/app/lib/extensions.js b/src/app/lib/extensions.js deleted file mode 100644 index 9abc3dd..0000000 --- a/src/app/lib/extensions.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - jwtAuth, - errHandler -} from './jwt.js' -import { - getConf -} from './conf.js' -export async function applyExtensions (app) { - const conf = await getConf() - if (conf && conf.extensions && conf.extensions.length) { - for (const ext of conf.extensions) { - if (ext && ext.appExtend) { - ext.appExtend(app, jwtAuth, errHandler) - } - } - } -} diff --git a/src/app/lib/fancy-console.js b/src/app/lib/fancy-console.js deleted file mode 100644 index 77fa574..0000000 --- a/src/app/lib/fancy-console.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Fancy console logging utilities with colors and decorations - */ - -// ANSI color codes -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - dim: '\x1b[2m', - - // Text colors - black: '\x1b[30m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m', - gray: '\x1b[90m', - - // Background colors - bgBlack: '\x1b[40m', - bgRed: '\x1b[41m', - bgGreen: '\x1b[42m', - bgYellow: '\x1b[43m', - bgBlue: '\x1b[44m', - bgMagenta: '\x1b[45m', - bgCyan: '\x1b[46m', - bgWhite: '\x1b[47m' -} - -// Emoji collections -const emoji = { - success: '✅', - error: '❌', - warning: '⚠️', - info: 'ℹ️', - rocket: '🚀', - lightning: '⚡', - gear: '⚙️', - package: '📦', - sparkles: '✨', - fire: '🔥', - folder: '📁', - file: '📄', - arrow: '➤', - bullet: '•', - star: '⭐', - hourglass: '⏳', - checkmark: '✔️', - cross: '✖️', - diamond: '💎', - heart: '❤️', - thumbsUp: '👍' -} - -/** - * Create a fancy box with title and content - * @param {string} title - Box title - * @param {string[]} lines - Content lines - * @param {object} options - Styling options - */ -export function fancyBox (title, lines = [], options = {}) { - const { - width = 80, - color = colors.cyan, - titleColor = colors.yellow, - borderChar = '═', - lineChar = '─' - } = options - - const titleLine = `${titleColor}${title}${colors.reset}` - const topBorder = borderChar.repeat(width) - const bottomBorder = borderChar.repeat(width) - - console.log('\n' + color + topBorder + colors.reset) - console.log(titleLine) - console.log(color + topBorder + colors.reset) - - lines.forEach(line => { - console.log(line) - }) - - if (lines.length > 0) { - console.log(color + lineChar.repeat(width) + colors.reset) - } - console.log(color + bottomBorder + colors.reset + '\n') -} - -/** - * Log success message with decoration - */ -export function success (message, details = []) { - fancyBox(`${emoji.success} SUCCESS`, [ - `${colors.green}${message}${colors.reset}`, - ...details - ], { color: colors.green, titleColor: colors.bright + colors.green }) -} - -/** - * Log error message with decoration - */ -export function error (message, details = []) { - fancyBox(`${emoji.error} ERROR`, [ - `${colors.red}${message}${colors.reset}`, - ...details - ], { color: colors.red, titleColor: colors.bright + colors.red }) -} - -/** - * Log warning message with decoration - */ -export function warning (message, details = []) { - fancyBox(`${emoji.warning} WARNING`, [ - `${colors.yellow}${message}${colors.reset}`, - ...details - ], { color: colors.yellow, titleColor: colors.bright + colors.yellow }) -} - -/** - * Log info message with decoration - */ -export function info (message, details = []) { - fancyBox(`${emoji.info} INFO`, [ - `${colors.cyan}${message}${colors.reset}`, - ...details - ], { color: colors.cyan, titleColor: colors.bright + colors.cyan }) -} - -/** - * Log migration notice with special styling - */ -export function migrationNotice (version, oldDb, newDb, command) { - const lines = [ - `${colors.cyan}${emoji.gear} Since ${version}, electerm-web uses ${newDb} for better performance and stability.${colors.reset}`, - `${colors.yellow}${emoji.package} Old ${oldDb} database detected!${colors.reset}`, - `${colors.green}${emoji.sparkles} Please migrate your data to ${newDb} for enhanced performance and stability.${colors.reset}`, - '', - `${colors.magenta}${emoji.arrow} MIGRATION COMMAND:${colors.reset}`, - `${colors.white} ${command}${colors.reset}`, - `${colors.cyan}${emoji.file} Then import data.json in the new version via data sync panel${colors.reset}` - ] - - fancyBox(`${emoji.lightning} ELECTERM-WEB MIGRATION NOTICE ${emoji.lightning}`, lines, { - color: colors.magenta, - titleColor: colors.bright + colors.yellow - }) -} - -/** - * Log startup message with ASCII art - */ -export function startup (appName, version, port) { - const art = [ - ' ___________ __ ', - ' / ____/ / /__ _____ ____ / /_ ___ ______ ___ ', - ' / __/ / / / _ \\/ ___/ / __ `/ __/ _ \\/ ___/ __ `__ \\ ', - ' / /___/ / / __/ / / /_/ / /_/ __/ / / / / / / / ', - '/_____/_/_/\\___/_/ \\__,_/\\__/\\___/_/ /_/ /_/ /_/ ' - ] - - console.log('\n' + colors.cyan + '═'.repeat(60) + colors.reset) - art.forEach(line => { - console.log(colors.bright + colors.blue + line + colors.reset) - }) - console.log(colors.cyan + '═'.repeat(60) + colors.reset) - console.log(`${colors.yellow}${emoji.rocket} ${appName} v${version}${colors.reset}`) - console.log(`${colors.green}${emoji.gear} Running on port ${port}${colors.reset}`) - console.log(colors.cyan + '═'.repeat(60) + colors.reset + '\n') -} - -/** - * Simple colored log - */ -export function colorLog (message, color = colors.white) { - console.log(`${color}${message}${colors.reset}`) -} - -// Export colors and emoji for direct use -export { colors, emoji } diff --git a/src/app/lib/font-list.js b/src/app/lib/font-list.js deleted file mode 100644 index 2f7c938..0000000 --- a/src/app/lib/font-list.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * load font list after start - */ -import log from '../common/log.js' - -// `font-list` (a native-ish module) may be absent on some platforms (e.g. the -// Android runtime). Load it lazily and tolerate its absence so the server can -// still start. -let fontsPromise = null -function loadGetFonts () { - if (!fontsPromise) { - fontsPromise = import('font-list') - .then(m => m.getFonts) - .catch(err => { - log.warn('font-list is not available:', err.message) - return null - }) - } - return fontsPromise -} - -export const loadFontList = async () => { - const getFonts = await loadGetFonts() - if (!getFonts) { - return [] - } - try { - const fonts = await getFonts() - return fonts.map(f => f.replace(/"/g, '')) - } catch (err) { - log.error('load font list error') - log.error(err) - return [] - } -} diff --git a/src/app/lib/fs.js b/src/app/lib/fs.js deleted file mode 100644 index a1f5357..0000000 --- a/src/app/lib/fs.js +++ /dev/null @@ -1,466 +0,0 @@ -import fs, { promises as fss } from 'fs' -import log from '../common/log.js' -import { isWin, isMac, tempDir } from '../common/runtime-constants.js' -import path from 'path' -import uid from '../common/uid.js' -import { promisify } from 'util' -import * as tar from 'tar' -import { getSizeCount, getSizeCountWin } from '../common/count-folder-data.js' -import { exec, spawn } from 'child_process' -const execAsync = promisify(exec) - -const ROOT_PATH = '/' - -function encodeUtf8Base64 (value) { - return Buffer.from(String(value), 'utf8').toString('base64') -} - -function spawnDetachedCommand (command, args, options = {}) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - detached: false, - stdio: ['ignore', 'ignore', 'pipe'], - ...options - }) - let stderr = '' - - child.stderr.on('data', data => { - stderr += data.toString() - }) - child.on('error', reject) - - let settled = false - const settle = (err) => { - if (settled) { - return - } - settled = true - clearTimeout(timer) - child.unref() - if (err) { - reject(err) - } else { - resolve() - } - } - - child.on('close', code => { - if (code !== 0) { - settle(new Error(stderr.trim() || `Command exited with code ${code}`)) - } else { - settle(null) - } - }) - - const timer = setTimeout(() => settle(null), 5000) - }) -} - -// Encoding function -function encodeUint8Array (uint8Arr) { - return Buffer.from(uint8Arr).toString('base64') -} - -// Decoding function -function decodeBase64String (base64String) { - return new Uint8Array(Buffer.from(base64String, 'base64')) -} - -const isWinDrive = function (path) { - return /^\w+:$/.test(path) -} - -// `node-bash` (a native-ish module) is not available on every platform -// (e.g. the Android runtime). Load it lazily and tolerate its absence so the -// server can still start; callers that need a local shell get a clear error. -let bashPromise = null -function loadBash () { - if (!bashPromise) { - bashPromise = import('node-bash') - .then(m => m.Bash) - .catch(err => { - log.warn('node-bash is not available, local shell features will be limited:', err.message) - return null - }) - } - return bashPromise -} - -/** - * run cmd - * @param {string} cmd - */ -const run = async (cmd) => { - const Bash = await loadBash() - if (!Bash) { - throw new Error('Local shell (node-bash) is not available on this platform') - } - const ps = new Bash({ - executableOptions: { - '--login': true - } - }) - return ps.invokeCommand(cmd) - .then(s => s.stdout.toString()) -} - -/** - * run windows cmd - * @param {string} cmd - */ -const runWinCmd = (cmd) => { - return execAsync(`powershell.exe -Command "${cmd}"`) -} - -/** - * Escape a string for safe use inside POSIX single quotes. - * Within single quotes the only special character is the single quote itself; - * escape it by closing the quote, inserting an escaped quote, and reopening: - * ' -> '\'' - */ -function escapePosixShellArg (value) { - return String(value).replace(/'/g, "'\\''") -} - -/** - * Escape a string for safe use inside PowerShell single-quoted strings. - * Single quotes are escaped by doubling them: ' -> '' - */ -function escapePowerShellArg (value) { - return String(value).replace(/'/g, "''") -} - -function getFolderSizeWin (folderPath) { - const safePath = escapePowerShellArg(folderPath) - return runWinCmd( - `Get-ChildItem -Path '${safePath}' -Recurse | Where-Object { ! $_.PSIsContainer } | Measure-Object -Property Length -Sum` - ).then(res => getSizeCountWin(res.stdout)) -} - -function getFolderSize (folderPath) { - if (isWin) { - return getFolderSizeWin(folderPath) - } - const safePath = escapePosixShellArg(folderPath) - return run(`du -sh '${safePath}' && find '${safePath}' -type f | wc -l`) - .then(getSizeCount) -} - -/** - * rm -rf directory - * @param {string} localFolderPath absolute path of directory - */ -const rmrf = (localFolderPath) => { - return fss.rm(localFolderPath, { recursive: true, force: true }) -} - -/** - * Recursive copy helper for Node.js < 16.7.0 (where fs.cp doesn't exist) - */ -async function cpRecursive (src, dest) { - const stat = await fss.stat(src) - if (stat.isDirectory()) { - await fss.mkdir(dest, { recursive: true }) - const entries = await fss.readdir(src) - for (const entry of entries) { - await cpRecursive(path.join(src, entry), path.join(dest, entry)) - } - } else { - await fss.copyFile(src, dest) - } -} - -/** - * cp from to - * @param {string} from absolute source path - * @param {string} to absolute destination path - */ -const cp = async (from, to) => { - if (typeof fss.cp === 'function') { - return fss.cp(from, to, { recursive: true, force: true }) - } - return cpRecursive(from, to) -} - -/** - * mv from to - * @param {string} from absolute source path - * @param {string} to absolute destination path - */ -const mv = async (from, to) => { - try { - await fss.rename(from, to) - } catch (error) { - if (!error || error.code !== 'EXDEV') { - throw error - } - // Cross-device move: copy then remove - await cp(from, to) - await fss.rm(from, { recursive: true, force: true }) - } - return true -} - -/** - * touch file - * @param {string} localFolderPath absolute path - */ -const touch = (localFilePath) => { - return fss.writeFile(localFilePath, '') -} - -/** - * open file - * @param {string} localFolderPath absolute path - */ -const openFile = (localFilePath) => { - if (isWin) { - const script = '$path = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:ELECTERM_OPEN_FILE_PATH_B64)); Invoke-Item -LiteralPath $path' - return spawnDetachedCommand('powershell.exe', [ - '-NoLogo', - '-NonInteractive', - '-Command', - script - ], { - windowsHide: true, - env: { - ...process.env, - ELECTERM_OPEN_FILE_PATH_B64: encodeUtf8Base64(localFilePath) - } - }) - } - return spawnDetachedCommand(isMac ? 'open' : 'xdg-open', [localFilePath]) -} - -/** - * zip file - * @param {string} localFolerPath absolute path of a folder - */ -const zipFolder = (localFolerPath) => { - const n = uid() - const p = path.resolve(tempDir, `electerm-temp-${n}.tar`) - const cwd = path.dirname(localFolerPath) - const file = path.basename(localFolerPath) - return tar.c({ - gzip: false, - file: p, - cwd - }, [file]) - .then(() => p) -} - -const handleWindowsDrive = async (localFilePath, targetFolderPath) => { - const tempExtractDir = path.join(tempDir, `electerm-unzip-${uid()}`) - await fss.mkdir(tempExtractDir, { recursive: true }) - - try { - await tar.x({ file: localFilePath, C: tempExtractDir }) - const items = await fss.readdir(tempExtractDir) - - await Promise.all(items.map(async (item) => { - const from = path.join(tempExtractDir, item) - const to = path.join(targetFolderPath, item) - await mv(from, to) - })) - } finally { - await rmrf(tempExtractDir).catch(log.error) - } -} - -/** - * unzip file - * @param {string} localFilePath absolute path of a zip file - * @param {string} targetFolderPath absolute path of unzip target folder - */ -const unzipFile = async (localFilePath, targetFolderPath) => { - if (isWin && isWinDrive(targetFolderPath)) { - await handleWindowsDrive(localFilePath, targetFolderPath) - } else { - await tar.x({ file: localFilePath, C: targetFolderPath }) - } - return 1 -} - -async function listWindowsRootPath () { - const drives = await new Promise((resolve, reject) => { - const command = 'powershell.exe -Command "Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Root"' - - exec(command, { encoding: 'utf8' }, (error, stdout, stderr) => { - if (error) { - reject(error) - return - } - if (stderr) { - reject(new Error(stderr)) - return - } - const drives = stdout.split('\r\n') - .map(line => line.trim()) - // Accept any valid Windows path that ends with backslash - .filter(line => /^[^<>:"/\\|?*]+:\\$/.test(line)) - .map(drive => drive.slice(0, -1)) // Remove trailing backslash - resolve(drives) - }) - }) - const distros = await listWslDistros() - return [...drives, ...distros] -} - -async function listWslDistros () { - try { - const { stdout } = await execAsync('wsl.exe -l -q', { encoding: 'buffer' }) - const output = Buffer.from(stdout).toString('utf16le').replace(/^\uFEFF/, '') - const distros = output.split(/\r?\n/) - .map(line => line.trim()) - .filter(Boolean) - .map(name => '\\\\wsl.localhost\\' + name) - return distros - } catch { - return [] - } -} - -const readCustom = (p1, len, ...args) => { - return new Promise((resolve, reject) => { - fs.read(p1, new Uint8Array(len), ...args, (err, n, buffer) => { - if (err) { - return reject(err) - } - return resolve({ n, newArr: encodeUint8Array(buffer) }) - }) - }) -} - -const writeCustom = (p1, arr) => { - return new Promise((resolve, reject) => { - const narr = decodeBase64String(arr) - fs.write(p1, narr, (err, n) => { - if (err) { - return reject(err) - } - return resolve(1) - }) - }) -} - -const openCustom = async (...args) => { - return new Promise((resolve, reject) => { - fs.open(...args, (err, n) => { - if (err) { - return reject(err) - } - return resolve(n) - }) - }) -} - -const closeCustom = async (...args) => { - return new Promise((resolve, reject) => { - fs.close(...args, (err) => { - if (err) { - return reject(err) - } - return resolve(true) - }) - }) -} - -const statCustom = async (...args) => { - const st = await fss.stat(...args) - st.isD = st.isDirectory() - st.isF = st.isFile() - return st -} - -const readdirOnly = async (path) => { - const r = await fss.readdir(path, { withFileTypes: true }) - return r.filter(dirent => dirent.isDirectory()) - .map(d => { - return { - name: d.name, - isDirectory: true - } - }) -} - -const readdirAndFiles = async (path) => { - const r = await fss.readdir(path, { withFileTypes: true }) - return r.map(d => { - return { - name: d.name, - isDirectory: d.isDirectory() - } - }) -} - -export const fsExport = Object.assign( - {}, - fss, - { - run, - getFolderSize, - runWinCmd, - rmrf, - touch, - cp, - mv, - openFile, - readCustom, - statCustom, - openCustom, - closeCustom, - writeCustom, - zipFolder, - unzipFile, - readdirOnly, - readdirAndFiles - }, - { - readdirAsync: (_path) => { - if (_path === ROOT_PATH && isWin) { - return listWindowsRootPath() - } - let path = _path - if (isWin && isWinDrive(path)) { - path = path + '\\' - } - return fss.readdir(path) - }, - statAsync: (...args) => { - return fss.stat(...args) - .then(res => { - return { - ...res, - isDirectory: res.isDirectory() - } - }) - }, - lstatAsync: (...args) => { - return fss.lstat(...args) - .then(res => { - return { - ...res, - isDirectory: res.isDirectory(), - isSymbolicLink: res.isSymbolicLink() - } - }) - }, - readFile: (...args) => { - return fss.readFile(...args, 'utf8') - }, - readFileAsBase64: (...args) => { - return fss.readFile(...args) - .then(res => { - return res.toString('base64') - }) - }, - writeFile: (path, txt, mode) => { - return fss.writeFile(path, txt, { mode }) - .then(() => true) - .catch((e) => { - log.error('fs.writeFile', e) - return false - }) - } - } -) diff --git a/src/app/lib/get-constants.js b/src/app/lib/get-constants.js deleted file mode 100644 index dc23d1a..0000000 --- a/src/app/lib/get-constants.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * ipc main - */ - -import * as constants from '../common/runtime-constants.js' -import { transferKeys } from '../server/transfer.js' -import fs from 'fs' -import os from 'os' -import _ from 'lodash' -import { sep } from 'path' -import { getConfig } from './init.js' -import copy from 'json-deep-copy' -const allowList = new Set([ - 'SHELL', 'TERM', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'COLORTERM', - 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_TERMINAL', 'LC_TERMINAL_VERSION', - 'HOME', 'USER', 'LOGNAME', 'USERNAME', - 'PATH', 'PATHEXT', - 'TMPDIR', 'TMP', 'TEMP', - 'DISPLAY', 'WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'XDG_RUNTIME_DIR', - 'XDG_DATA_DIRS', 'XDG_CONFIG_DIRS', 'XDG_CURRENT_DESKTOP', 'XDG_SEAT', 'XDG_VTNR', - 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'SSH_CLIENT', 'SSH_CONNECTION', 'SSH_TTY', - 'NODE_PATH', 'NODE_ENV', 'NVM_DIR', 'NVM_BIN', - 'NPM_CONFIG_PREFIX', 'NPM_CONFIG_CACHE', - 'GIT_EDITOR', 'GIT_PAGER', 'GIT_TERMINAL_PROMPT', - 'EDITOR', 'VISUAL', 'PAGER', - 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', - 'APPDATA', 'LOCALAPPDATA', 'ProgramFiles', 'ProgramFiles(x86)', 'CommonProgramFiles', - 'ComSpec', 'SystemRoot', 'SystemDrive', 'USERPROFILE', 'USERDOMAIN', - 'COMPUTERNAME', 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE', 'OS', - 'Apple_PubSub_Socket_Render', - 'DBUS_SESSION_BUS_ADDRESS', 'DESKTOP_SESSION', 'GNOME_DESKTOP_SESSION_ID', 'KDE_FULL_SESSION', - 'CI', 'DOCKER_HOST', 'CONTAINER' -]) - -export function getEnv (key) { - if (key) { - if (!allowList.has(key)) { - return '' - } - return process.env[key] - } - return Object.fromEntries( - Object.entries(process.env).filter(([k]) => allowList.has(k)) - ) -} - -export async function getConstants (req, res) { - const config = await getConfig(true) - const data = { - osInfoData: (() => { - return Object.keys(os).map((k, i) => { - const vf = os[k] - if (!_.isFunction(vf)) { - return null - } - let v - try { - v = vf() - } catch (e) { - return null - } - if (!v) { - return null - } - v = JSON.stringify(v, null, 2) - return { k, v } - }).filter(d => d) - })(), - config, - sep, - fsConstants: fs.constants, - ...constants, - env: (() => { - return Object.fromEntries( - Object.entries(process.env).filter(([k]) => allowList.has(k)) - ) - })(), - versions: copy(process.versions), - transferKeys - } - res.send( - data - ) -} diff --git a/src/app/lib/global-state.js b/src/app/lib/global-state.js deleted file mode 100644 index a23bffe..0000000 --- a/src/app/lib/global-state.js +++ /dev/null @@ -1,40 +0,0 @@ -// src/app/lib/global-state.js - -class GlobalState { - constructor () { - this._state = { - win: null, - config: {}, - closeAction: '', - requireAuth: false, - serverInited: false, - langMap: null, - getLang: null, - translate: null, - timer: null, - childPid: null, - app: null, - rawArgs: null, - loadTime: null, - initTime: Date.now(), - watchFilePath: '', - oldRectangle: null, - serverPort: null, - isSecondInstance: false - } - } - - get (key) { - return this._state[key] - } - - set (key, value) { - this._state[key] = value - } - - update (key, updates) { - this._state[key] = { ...this._state[key], ...updates } - } -} - -export default new GlobalState() diff --git a/src/app/lib/init.js b/src/app/lib/init.js deleted file mode 100644 index 22b9687..0000000 --- a/src/app/lib/init.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * ipc main - */ - -import defaultSetting from '../common/config-default.js' -import { userConfigId } from '../common/constants.js' -import { isDev } from '../common/runtime-constants.js' -import { dbAction } from './db.js' -import installSrc from './install-src.js' -import * as langMap from '@electerm/electerm-locales' - -export async function getConfig () { - const userConfig = await dbAction('data', 'findOne', { - _id: userConfigId - }) || {} - delete userConfig._id - delete userConfig.host - delete userConfig.terminalTypes - delete userConfig.tokenElecterm - const config = { - ...defaultSetting, - ...userConfig, - port: process.env.PORT, - host: process.env.HOST, - wsHost: isDev ? process.env.DEV_HOST : process.env.HOST, - wsPort: isDev ? process.env.DEV_PORT : process.env.PORT, - server: process.env.SERVER, - useSystemTitleBar: true - } - return config -} - -export async function init () { - const config = await getConfig(true) - return { - config, - isPortable: true, - installSrc, - langs: Object.keys(langMap).map(id => { - return { - id, - ...langMap[id] - } - }), - langMap - } -} diff --git a/src/app/lib/install-src.js b/src/app/lib/install-src.js deleted file mode 100644 index 32fa05d..0000000 --- a/src/app/lib/install-src.js +++ /dev/null @@ -1,31 +0,0 @@ -// install-src.js -// Determines the Android APK architecture identifier at runtime. -// Used to match the correct release asset when checking/downloading upgrades. -// -// The Android APK splits produce four flavors: -// arm64-v8a -> Node.js os.arch() === 'arm64' -// armeabi-v7a -> Node.js os.arch() === 'arm' -// x86_64 -> Node.js os.arch() === 'x64' -// universal -> (ignored; the device CPU resolves to one of the above) -// -// We resolve at runtime from os.arch() so the same bundled code works for -// every split without a build-time injection step: the APK the user installed -// only contains the native libraries for its target ABI, so os.arch() always -// reflects the ABI that is actually running on device. - -import os from 'os' - -const archMap = { - arm64: 'arm64-v8a', - arm: 'armeabi-v7a', - x64: 'x86_64', - // 32-bit x86 is virtually nonexistent on Android; treat it as x86_64 so - // upgrade matching still resolves to a real asset. - ia32: 'x86_64', - x32: 'x86_64' -} - -const arch = os.arch() -const installSrc = 'electerm-android-' + (archMap[arch] || 'arm64-v8a') - -export default installSrc diff --git a/src/app/lib/iterm-theme.js b/src/app/lib/iterm-theme.js deleted file mode 100644 index 6b2bdfc..0000000 --- a/src/app/lib/iterm-theme.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * read themes from https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/electerm - */ - -import log from '../common/log.js' - -export async function listItermThemes (ws, msg) { - const all = await import('@electerm/electerm-themes/dist/index.mjs').then(d => d.default) - return Promise.all(all).catch(e => { - log.error('list Iterm Themes error', e) - return [] - }) -} diff --git a/src/app/lib/jwt.js b/src/app/lib/jwt.js deleted file mode 100644 index 73b2014..0000000 --- a/src/app/lib/jwt.js +++ /dev/null @@ -1,33 +0,0 @@ -import { expressjwt } from 'express-jwt' -import jwtb from 'jsonwebtoken' - -export const jwtAuth = expressjwt({ - secret: process.env.SERVER_SECRET, - algorithms: ['HS256'], - getToken: function fromHeaderOrQuerystring (req) { - return req.headers.token - } -}) - -export const errHandler = function (err, req, res, next) { - if (err && err.name === 'UnauthorizedError') { - res.status(401).send('invalid token...') - } else { - next() - } -} - -export function createToken ( - user = process.env.SERVER_USER, - pass = process.env.SERVER_SECRET, - expire = process.env.TOKEN_EXPIRED_TIME || '120y' -) { - const x = jwtb.sign({ - id: user - }, pass, { expiresIn: expire }) - return x -} - -export function verify (token) { - return jwtb.verify(token, process.env.SERVER_SECRET) -} diff --git a/src/app/lib/login.js b/src/app/lib/login.js deleted file mode 100644 index 6b595ca..0000000 --- a/src/app/lib/login.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * simple login with password only - */ - -import { createToken } from './jwt.js' - -const { - SERVER_PASS -} = process.env - -export function login (req, res) { - const { password } = req.body - if (password !== SERVER_PASS) { - return res.status(401).send('pass not right') - } - const token = createToken() - res.send(token) -} diff --git a/src/app/lib/lookup.js b/src/app/lib/lookup.js deleted file mode 100644 index 1db8526..0000000 --- a/src/app/lib/lookup.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * dns lookup - */ -import dns from 'dns' - -export default (host) => { - const v4 = new Promise((resolve, reject) => { - dns.resolve4(host, function (err, result) { - if (err) { - console.log(`v4 dns lookup error: ${err.message}`) - return resolve([]) - } - resolve(result) - }) - }) - const v6 = new Promise((resolve, reject) => { - dns.resolve6(host, function (err, result) { - if (err) { - console.log(`v6 dns lookup error: ${err.message}`) - return resolve([]) - } - resolve(result) - }) - }) - return Promise.all([v4, v6]).then(result => { - return [...result[0], ...result[1]] - }) -} diff --git a/src/app/lib/npm.js b/src/app/lib/npm.js deleted file mode 100644 index c6fd533..0000000 --- a/src/app/lib/npm.js +++ /dev/null @@ -1,74 +0,0 @@ -import path from 'path' -import fs from 'fs' -import * as tar from 'tar' -import axios from 'axios' -import { pipeline } from 'stream/promises' -import zlib from 'zlib' - -const npmRegistry = (process.env.NPM_REGISTRY || 'https://registry.npmjs.org').replace(/\/$/, '') - -async function fetchManifest (packageName) { - const encoded = packageName.replace('/', '%2f') - const { data } = await axios.get(`${npmRegistry}/${encoded}/latest`) - return data -} - -async function extractTarball (tarballUrl, destDir) { - const { data: stream } = await axios.get(tarballUrl, { responseType: 'stream' }) - fs.mkdirSync(destDir, { recursive: true }) - try { - await pipeline( - stream, - zlib.createGunzip(), - tar.extract({ cwd: destDir, strip: 1 }) - ) - } catch (err) { - fs.rmSync(destDir, { recursive: true, force: true }) - throw err - } -} - -function isPackageInstalled (packageDir) { - return fs.existsSync(path.join(packageDir, 'package.json')) -} - -async function installPackage (packageName, targetFolder, visited = new Set()) { - const cacheKey = `${packageName}@${npmRegistry}` - if (visited.has(cacheKey)) { - return - } - visited.add(cacheKey) - - const packageDir = path.join(targetFolder, 'node_modules', packageName) - if (isPackageInstalled(packageDir)) { - return - } - - const manifest = await fetchManifest(packageName) - const tarballUrl = manifest.dist && manifest.dist.tarball - if (!tarballUrl) { - throw new Error(`No tarball URL found for ${packageName}`) - } - - await extractTarball(tarballUrl, packageDir) - - const deps = { - ...manifest.dependencies, - ...manifest.optionalDependencies - } - - for (const [depName] of Object.entries(deps || {})) { - await installPackage(depName, targetFolder, visited) - } -} - -export async function downloadPackage (packageName, targetFolder) { - const npmPath = path.join(targetFolder, 'node_modules', packageName) - if (isPackageInstalled(npmPath)) { - return npmPath - } - - await installPackage(packageName, targetFolder) - - return npmPath -} diff --git a/src/app/lib/proxy-agent.js b/src/app/lib/proxy-agent.js deleted file mode 100644 index 5f872c4..0000000 --- a/src/app/lib/proxy-agent.js +++ /dev/null @@ -1,23 +0,0 @@ -import { HttpsProxyAgent } from 'https-proxy-agent' -import { SocksProxyAgent } from 'socks-proxy-agent' -import { getSystemCAsList } from './system-ca.js' - -// common proxy agent creator -export const createProxyAgent = (url = '', options = {}) => { - if ( - typeof url !== 'string' || - (!url.startsWith('http') && !url.startsWith('socks')) - ) { - return - } - const Cls = url.startsWith('http') - ? HttpsProxyAgent - : SocksProxyAgent - const certs = getSystemCAsList() - const caOptions = certs.length ? { ca: certs } : {} - return new Cls(url, { - keepAlive: true, - ...caOptions, - ...options - }) -} diff --git a/src/app/lib/run-sync.js b/src/app/lib/run-sync.js deleted file mode 100644 index bcbecb6..0000000 --- a/src/app/lib/run-sync.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * serial port lib - */ -import log from '../common/log.js' -import { listItermThemes } from '../lib/iterm-theme.js' -import { listSerialPorts } from '../lib/serial-port.js' -import { dbAction } from './db.js' -import { encryptAsync, decryptAsync } from '../lib/enc.js' -import { loadFontList } from './font-list.js' -import { loadSshConfig } from './ssh-config.js' -import { saveUserConfig } from './user-config.js' -import { checkDbUpgrade, doUpgrade } from '../upgrade/index.js' -import { watchFile, unwatchFile } from './watch-file.js' -import lookup from './lookup.js' -import { init } from './init.js' -import { showItemInFolder } from './show-item-in-folder.js' -import { AIchat, AIchatWithTools, getStreamContent, stopStream } from './ai.js' -import { - listWidgets, - runWidget, - stopWidget, - runWidgetFunc -} from '../widgets/load-widget.js' -import globalState from './global-state.js' -import { getEnv } from './get-constants.js' - -const globs = { - AIchat, - AIchatWithTools, - getStreamContent, - stopStream, - encryptAsync, - decryptAsync, - showItemInFolder, - dbAction, - lookup, - watchFile, - unwatchFile, - listSerialPorts, - checkDbUpgrade, - doUpgrade, - loadSshConfig, - listItermThemes, - init, - initCommandLine: () => Promise.resolve(0), - getInitTime: () => { - return globalState.get('initTime') - }, - loadFontList, - saveUserConfig, - registerDeepLink: () => Promise.resolve(1), - setWindowSize: () => Promise.resolve(1), - getScreenSize: () => Promise.resolve({ width: 1920, height: 1080 }), - checkMigrate: () => Promise.resolve(false), - setBackgroundColor: () => { - return Promise.resolve(1) - }, - listWidgets, - runWidget, - stopWidget, - runWidgetFunc, - getPendingDeepLink: () => Promise.resolve(null), - getEnv: () => Promise.resolve(getEnv()) -} - -export function runSync (ws, msg) { - const { - id, - func, - args = [] - } = msg - // console.log('runSync', func, args) - // Security: only dispatch to functions that are explicitly wired into - // the globs object as own properties. Checking hasOwnProperty (instead of - // a hand-maintained name list) means the allowlist can never drift from the - // real exports, and it blocks prototype-chain pivots like 'constructor', - // 'toString', '__proto__', 'hasOwnProperty' (CWE-863 / CWE-749). - if (!Object.prototype.hasOwnProperty.call(globs, func) || typeof globs[func] !== 'function') { - log.error('[security] blocked runSync call: ' + func) - ws.s({ - error: { - message: 'invalid function: ' + func, - stack: '' - }, - id - }) - return - } - globs[func](...args) - .then(data => { - ws.s({ - data, - id: msg.id - }) - }) - .catch(err => { - log.error(id, func, args, err) - ws.s({ - error: err, - id - }) - }) -} diff --git a/src/app/lib/serial-port.js b/src/app/lib/serial-port.js deleted file mode 100644 index 075cc7c..0000000 --- a/src/app/lib/serial-port.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * serial port lib - */ -import log from '../common/log.js' - -export async function listSerialPorts () { - return import('serialport') - .then(({ SerialPort }) => SerialPort.list()) - .catch(err => { - log.error('SerialPort not available or failed to list ports:', err) - return [] - }) -} diff --git a/src/app/lib/show-item-in-folder.js b/src/app/lib/show-item-in-folder.js deleted file mode 100644 index 0159e05..0000000 --- a/src/app/lib/show-item-in-folder.js +++ /dev/null @@ -1,39 +0,0 @@ -import { exec } from 'child_process' -import { - isWin, - isMac -} from '../common/runtime-constants.js' -import { dirname, resolve } from 'path' - -export async function showItemInFolder (filePath) { - const itemPath = resolve(filePath) - const folderPath = dirname(itemPath) - let command = '' - - if (isWin) { - // For Windows - command = `explorer.exe /select,"${itemPath}"` - } else if (isMac) { - // For macOS - command = `open -R "${folderPath}"` - } else { - // For Linux or other Unix-like systems - command = `xdg-open "${folderPath}"` - } - - return new Promise((resolve) => { - // Best-effort: the file manager may be unavailable (e.g. Android, headless - // Linux). Never reject — "show in folder" is purely cosmetic and a missing - // handler must not crash or surface an unhandled rejection. - exec(command, (error, _stdout, stderr) => { - if (error) { - resolve('no file manager available') - return - } - if (stderr) { - console.warn('showItemInFolder stderr:', stderr.toString()) - } - resolve('Item shown in folder successfully.') - }) - }) -} diff --git a/src/app/lib/sqlite.js b/src/app/lib/sqlite.js deleted file mode 100644 index c8a0c40..0000000 --- a/src/app/lib/sqlite.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * sqlite api wrapper - * Updated to use two database files: one for 'data' table, one for others - */ - -import { cwd } from '../common/runtime-constants.js' -import { resolve } from 'path' -import fs from 'fs' -import uid from '../common/uid.js' -import { DatabaseSync } from 'node:sqlite' - -// Define database folder and paths for two database files -const dbFolder = process.env.DB_PATH || resolve(cwd, 'data') -const baseFolder = resolve(dbFolder, 'sqlite') -const mainDbPath = resolve(baseFolder, 'electerm.db') -const dataDbPath = resolve(baseFolder, 'electerm_data.db') - -// Ensure parent directory exists -if (!fs.existsSync(baseFolder)) { - fs.mkdirSync(baseFolder, { recursive: true }) -} -// Create two database instances -const mainDb = new DatabaseSync(mainDbPath) -const dataDb = new DatabaseSync(dataDbPath) - -export const tables = [ - 'bookmarks', - 'bookmarkGroups', - 'addressBookmarks', - 'terminalThemes', - 'lastStates', - 'data', - 'quickCommands', - 'log', - 'dbUpgradeLog', - 'profiles', - 'workspaces', - 'history', - 'terminalCommandHistory', - 'aiChatHistory', - 'autoRunWidgets' -] - -// Create tables in appropriate databases -for (const table of tables) { - if (table === 'data') { - dataDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) - } else { - mainDb.exec(`CREATE TABLE IF NOT EXISTS \`${table}\` (_id TEXT PRIMARY KEY, data TEXT)`) - } -} - -// Helper function to get the appropriate database for a table -function getDatabase (dbName) { - return dbName === 'data' ? dataDb : mainDb -} - -function toDoc (row) { - if (!row) return null - let r = {} - try { - r = JSON.parse(row.data || '{}') - } catch (e) { - log.error(e) - } - return { - ...r, - _id: row._id - } -} - -function toRow (doc) { - const _id = doc._id || doc.id || uid() - const copy = { ...doc } - delete copy._id - delete copy.id - return { - _id, - data: JSON.stringify(copy) - } -} - -export async function dbAction (dbName, op, ...args) { - if (op === 'compactDatafile') { - return - } - if (!tables.includes(dbName)) { - throw new Error(`Table ${dbName} does not exist`) - } - - // Get the appropriate database for this table - const db = getDatabase(dbName) - - if (op === 'find') { - const sql = `SELECT * FROM \`${dbName}\`` - const stmt = db.prepare(sql) - const rows = stmt.all() - return (rows || []).map(toDoc).filter(Boolean) - } else if (op === 'findOne') { - const query = args[0] || {} - const sql = `SELECT * FROM \`${dbName}\` WHERE _id = ? LIMIT 1` - const params = [query._id] - const stmt = db.prepare(sql) - const row = stmt.get(...params) - return toDoc(row) - } else if (op === 'insert') { - const inserts = Array.isArray(args[0]) ? args[0] : [args[0]] - const inserted = [] - for (const doc of inserts) { - const { _id, data } = toRow(doc) - const stmt = db.prepare(`INSERT OR REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) - stmt.run(_id, data) - inserted.push({ ...doc, _id }) - } - return Array.isArray(args[0]) ? inserted : inserted[0] - } else if (op === 'remove') { - const query = args[0] || {} - const sql = `DELETE FROM \`${dbName}\` WHERE _id = ?` - const params = [query._id] - const stmt = db.prepare(sql) - const res = stmt.run(...params) - return res.changes - } else if (op === 'update') { - const query = args[0] - const updateObj = args[1] - const options = args[2] || {} - const { upsert = false } = options - const qid = query._id || query.id - const newData = updateObj.$set || updateObj - const { _id, data } = toRow({ - _id: qid, - ...newData - }) - let stmt - let res - if (upsert) { - stmt = db.prepare(`REPLACE INTO \`${dbName}\` (_id, data) VALUES (?, ?)`) - res = stmt.run(_id, data) - } else { - stmt = db.prepare(`UPDATE \`${dbName}\` SET data = ? WHERE _id = ?`) - res = stmt.run(data, qid) - } - return res.changes - } -} diff --git a/src/app/lib/ssh-config.js b/src/app/lib/ssh-config.js deleted file mode 100644 index 15ca3bc..0000000 --- a/src/app/lib/ssh-config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * read ssh config - */ -import { loadAndConvert } from 'ssh-config-loader' - -export async function loadSshConfig () { - return loadAndConvert() -} diff --git a/src/app/lib/system-ca.js b/src/app/lib/system-ca.js deleted file mode 100644 index 2932319..0000000 --- a/src/app/lib/system-ca.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Load system-trusted CA certificates for the main web app process. - */ - -import { execSync } from 'child_process' -import { existsSync, readdirSync, readFileSync } from 'fs' -import https from 'https' -import os from 'os' -import { join } from 'path' - -let cachedPem = null -let globalApplied = false - -function loadMacOS () { - try { - return execSync( - 'security find-certificate -a -p ' + - '/System/Library/Keychains/SystemRootCertificates.keychain ' + - '/Library/Keychains/System.keychain ' + - `${os.homedir()}/Library/Keychains/login.keychain-db`, - { encoding: 'utf8', timeout: 10000 } - ) - } catch { - return '' - } -} - -function loadLinux () { - const dirs = [ - '/etc/ssl/certs', - '/etc/pki/tls/certs', - '/etc/pki/ca-trust/extracted/pem', - '/usr/local/share/certs' - ] - const files = [] - for (const dir of dirs) { - if (!existsSync(dir)) { - continue - } - try { - for (const f of readdirSync(dir)) { - if (f.endsWith('.crt') || f.endsWith('.pem')) { - files.push(join(dir, f)) - } - } - break - } catch { - // try next directory - } - } - - if (files.length > 0) { - return files.map((f) => { - try { - return readFileSync(f, 'utf8') - } catch { - return '' - } - }).join('\n') - } - - const bundlePaths = [ - '/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem' - ] - for (const p of bundlePaths) { - if (existsSync(p)) { - return readFileSync(p, 'utf8') - } - } - return '' -} - -function loadWindows () { - try { - return execSync( - 'powershell -Command ' + - '"Get-ChildItem -Path Cert:\\LocalMachine\\Root, Cert:\\LocalMachine\\CA, Cert:\\CurrentUser\\Root, Cert:\\CurrentUser\\CA ' + - '| Where-Object { $_.NotAfter -gt (Get-Date) } ' + - '| ForEach-Object { \'-----BEGIN CERTIFICATE-----\'; ' + - '[System.Convert]::ToBase64String($_.RawData, \'InsertLineBreaks\'); ' + - '\'-----END CERTIFICATE-----\' }"', - { encoding: 'utf8', timeout: 10000, windowsHide: true } - ) - } catch { - return '' - } -} - -export function getSystemCAsPem () { - if (cachedPem !== null) { - return cachedPem - } - switch (os.platform()) { - case 'darwin': - cachedPem = loadMacOS() - break - case 'linux': - cachedPem = loadLinux() - break - case 'win32': - cachedPem = loadWindows() - break - default: - cachedPem = '' - break - } - return cachedPem -} - -export function getSystemCAsList () { - const pem = getSystemCAsPem() - if (!pem) { - return [] - } - return pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) || [] -} - -export function applySystemCAsToGlobalAgent () { - if (globalApplied) { - return 0 - } - const certs = getSystemCAsList() - if (!certs.length) { - return 0 - } - - const existing = https.globalAgent.options.ca - const existingList = Array.isArray(existing) - ? existing - : (typeof existing === 'string' ? [existing] : []) - const merged = Array.from(new Set(existingList.concat(certs))) - https.globalAgent.options.ca = merged - globalApplied = true - return certs.length -} diff --git a/src/app/lib/user-config.js b/src/app/lib/user-config.js deleted file mode 100644 index b7026b0..0000000 --- a/src/app/lib/user-config.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * user-controll.json controll - */ - -import { dbAction } from './db.js' -import { userConfigId } from '../common/constants.js' - -export async function saveUserConfig (userConfig) { - const q = { - _id: userConfigId - } - delete userConfig.host - delete userConfig.terminalTypes - delete userConfig.tokenElecterm - delete userConfig.port - delete userConfig.server - delete userConfig.wsPort - delete userConfig.wsHost - delete userConfig.useSystemTitleBar - await dbAction('data', 'update', q, { - ...q, - ...userConfig - }, { - upsert: true - }) -} diff --git a/src/app/lib/view.js b/src/app/lib/view.js deleted file mode 100644 index e7d3f8f..0000000 --- a/src/app/lib/view.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * simple login with password only - */ - -import { - isDev, - isMac, - isWin, - packInfo, - home, - extIconPath, - defaultUserName, - cwd -} from '../common/runtime-constants.js' -import { migrationNotice } from './fancy-console.js' -import fsFunctions from '../common/fs-functions.js' -import copy from 'json-deep-copy' -import { createToken } from './jwt.js' -import { logDir } from '../server/session-log.js' -import { resolve } from 'path' -import fs from 'fs' - -const defaultAIPreset = { - baseURLAI: 'https://ai.electerm.org/api/ai', - apiPathAI: '/chat/completions', - modelAI: 'mistral-small-latest', - authHeaderNameAI: 'Authorization: Bearer', - id: 'ai.electerm.org', - nameAI: 'ai.electerm.org(default free)' -} - -function buildServer () { - return `http://${process.env.HOST}:${process.env.PORT}` -} - -export async function index (req, res) { - const server = process.env.SERVER || (isDev ? buildServer() : '') - const cdn = process.env.CDN || server - const hasNodePty = false - // All session types the app knows about. - const supportSessionTypes = [ - 'ssh', - 'telnet', - 'web', - 'rdp', - 'vnc', - 'ftp', - 'spice' - ] - const data = { - isDev, - isMac, - isWin, - packInfo, - home, - version: packInfo.version, - siteName: packInfo.name, - defaultAIPreset, - fsFunctions, - isWebApp: true, - disableUpgradeCheck: false, - versionFile: 'version-android.html', - downloadUpgradeFromBrowser: true, - extIconPath: cdn + extIconPath, - cdn, - sessionLogPath: logDir, - query: req.query, - server, - hasNodePty, - needMigrate: false, - supportSessionTypes - } - const { - ENABLE_AUTH - } = process.env - if (!ENABLE_AUTH) { - data.tokenElecterm = createToken() - } - data._global = copy(data) - res.render('index', data) -} diff --git a/src/app/lib/watch-file.js b/src/app/lib/watch-file.js deleted file mode 100644 index 8a09e7c..0000000 --- a/src/app/lib/watch-file.js +++ /dev/null @@ -1,40 +0,0 @@ -import fs from 'fs' -import globalState from './global-state.js' -import _ from 'lodash' - -const onWatch = _.debounce(() => { - try { - const filePath = globalState.get('watchFilePath') - if (fs.existsSync(filePath)) { - const text = fs.readFileSync(filePath, 'utf8') - globalState.get('win').webContents.send('file-change', text) - } else { - console.log('Watched file no longer exists') - globalState.get('win').webContents.send('file-deleted') - } - } catch (e) { - console.error('Error reading file:', e) - globalState.get('win').webContents.send('file-read-error', e.message) - } -}, 300, { leading: false, trailing: true }) - -export const watchFile = (path) => { - globalState.set('watchFilePath', path) - fs.watchFile(path, onWatch) -} - -export const unwatchFile = (path) => { - globalState.set('watchFilePath', '') - fs.unwatchFile(path, onWatch) -} - -const cleanWatchFile = () => { - globalState.set('watchFilePath', '') - const filePath = globalState.get('watchFilePath') - if (!filePath) { - return - } - fs.unwatchFile(filePath, onWatch) -} - -process.on('exit', cleanWatchFile) diff --git a/src/app/lib/zod.js b/src/app/lib/zod.js deleted file mode 100644 index 338c156..0000000 --- a/src/app/lib/zod.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Lightweight zod replacement for electerm - * Covers only the API surface used in the project: - * z.string(), z.number(), z.boolean(), z.any(), - * z.enum(), z.object(), z.array(), z.record(), - * .optional(), .describe(), z.toJSONSchema() - */ - -class ZodType { - constructor (typeName, meta = {}) { - this._typeName = typeName - this._optional = false - this._description = undefined - this._meta = meta - // Mark as zod-compatible schema - this['~standard'] = { type: typeName } - } - - optional () { - const clone = this._clone() - clone._optional = true - return clone - } - - describe (desc) { - const clone = this._clone() - clone._description = desc - return clone - } - - _clone () { - const clone = Object.create(Object.getPrototypeOf(this)) - Object.assign(clone, this) - // Re-create the ~standard marker so it's own-property - clone['~standard'] = { ...this['~standard'] } - return clone - } - - _toJsonSchema () { - throw new Error('_toJsonSchema not implemented for ' + this._typeName) - } -} - -class ZodString extends ZodType { - constructor () { - super('string') - } - - _toJsonSchema () { - return { type: 'string' } - } -} - -class ZodNumber extends ZodType { - constructor () { - super('number') - } - - _toJsonSchema () { - return { type: 'number' } - } -} - -class ZodBoolean extends ZodType { - constructor () { - super('boolean') - } - - _toJsonSchema () { - return { type: 'boolean' } - } -} - -class ZodAny extends ZodType { - constructor () { - super('any') - } - - _toJsonSchema () { - return {} - } -} - -class ZodEnum extends ZodType { - constructor (values) { - super('enum', { values }) - } - - _toJsonSchema () { - return { type: 'string', enum: this._meta.values } - } -} - -class ZodArray extends ZodType { - constructor (itemSchema) { - super('array', { itemSchema }) - } - - _toJsonSchema () { - const items = schemaToJsonSchema(this._meta.itemSchema) - return { type: 'array', items } - } -} - -class ZodObject extends ZodType { - constructor (shape) { - super('object', { shape }) - } - - _toJsonSchema () { - const properties = {} - const required = [] - const shape = this._meta.shape || {} - for (const [key, schema] of Object.entries(shape)) { - properties[key] = schemaToJsonSchema(schema) - if (schema._description) { - properties[key].description = schema._description - } - if (!schema._optional) { - required.push(key) - } - } - const result = { type: 'object', properties } - if (required.length > 0) { - result.required = required - } - return result - } -} - -class ZodRecord extends ZodType { - constructor (valueSchema) { - super('record', { valueSchema }) - } - - _toJsonSchema () { - const additionalProperties = schemaToJsonSchema(this._meta.valueSchema) - return { type: 'object', additionalProperties } - } -} - -function schemaToJsonSchema (schema) { - if (!schema) { - return {} - } - if (schema instanceof ZodType) { - const base = schema._toJsonSchema() - if (schema._description) { - base.description = schema._description - } - return base - } - // Plain object with zod values (used as inputSchema in MCP tools) - if (typeof schema === 'object' && !Array.isArray(schema)) { - return objectShapeToJsonSchema(schema) - } - return {} -} - -function objectShapeToJsonSchema (shape) { - const properties = {} - const required = [] - for (const [key, value] of Object.entries(shape)) { - if (value instanceof ZodType) { - properties[key] = schemaToJsonSchema(value) - if (!value._optional) { - required.push(key) - } - } - } - const result = { type: 'object', properties } - if (required.length > 0) { - result.required = required - } - return result -} - -const z = { - string: () => new ZodString(), - number: () => new ZodNumber(), - boolean: () => new ZodBoolean(), - any: () => new ZodAny(), - enum: (values) => new ZodEnum(values), - object: (shape) => new ZodObject(shape || {}), - array: (itemSchema) => new ZodArray(itemSchema), - record: (keyOrValue, maybeValue) => { - // z.record(valueSchema) or z.record(keySchema, valueSchema) - const valueSchema = maybeValue || keyOrValue - return new ZodRecord(valueSchema) - }, - toJSONSchema: (schema) => { - if (schema instanceof ZodType) { - return schema._toJsonSchema() - } - if (typeof schema === 'object' && schema !== null) { - // Check if it's a plain shape object with ~standard values - const hasZodValues = Object.values(schema).some( - v => v instanceof ZodType - ) - if (hasZodValues) { - return objectShapeToJsonSchema(schema) - } - } - return { type: 'object', properties: {} } - } -} - -export { z, ZodType } diff --git a/src/app/mcp/server/mcp.js b/src/app/mcp/server/mcp.js deleted file mode 100644 index ba529b8..0000000 --- a/src/app/mcp/server/mcp.js +++ /dev/null @@ -1,32 +0,0 @@ -class McpServer { - constructor (options) { - this.name = options.name - this.version = options.version - this.tools = new Map() - // Optional TaskManager instance (src/app/mcp/server/tasks.js). - // When set, the transport advertises the io.modelcontextprotocol/tasks - // extension and serves tasks/get + tasks/cancel. - this.taskManager = options.taskManager || null - // Newest first — initialize echoes the client's requested version when - // supported, otherwise responds with the newest we support. - this.supportedProtocolVersions = options.supportedProtocolVersions || [ - '2025-11-25', - '2025-06-18', - '2024-11-05' - ] - } - - registerTool (name, { description, inputSchema }, handler) { - this.tools.set(name, { description, inputSchema, handler }) - } - - async connect (transport) { - await transport.connect(this) - } - - async close () { - // nothing - } -} - -export { McpServer } diff --git a/src/app/mcp/server/streamableHttp.js b/src/app/mcp/server/streamableHttp.js deleted file mode 100644 index 4e962a6..0000000 --- a/src/app/mcp/server/streamableHttp.js +++ /dev/null @@ -1,319 +0,0 @@ -import { z } from '../../lib/zod.js' - -function zodToJsonSchema (zodSchema) { - if (!zodSchema) { - return { type: 'object', properties: {} } - } - try { - if (zodSchema && typeof zodSchema === 'object') { - const hasZodStandard = Object.values(zodSchema).some( - v => v && typeof v === 'object' && '~standard' in v - ) - if (hasZodStandard) { - const zodObject = z.object( - Object.fromEntries( - Object.entries(zodSchema).map(([key, value]) => [key, value]) - ) - ) - const jsonSchema = z.toJSONSchema(zodObject) - return jsonSchema || { type: 'object', properties: {} } - } - } - if (zodSchema && typeof zodSchema === 'object' && '~standard' in zodSchema) { - const jsonSchema = z.toJSONSchema(zodSchema) - return jsonSchema || { type: 'object', properties: {} } - } - return { type: 'object', properties: {} } - } catch (e) { - return { type: 'object', properties: {} } - } -} - -class StreamableHTTPServerTransport { - constructor (options) { - this.sessionIdGenerator = options.sessionIdGenerator - this.onsessioninitialized = options.onsessioninitialized - this.onclose = null - this.server = null - this.sessionId = null - this.initialized = false - // Whether the client advertised the io.modelcontextprotocol/tasks - // extension — via initialize capabilities or per-request _meta. - this.clientSupportsTasks = false - } - - async connect (server) { - this.server = server - this.sessionId = this.sessionIdGenerator() - if (this.onsessioninitialized) { - this.onsessioninitialized(this.sessionId) - } - } - - // Send a single JSON-RPC result as an SSE `message` event and close the - // stream. Used for tool-call responses that may be streamed in the future. - _sendSSE (res, data) { - res.setHeader('Content-Type', 'text/event-stream') - res.setHeader('Cache-Control', 'no-cache') - res.setHeader('Connection', 'keep-alive') - res.write('event: message\n') - res.write(`data: ${JSON.stringify(data)}\n\n`) - res.end() - } - - // Send a plain JSON response (not SSE). Some MCP clients (notably Codex's - // rmcp StreamableHttpClientWorker) treat the closing of the SSE stream as a - // transport-channel closure and fail when sending the follow-up - // `notifications/initialized` on the same worker. Returning a regular JSON - // response lets the HTTP request complete normally so the client can open a - // new request for the next message. - _sendJSON (res, data, sessionId) { - res.setHeader('Content-Type', 'application/json') - if (sessionId) { - res.setHeader('mcp-session-id', sessionId) - } - res.json(data) - } - - // Detect tasks-extension support from request params. Accepts both the - // initialize-time capabilities.extensions and the SEP-2663 per-request - // _meta["io.modelcontextprotocol/clientCapabilities"].extensions form. - _captureClientCaps (params) { - if (!params || typeof params !== 'object') { - return - } - const initExt = params.capabilities && params.capabilities.extensions - const metaExt = params._meta && - params._meta['io.modelcontextprotocol/clientCapabilities'] && - params._meta['io.modelcontextprotocol/clientCapabilities'].extensions - for (const ext of [initExt, metaExt]) { - if (ext && typeof ext === 'object' && 'io.modelcontextprotocol/tasks' in ext) { - this.clientSupportsTasks = true - } - } - } - - async _handleTasksGet (request) { - if (!this.server.taskManager) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32601, message: 'Tasks extension not enabled' } - } - } - const taskId = request.params && request.params.taskId - if (!taskId) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32602, message: 'Missing required param: taskId' } - } - } - try { - const task = await this.server.taskManager.get(taskId) - return { - jsonrpc: '2.0', - id: request.id, - result: task - } - } catch (error) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32602, message: error.message } - } - } - } - - async _handleTasksCancel (request) { - if (!this.server.taskManager) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32601, message: 'Tasks extension not enabled' } - } - } - const taskId = request.params && request.params.taskId - if (!taskId) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32602, message: 'Missing required param: taskId' } - } - } - try { - const task = await this.server.taskManager.cancel(taskId) - return { - jsonrpc: '2.0', - id: request.id, - result: task - } - } catch (error) { - return { - jsonrpc: '2.0', - id: request.id, - error: { code: -32602, message: error.message } - } - } - } - - async handleRequest (req, res, body) { - if (body) { - const request = body - let result - if (request.method === 'initialize') { - this._captureClientCaps(request.params) - const versions = this.server.supportedProtocolVersions || ['2024-11-05'] - const requested = request.params && request.params.protocolVersion - const protocolVersion = versions.includes(requested) ? requested : versions[0] - const capabilities = { - tools: { - listChanged: false - } - } - if (this.server.taskManager) { - capabilities.extensions = { - 'io.modelcontextprotocol/tasks': {} - } - } - result = { - jsonrpc: '2.0', - id: request.id, - result: { - protocolVersion, - capabilities, - serverInfo: { - name: this.server.name, - version: this.server.version - } - } - } - // Use a plain JSON response for initialize so the HTTP request - // completes cleanly. Clients like Codex/rmcp treat the closing of - // an SSE stream as a transport-channel closure and then fail when - // trying to send `notifications/initialized` on the same worker. - this._sendJSON(res, result, this.sessionId) - return - } else if (request.method === 'notifications/initialized') { - this.initialized = true - // Per MCP Streamable HTTP spec, notifications MUST be answered with - // 202 Accepted and no body. A 200 with an empty body (and thus no - // Content-Type) is treated as a fatal UnexpectedContentType error by - // Codex's rmcp HTTP adapter, killing the transport during handshake. - res.status(202).end() - return - } else if (request.method === 'tools/list') { - const tools = Array.from(this.server.tools.entries()).map(([name, { description, inputSchema }]) => ({ - name, - description, - inputSchema: zodToJsonSchema(inputSchema) - })) - result = { - jsonrpc: '2.0', - id: request.id, - result: { tools } - } - } else if (request.method === 'tools/call') { - this._captureClientCaps(request.params) - const { name, arguments: args } = request.params - const tool = this.server.tools.get(name) - if (tool) { - try { - const toolResult = await tool.handler(args, { - clientSupportsTasks: this.clientSupportsTasks && !!this.server.taskManager, - taskManager: this.server.taskManager - }) - result = { - jsonrpc: '2.0', - id: request.id, - result: toolResult - } - } catch (error) { - result = { - jsonrpc: '2.0', - id: request.id, - result: { - content: [{ type: 'text', text: error.message }], - isError: true - } - } - } - } else { - result = { - jsonrpc: '2.0', - id: request.id, - error: { code: -32601, message: `Tool not found: ${name}` } - } - } - } else if (request.method === 'tasks/get') { - result = await this._handleTasksGet(request) - } else if (request.method === 'tasks/cancel') { - result = await this._handleTasksCancel(request) - } else if (request.method === 'tasks/list' || request.method === 'tasks/update' || request.method === 'tasks/result') { - // tasks/list is unsafe without an authorization context, and - // tasks/update / tasks/result are only needed for input_required - // flows — intentionally not implemented (SEP-2663). - result = { - jsonrpc: '2.0', - id: request.id, - error: { code: -32601, message: `Method not implemented: ${request.method}` } - } - } else if (request.method === 'ping') { - result = { - jsonrpc: '2.0', - id: request.id, - result: {} - } - } else { - result = { - jsonrpc: '2.0', - id: request.id, - error: { code: -32601, message: `Method not found: ${request.method}` } - } - } - // For JSON-RPC requests with an id (requires a response), return a - // plain JSON body. This is the most broadly compatible approach — - // some clients (Codex/rmcp, Claude Agent SDK) handle plain JSON - // responses more reliably than short-lived SSE streams. - if (request.id === undefined || request.id === null) { - // Notification — client does not expect a result, just an ack. - // 202 Accepted per MCP Streamable HTTP spec (see note above). - res.status(202).end() - return - } - this._sendJSON(res, result) - } else { - if (req.method === 'DELETE') { - this.close() - res.status(200).end() - } else if (req.method === 'GET') { - // Open a no-op SSE listening stream per MCP Streamable HTTP spec. - // The server does not currently push server-initiated messages, - // but keeping the stream alive with heartbeats satisfies strict - // clients (e.g. Claude Agent SDK) that require a valid SSE stream. - res.setHeader('Content-Type', 'text/event-stream') - res.setHeader('Cache-Control', 'no-cache') - res.setHeader('Connection', 'keep-alive') - res.status(200) - // Send an initial SSE comment to flush headers - res.write(': ping\n\n') - // Heartbeat every 15s to keep the connection alive - const heartbeat = setInterval(() => { - res.write(': ping\n\n') - }, 15000) - // Clean up when the client disconnects - req.on('close', () => { - clearInterval(heartbeat) - }) - } else { - res.status(200).end() - } - } - } - - async close () { - if (this.onclose) this.onclose() - } -} - -export { StreamableHTTPServerTransport } diff --git a/src/app/mcp/server/tasks.js b/src/app/mcp/server/tasks.js deleted file mode 100644 index 04b482b..0000000 --- a/src/app/mcp/server/tasks.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * MCP Tasks extension (SEP-2663) — server-side task lifecycle manager. - * - * A Task is a durable handle for a long-running tool call. The server - * decides per-request whether to materialize a task; clients poll with - * tasks/get and may send tasks/cancel. Status machine: - * - * working ──► completed (task.result set) - * ──► failed (task.error set) - * ──► cancelled (cooperative; work may not stop) - * - * `input_required` and tasks/update are intentionally not implemented (v1). - * tasks/list is intentionally not implemented — without an authorization - * context per SEP-2663 guidance, listing tasks is unsafe. - * - * Hooks (all optional, async): - * onGet(task) — refresh a working task's state before returning it - * onCancel(task) — perform the real cancellation (kill remote process) - * onSweep(task) — clean up resources when a terminal task is swept - */ - -import uid from '../../common/uid.js' - -const STATUS = { - working: 'working', - completed: 'completed', - failed: 'failed', - cancelled: 'cancelled' -} - -const TERMINAL_STATUSES = new Set([ - STATUS.completed, - STATUS.failed, - STATUS.cancelled -]) - -class TaskManager { - constructor (options = {}) { - this.tasks = new Map() - this.ttl = options.ttl > 0 ? options.ttl : 3600000 - this.pollIntervalMs = options.pollIntervalMs > 0 ? options.pollIntervalMs : 2000 - this.maxTasks = options.maxTasks > 0 ? options.maxTasks : 100 - this.onGet = null - this.onCancel = null - this.onSweep = null - this._sweepTimer = setInterval(() => { - this.sweep().catch(() => {}) - }, Math.min(this.ttl, 60000)) - if (typeof this._sweepTimer.unref === 'function') { - this._sweepTimer.unref() - } - } - - // Create a task in `working` state. meta holds server-private linkage - // (e.g. the renderer background task id) and is never sent to clients. - create ({ toolName, meta } = {}) { - if (this.tasks.size >= this.maxTasks) { - this._evictOldest() - } - const taskId = `task-${uid()}` - const task = { - taskId, - status: STATUS.working, - createdAt: new Date().toISOString(), - endedAt: null, - ttl: this.ttl, - pollIntervalMs: this.pollIntervalMs, - statusMessage: toolName ? `Started ${toolName}` : 'Task started', - toolName: toolName || null, - result: null, - error: null, - meta: meta || {} - } - this.tasks.set(taskId, task) - return task - } - - _evictOldest () { - // Prefer evicting the oldest terminal task; fall back to oldest overall - let oldestTerminal = null - let oldest = null - for (const task of this.tasks.values()) { - if (!oldest || task.createdAt < oldest.createdAt) oldest = task - if (TERMINAL_STATUSES.has(task.status) && - (!oldestTerminal || task.createdAt < oldestTerminal.createdAt)) { - oldestTerminal = task - } - } - const victim = oldestTerminal || oldest - if (victim) { - this.tasks.delete(victim.taskId) - if (this.onSweep) { - Promise.resolve(this.onSweep(victim)).catch(() => {}) - } - } - } - - // tasks/get — refreshes working tasks via onGet before returning. - // Throws on unknown task id (transport maps this to JSON-RPC -32602). - async get (taskId) { - const task = this.tasks.get(taskId) - if (!task) { - throw new Error(`Unknown task: ${taskId}`) - } - if (task.status === STATUS.working && this.onGet) { - await this.onGet(task) - } - return this.toWire(task) - } - - // tasks/cancel — cooperative: runs the onCancel hook, then marks the - // task cancelled. Cancelling a terminal task is a no-op per spec. - async cancel (taskId) { - const task = this.tasks.get(taskId) - if (!task) { - throw new Error(`Unknown task: ${taskId}`) - } - if (!TERMINAL_STATUSES.has(task.status)) { - if (this.onCancel) { - await this.onCancel(task) - } - this._markCancelled(task) - } - return this.toWire(task) - } - - // Mark cancelled without invoking the onCancel hook — used when the - // underlying execution already reported a cancelled state. - cancelLocal (taskId) { - const task = this.tasks.get(taskId) - if (task && !TERMINAL_STATUSES.has(task.status)) { - this._markCancelled(task) - } - return task || null - } - - _markCancelled (task) { - task.status = STATUS.cancelled - task.statusMessage = 'Task cancelled' - task.endedAt = new Date().toISOString() - } - - complete (taskId, result) { - const task = this.tasks.get(taskId) - if (!task || TERMINAL_STATUSES.has(task.status)) { - return task || null - } - task.status = STATUS.completed - task.statusMessage = 'Task completed' - task.result = result - task.endedAt = new Date().toISOString() - return task - } - - fail (taskId, message) { - const task = this.tasks.get(taskId) - if (!task || TERMINAL_STATUSES.has(task.status)) { - return task || null - } - task.status = STATUS.failed - task.statusMessage = 'Task failed' - task.error = { - code: -32603, - message: message || 'Task execution failed' - } - task.endedAt = new Date().toISOString() - return task - } - - // Client-facing wire shape — never leaks server-private `meta`. - toWire (task) { - const wire = { - taskId: task.taskId, - status: task.status, - createdAt: task.createdAt, - ttl: task.ttl, - pollIntervalMs: task.pollIntervalMs, - statusMessage: task.statusMessage - } - if (task.status === STATUS.completed && task.result !== null) { - wire.result = task.result - } - if (task.status === STATUS.failed && task.error) { - wire.error = task.error - } - return wire - } - - // Remove terminal tasks whose retention TTL has expired. - async sweep () { - const now = Date.now() - for (const task of Array.from(this.tasks.values())) { - if (!TERMINAL_STATUSES.has(task.status) || !task.endedAt) { - continue - } - if (now - Date.parse(task.endedAt) > this.ttl) { - this.tasks.delete(task.taskId) - if (this.onSweep) { - try { - await this.onSweep(task) - } catch (_) { - // best-effort cleanup - } - } - } - } - } - - destroy () { - if (this._sweepTimer) { - clearInterval(this._sweepTimer) - this._sweepTimer = null - } - this.tasks.clear() - } -} - -export { TaskManager, STATUS, TERMINAL_STATUSES } diff --git a/src/app/routes/file-transfer.js b/src/app/routes/file-transfer.js deleted file mode 100644 index 445178b..0000000 --- a/src/app/routes/file-transfer.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * file download/upload routes - */ - -import multer from 'multer' -import fs from 'fs' -import path, { resolve } from 'path' -import { spawn } from 'child_process' -import { - jwtAuth, - errHandler -} from '../lib/jwt.js' - -const uploadDir = resolve(process.env.DB_PATH || resolve(process.cwd(), 'data'), 'uploads') -fs.mkdirSync(uploadDir, { recursive: true }) -const upload = multer({ dest: uploadDir }) - -export function fileTransferRoutes (app) { - app.get('/api/download', jwtAuth, errHandler, (req, res) => { - const filePath = req.query.path - if (!filePath) { - return res.status(400).json({ error: 'path is required' }) - } - try { - const stat = fs.statSync(filePath) - if (stat.isFile()) { - const fileName = path.basename(filePath) - res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`) - res.setHeader('Content-Type', 'application/octet-stream') - fs.createReadStream(filePath).pipe(res) - } else if (stat.isDirectory()) { - const dirName = path.basename(filePath) - const parentDir = path.dirname(filePath) - res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`) - res.setHeader('Content-Type', 'application/gzip') - const tar = spawn('tar', ['czf', '-', '-C', parentDir, dirName]) - tar.stdout.pipe(res) - tar.stderr.on('data', (data) => { - console.error('tar stderr:', data.toString()) - }) - tar.on('error', (err) => { - console.error('tar error:', err) - if (!res.headersSent) { - res.status(500).json({ error: err.message }) - } - }) - } else { - res.status(400).json({ error: 'path is not a file or directory' }) - } - } catch (err) { - console.error('download error:', err) - res.status(500).json({ error: err.message }) - } - }) - - app.post('/api/upload', jwtAuth, errHandler, upload.single('file'), (req, res) => { - const targetDir = req.body.path - if (!targetDir || !req.file) { - return res.status(400).json({ error: 'path and file are required' }) - } - try { - const originalName = Buffer.from(req.file.originalname, 'latin1').toString('utf8') - const destPath = path.join(targetDir, originalName) - fs.renameSync(req.file.path, destPath) - res.json({ success: true, path: destPath }) - } catch (err) { - console.error('upload error:', err) - res.status(500).json({ error: err.message }) - } - }) -} diff --git a/src/app/routes/http.js b/src/app/routes/http.js deleted file mode 100644 index e8ddd82..0000000 --- a/src/app/routes/http.js +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express' -import { login } from '../lib/login.js' -import { index } from '../lib/view.js' -import { getConstants } from '../lib/get-constants.js' -import { resolve } from 'path' -import { - cwd, - isDev -} from '../common/runtime-constants.js' -import { - jwtAuth, - errHandler -} from '../lib/jwt.js' -import { fileTransferRoutes } from './file-transfer.js' - -export function httpRoutes (app) { - app.get('/', index) - app.post('/api/login', login) - app.get('/api/get-constants', jwtAuth, errHandler, getConstants) - fileTransferRoutes(app) - if (isDev) { - app.use(express.static( - resolve(cwd, 'node_modules') - )) - app.use(express.static( - resolve(cwd, 'src/client/statics') - )) - } else { - app.use(express.static( - resolve(cwd, 'dist/assets') - )) - } -} diff --git a/src/app/routes/ws.js b/src/app/routes/ws.js deleted file mode 100644 index fcb55ec..0000000 --- a/src/app/routes/ws.js +++ /dev/null @@ -1,356 +0,0 @@ -import log from '../common/log.js' -import expressWs from 'express-ws' -import { - isWin -} from '../common/runtime-constants.js' -import { verifyWs, initWs } from '../server/dispatch-center.js' -import { - terminals, - cleanAllSessions -} from '../server/remote-common.js' -import { zmodemManager } from '../server/zmodem.js' -import { trzszManager } from '../server/trzsz.js' -import { xmodemManager } from '../server/xmodem.js' - -function cleanup () { - cleanAllSessions() -} - -// True when the buffered data ends mid-way through a multi-byte UTF-8 -// sequence (CJK chars are 3 bytes). Slow SSH servers (embedded router CLIs) -// often deliver one char split across TCP segments; flushing such a buffer -// right away would push a partial char to the client. Only the tail of the -// last buffer is inspected (at most 4 bytes), so this is O(1). -function hasIncompleteTrailingUtf8 (bufs) { - const last = bufs[bufs.length - 1] - if (!last) { - return false - } - const buf = Buffer.isBuffer(last) ? last : Buffer.from(last) - const len = buf.length - if (!len) { - return false - } - // Count trailing continuation bytes (10xxxxxx), at most 3 - let cont = 0 - while (cont < 3 && cont < len && (buf[len - 1 - cont] & 0xc0) === 0x80) { - cont++ - } - const leadIdx = len - 1 - cont - if (leadIdx < 0) { - // Whole buffer is continuation bytes; the lead byte was in a chunk that - // was already flushed, so holding can not reassemble anything. - return false - } - const lead = buf[leadIdx] - if (lead < 0xc0) { - // ASCII last byte, or stray continuations after ASCII: nothing to wait for - return false - } - // Expected continuation count for this lead byte: - // 110xxxxx -> 1, 1110xxxx -> 2, 11110xxx -> 3 - const needed = lead < 0xe0 ? 1 : lead < 0xf0 ? 2 : 3 - return cont < needed -} - -export function wsRoutes (app) { - expressWs(app, undefined, { - wsOptions: { - perMessageDeflate: false - } - }) - app.ws('/spice/:pid', function (ws, req) { - const { query } = req - verifyWs(req) - const { pid } = req.params - const term = terminals(pid) - log.debug('ws: connected to spice session ->', pid) - term.start(query, ws) - ws.on('error', (err) => { - log.error(err) - }) - }) - app.ws('/terminals/:pid', function (ws, req) { - verifyWs(req) - const term = terminals(req.params.pid) - const { pid } = term - log.debug('ws: connected to terminal ->', pid) - - const dataBuffer = [] - let sendTimeout = null - // Time of the last actual flush. Lets a chunk arriving after an idle gap - // (keystroke echo, command result) skip the coalescing delay entirely, - // so only chunks arriving inside an active burst (floods) pay the 10ms - // wait. Mirrors the client-side coalescing fast path. - let lastFlushTime = 0 - const flushIntervalMs = 10 - - // Auto-trigger XMODEM when the serial device sends a marker message. - // The serial-shell.js sends these markers when the user types tx/rx. - function detectXmodemMarker (text) { - const txMatch = text.match(/\[XMODEM:TX:(.+?)\]/) - if (txMatch) { - ws.s({ - action: 'xmodem-event', - event: 'auto-trigger-receive', - name: txMatch[1] - }) - return - } - const rxMatch = text.match(/\[XMODEM:RX\]/) - if (rxMatch) { - ws.s({ - action: 'xmodem-event', - event: 'auto-trigger-send' - }) - } - } - - const flushBufferedData = () => { - if (!dataBuffer.length) { - sendTimeout = null - return - } - lastFlushTime = Date.now() - const combinedData = Buffer.concat(dataBuffer.splice(0).map(d => Buffer.isBuffer(d) ? d : Buffer.from(d))) - - // Write to log (keep this) - term.writeLog(combinedData) - - // Detect XMODEM auto-trigger markers from serial device - if (term.port) { - detectXmodemMarker(combinedData.toString('utf8')) - } - - // Check for zmodem escape sequence before sending to client - const zmodemConsumed = zmodemManager.handleData(pid, combinedData, term, ws) - if (zmodemConsumed) { - sendTimeout = null - return - } - - // Check for trzsz magic key before sending to client - const trzszConsumed = trzszManager.handleData(pid, combinedData, term, ws) - if (trzszConsumed) { - sendTimeout = null - return - } - - // Check for xmodem protocol before sending to client - const xmodemConsumed = xmodemManager.handleData(pid, combinedData, term, ws) - if (xmodemConsumed) { - sendTimeout = null - return - } - - // Not zmodem, trzsz, or xmodem data, send to WebSocket - ws.send(combinedData) - sendTimeout = null - } - - // Create ws.s function for zmodem to send messages to client - ws.s = (data) => { - ws.send(JSON.stringify(data)) - } - - // In the WebSocket setup, replace the data handler: - term.on('data', function (data) { - // Check if zmodem session is active and handle data - if (zmodemManager.isActive(pid)) { - // Let zmodem handle the data, but still log it - term.writeLog(data) - zmodemManager.handleData(pid, data, term, ws) - return - } - - // Check if trzsz session is active and handle data - if (trzszManager.isActive(pid)) { - // Let trzsz handle the data, but still log it - term.writeLog(data) - trzszManager.handleData(pid, data, term, ws) - return - } - - // Check if xmodem session is active and handle data. - // For serial terminals (term.port exists) a raw port listener (registered below) - // bypasses rxLineEnding transformation and feeds raw bytes to xmodem. - if (xmodemManager.isActive(pid)) { - if (!term.port) { - // Non-serial fallback (should not normally happen) - term.writeLog(data) - xmodemManager.handleData(pid, data, term, ws) - } - return - } - - // Detect XMODEM auto-trigger markers from serial device - if (term.port) { - const text = Buffer.isBuffer(data) ? data.toString('utf8') : data - detectXmodemMarker(text) - } - - const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data) - const shouldBypassBatch = chunk.length > 16384 - - // Bypass batching for very large chunks to avoid parser desync. - if (shouldBypassBatch) { - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - if (dataBuffer.length) { - flushBufferedData() - } - term.writeLog(chunk) - const zmodemConsumed = zmodemManager.handleData(pid, chunk, term, ws) - if (zmodemConsumed) { - return - } - const trzszConsumed = trzszManager.handleData(pid, chunk, term, ws) - if (trzszConsumed) { - return - } - const xmodemConsumed = xmodemManager.handleData(pid, chunk, term, ws) - if (xmodemConsumed) { - return - } - ws.send(chunk) - return - } - - // Buffer incoming data instead of sending immediately for normal text workload - dataBuffer.push(chunk) - - // Idle fast path: if nothing has been flushed within the coalescing - // window, this is the start of a new burst (or a lone interactive - // echo) rather than a continuation of a flood - send it right away - // instead of paying the fixed delay. Only chunks arriving while a - // burst is already in flight (elapsed < flushIntervalMs) get batched. - const elapsed = Date.now() - lastFlushTime - if (elapsed >= flushIntervalMs) { - // Never fast-flush a buffer that ends mid-way through a multi-byte - // UTF-8 char: a slow peer (router CLI) may deliver one char split - // across TCP segments, and the remaining bytes usually land within a - // few ms. Hold one coalescing window so they get concatenated first - // (the completing chunk then flushes immediately via this same fast - // path). Bounded by the timeout, so it can not stick. - if (hasIncompleteTrailingUtf8(dataBuffer)) { - if (!sendTimeout) { - sendTimeout = setTimeout(flushBufferedData, flushIntervalMs) - } - return - } - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - flushBufferedData() - return - } - - // If no timeout is pending, schedule a batched send - if (!sendTimeout) { - sendTimeout = setTimeout(flushBufferedData, flushIntervalMs - elapsed) - } - }) - - // For serial terminals, register a raw data listener directly on the port to - // feed binary XMODEM data to xmodemManager without rxLineEnding transformation. - if (term.port) { - term.port.on('data', function (rawData) { - if (xmodemManager.isActive(pid)) { - term.writeLog(rawData) - xmodemManager.handleData(pid, rawData, term, ws) - } - }) - } - - function onClose () { - // Cancel any pending batched send - if (sendTimeout) { - clearTimeout(sendTimeout) - sendTimeout = null - } - // Clean up zmodem session - zmodemManager.destroySession(pid) - // Clean up trzsz session - trzszManager.destroySession(pid) - // Clean up xmodem session - xmodemManager.destroySession(pid) - term.kill() - log.debug('Closed terminal ' + pid) - // Clean things up - ws.close && ws.close() - cleanup() - } - - term.on('close', onClose) - if (term.isLocal && isWin) { - term.on('exit', onClose) - } - - ws.on('message', function (msg) { - try { - // Check if message is a zmodem or trzsz control message (JSON) - if (typeof msg === 'string') { - try { - const parsed = JSON.parse(msg) - if (parsed.action === 'zmodem-event') { - zmodemManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'trzsz-event') { - trzszManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'xmodem-event') { - xmodemManager.handleMessage(pid, parsed, term, ws) - return - } - if (parsed.action === 'keepalive') { - // Write \n to the PTY. In canonical mode the TTY line discipline - // only delivers data to read() when a newline completes the line, - // so \x00 (NUL) sits in the buffer and never wakes bash up. - // A newline wakes bash's read(), resets the TMOUT alarm, and bash - // simply re-displays the prompt. The client suppresses that echo. - term.write('\n\r\x1b[K') - return - } - } catch (e) { - // Not JSON, treat as regular terminal input - } - } - term.write(msg) - } catch (ex) { - log.error(ex) - } - }) - - ws.on('error', (err) => { - log.error(err) - }) - - ws.on('close', onClose) - }) - app.ws('/rdp/:pid', function (ws, req) { - const { width, height } = req.query - verifyWs(req) - const term = terminals(req.params.pid) - term.ws = ws - term.start(width, height) - const { pid } = term - log.debug('ws: connected to rdp session ->', pid) - ws.on('error', log.error) - }) - app.ws('/vnc/:pid', function (ws, req) { - const { query } = req - verifyWs(req) - const { pid } = req.params - const term = terminals(pid) - term.ws = ws - term.start(query) - log.debug('ws: connected to vnc session ->', pid) - ws.on('error', log.error) - }) - initWs(app) -} diff --git a/src/app/server/dispatch-center.js b/src/app/server/dispatch-center.js deleted file mode 100644 index 6e06126..0000000 --- a/src/app/server/dispatch-center.js +++ /dev/null @@ -1,276 +0,0 @@ -/** - * communication between webview and app - * run functions in seprate process, avoid using electron.remote directly - */ - -import { Sftp } from './session-sftp.js' -import { Ftp } from './session-ftp.js' -import { instSftpKeys } from '../common/constants.js' -import { - sftp, - transfer, - onDestroySftp, - onDestroyTransfer -} from './remote-common.js' -import { Transfer, transferKeys } from './transfer.js' -import { FtpTransfer } from './ftp-transfer.js' -import { Upgrade } from './download-upgrade.js' -import fs from './fs.js' -import log from '../common/log.js' -import fetch from './fetch.js' -import sync from './sync.js' -import { verify } from '../lib/jwt.js' -import { runSync } from '../lib/run-sync.js' -import { - createTerm, - testTerm, - resize, - runCmd, - execCmd, - toggleTerminalLog, - toggleTerminalLogTimestamp, - setTerminalLogPath, - startTerminalLogFile -} from './terminal-api.js' -import globalState from './global-state.js' - -const { - SERVER_USER -} = process.env - -/** - * add ws.s function - * @param {*} ws - */ -const wsDec = (ws) => { - ws.s = msg => { - try { - ws.send(JSON.stringify(msg)) - } catch (e) { - log.error('ws send error') - log.error(e) - } - } - ws.on('error', log.error) - ws.once = (callack, id) => { - const func = (evt) => { - const arg = JSON.parse(evt.data) - if (id === arg.id) { - callack(arg) - ws.removeEventListener('message', func) - } - } - ws.addEventListener('message', func) - } - ws._socket.setKeepAlive(true, 30 * 1000) -} - -export function verifyWs (req) { - const { token } = req.query - const data = verify(token) - if (SERVER_USER !== data.id) { - throw new Error('not valid request') - } -} - -export function initWs (app) { - // sftp function - app.ws('/sftp/:id', (ws, req) => { - verifyWs(req) - wsDec(ws) - const { id } = req.params - ws.on('close', () => { - onDestroySftp(id) - }) - ws.on('message', (message) => { - const msg = JSON.parse(message) - const { action } = msg - - if (action === 'sftp-new') { - const { id, terminalId, type } = msg - const Cls = type === 'ftp' ? Ftp : Sftp - sftp(id, new Cls({ - uid: id, - terminalId, - type - })) - } else if (action === 'sftp-func') { - const { id, args, func, uid } = msg - const inst = sftp(id) - if (inst) { - if (!instSftpKeys.includes(func) || typeof inst[func] !== 'function') { - ws.s({ - id: uid, - error: { - message: 'invalid sftp function: ' + func, - stack: '' - } - }) - return - } - inst[func](...args) - .then(data => { - ws.s({ - id: uid, - data - }) - }) - .catch(err => { - ws.s({ - id: uid, - error: { - message: err.message, - stack: err.stack - } - }) - }) - } - } else if (action === 'sftp-destroy') { - const { id } = msg - ws.close() - onDestroySftp(id) - } - }) - // end - }) - - // transfer function - app.ws('/transfer/:id', (ws, req) => { - verifyWs(req) - wsDec(ws) - const { id } = req.params - const { sftpId } = req.query - ws.on('close', () => { - onDestroyTransfer(id, sftpId) - }) - ws.on('message', (message) => { - const msg = JSON.parse(message) - const { action } = msg - - if (action === 'transfer-new') { - const { sftpId, id, isFtp } = msg - const session = sftp(sftpId) - const encode = session.initOptions?.encode || 'utf8' - const opts = Object.assign({}, msg, { - sftp: session.sftp, - conn: session.client, - ftpSession: isFtp ? session : null, - sftpId, - ws, - encode - }) - const Cls = isFtp ? FtpTransfer : Transfer - transfer(id, sftpId, new Cls(opts)) - } else if (action === 'transfer-func') { - const { id, func, args, sftpId } = msg - if (func === 'destroy') { - return onDestroyTransfer(id, sftpId) - } - if (!transferKeys.includes(func)) { - return - } - const tr = transfer(id, sftpId) - if (!tr || typeof tr[func] !== 'function') { - return - } - tr[func](...args) - } - }) - // end - }) - - // upgrade - app.ws('/upgrade/:id', (ws, req) => { - verifyWs(req) - wsDec(ws) - const { id } = req.params - ws.on('close', () => { - const inst = globalState.getUpgradeInst(id) - if (inst) { - inst.destroy() - } - }) - ws.on('message', async (message) => { - try { - const msg = JSON.parse(message) - const { action } = msg - - if (action === 'upgrade-new') { - const { id } = msg - const opts = Object.assign({}, msg, { - ws - }) - const inst = new Upgrade(opts) - globalState.setUpgradeInst(id, inst) - await inst.init() - } else if (action === 'upgrade-func') { - const { id, func, args } = msg - const inst = globalState.getUpgradeInst(id) - if (!inst) { - return - } - if (!transferKeys.includes(func) || typeof inst[func] !== 'function') { - log.error('invalid upgrade function:', func) - return - } - inst[func](...args) - } - } catch (err) { - log.error('upgrade ws error', err) - } - }) - // end - }) - - // common functions - app.ws('/common/s', (ws, req) => { - verifyWs(req) - wsDec(ws) - globalState.setCommonWs(ws) - ws.on('message', async (message) => { - try { - const msg = JSON.parse(message) - const { action, body = {}, id } = msg - if (action === 'fetch') { - fetch(ws, msg) - } else if (action === 'sync') { - sync(ws, msg) - } else if (action === 'fs') { - fs(ws, msg) - } else if (action === 'create-terminal') { - if (body.termType === 'ftp') { - ws.s({ - id, - data: { - pid: 'ok' - } - }) - return - } - createTerm(ws, msg) - } else if (action === 'test-terminal') { - testTerm(ws, msg) - } else if (action === 'resize-terminal') { - resize(ws, msg) - } else if (action === 'toggle-terminal-log') { - toggleTerminalLog(ws, msg) - } else if (action === 'toggle-terminal-log-timestamp') { - toggleTerminalLogTimestamp(ws, msg) - } else if (action === 'set-terminal-log-path') { - setTerminalLogPath(ws, msg) - } else if (action === 'start-terminal-log-file') { - startTerminalLogFile(ws, msg) - } else if (action === 'run-cmd') { - runCmd(ws, msg) - } else if (action === 'exec-cmd') { - execCmd(ws, msg) - } if (action === 'runSync') { - runSync(ws, msg) - } - } catch (e) { - log.error(e) - } - }) - }) - // end -} diff --git a/src/app/server/download-upgrade.js b/src/app/server/download-upgrade.js deleted file mode 100644 index e7bc7d2..0000000 --- a/src/app/server/download-upgrade.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * download upgrade class - * - * Ported from the desktop electerm source. Adapted for the - * electerm-android ESM backend: - * - ESM imports - * - message ids aligned with the @electerm/electerm-react client - * contract (upgrade:data / upgrade:end / upgrade:err) - * - `process.send` (Electron IPC) replaced with `showItemInFolder` - * so it works under the on-device Node runtime - */ - -import fs from 'fs' -import { resolve } from 'path' -import axios from 'axios' -import _ from 'lodash' -import { packInfo, tempDir } from '../common/runtime-constants.js' -import installSrc from '../lib/install-src.js' -import { fsExport } from '../lib/fs.js' -import { createProxyAgent } from '../lib/proxy-agent.js' -import { showItemInFolder } from '../lib/show-item-in-folder.js' -import log from '../common/log.js' -import globalState from './global-state.js' - -axios.defaults.proxy = false -const { openFile, rmrf } = fsExport - -function getUrl (url, mirror) { - if (mirror === 'gh-proxy') { - return `https://electerm-mirror.html5beta.com/${url}` - } if (mirror === 'sourceforge') { - const arr = url.split('/') - const len = arr.length - return `https://master.dl.sourceforge.net/project/electerm.mirror/${arr[len - 2]}/${arr[len - 1]}?viasf=1` - } else if (mirror === 'r2') { - return `https://electerm-store.html5beta.com/r/${url.split('/').pop()}` - } else { - return url - } -} - -function getReleaseInfo (filter, releaseInfoUrl, agent) { - const conf = { - url: releaseInfoUrl, - timeout: 15000 - } - if (agent) { - conf.httpsAgent = agent - } - return axios(conf) - .then((res) => { - return res.data - .release - .assets - .filter(filter)[0] - }) -} - -class Upgrade { - constructor (options) { - this.options = options - } - - async init () { - const { - id, - ws, - proxy, - mirror - } = this.options - // register id early so destroy() works even if init() is aborted - this.id = id - const agent = createProxyAgent(proxy) - const releaseInfoUrl = `${packInfo.homepage}/data/electerm-github-release.json?_=${+new Date()}` - const filter = r => { - return r.name.includes(installSrc) - } - const releaseInfo = await getReleaseInfo(filter, releaseInfoUrl, agent) - .catch(err => this.onError(err, id, ws)) - if (!releaseInfo) { - return - } - const localPath = resolve(tempDir, releaseInfo.name) - const remotePath = getUrl(releaseInfo.browser_download_url, mirror) - await rmrf(localPath).catch(log.error) - const { size } = releaseInfo - this.localPath = localPath - const readSteam = await axios({ - url: remotePath, - httpsAgent: agent, - responseType: 'stream' - }) - .then(r => r.data) - .catch(err => { - this.onError(err, id, ws) - }) - if (!readSteam) { - return - } - const writeSteam = fs.createWriteStream(localPath) - - let count = 0 - - this.pausing = false - - this.onData = _.throttle((count) => { - if (this.onDestroy) { - return - } - - ws.s({ - id: 'upgrade:data:' + id, - data: Math.floor(count * 100 / size) - }) - }, 1000) - - readSteam.on('data', chunk => { - const res = writeSteam.write(chunk) - if (res) { - count += chunk.length - this.onData(count) - } else { - readSteam.pause() - writeSteam.once('drain', () => { - count += chunk.length - this.onData(count) - if (!this.pausing) { - readSteam.resume() - } - }) - } - }) - - readSteam.on('close', () => { - writeSteam.end('', () => this.onEnd(id, ws)) - }) - - readSteam.on('error', (err) => this.onError(err, id, ws)) - - this.readSteam = readSteam - this.writeSteam = writeSteam - this.ws = ws - this.destroy = this.destroy.bind(this) - } - - onEnd (id, ws) { - if (this.onDestroy) { - return - } - openFile(this.localPath).catch(log.error) - // showItemInFolder(this.localPath).catch(log.error) - ws.s({ - id: 'upgrade:end:' + id, - data: this.localPath - }) - } - - onError (err, id, ws) { - ws.s({ - id: 'upgrade:err:' + id, - error: { - message: err.message, - stack: err.stack - } - }) - } - - pause () { - this.pausing = true - this.readSteam.pause() - } - - resume () { - this.pausing = false - this.readSteam.resume() - } - - destroy () { - this.onDestroy = true - this.readSteam && this.readSteam.destroy() - this.ws && this.ws.close() - globalState.removeUpgradeInst(this.id) - } - - // end -} - -export { Upgrade } diff --git a/src/app/server/fetch.js b/src/app/server/fetch.js deleted file mode 100644 index 587d6de..0000000 --- a/src/app/server/fetch.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * node fetch in server side - */ - -import rp from 'axios' -import { createProxyAgent } from '../lib/proxy-agent.js' - -rp.defaults.proxy = false - -function fetch (options) { - return rp(options) - .then((res) => { - return res.data - }) - .catch(error => { - return { - error - } - }) -} - -export default async function wsFetchHandler (ws, msg) { - const { id, options, proxy } = msg - const agent = createProxyAgent(proxy) - if (agent) { - options.httpAgent = agent - options.httpsAgent = agent - } else { - options.proxy = false - } - const res = await fetch(options) - if (res.error) { - console.log(res.error) - ws.s({ - error: res.error.message, - id - }) - } else { - ws.s({ - data: res, - id - }) - } -} diff --git a/src/app/server/fs.js b/src/app/server/fs.js deleted file mode 100644 index 1fd26d9..0000000 --- a/src/app/server/fs.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * fs in child process - */ - -import { fsExport as fs } from '../lib/fs.js' - -export default function handleFs (ws, msg) { - const { id, args, func } = msg - // only dispatch to fs helpers defined on the export itself, never to - // anything reached through the prototype chain - if (!Object.prototype.hasOwnProperty.call(fs, func) || typeof fs[func] !== 'function') { - return ws.s({ - id, - error: { - message: 'invalid fs function: ' + func, - stack: '' - } - }) - } - fs[func](...args) - .then(data => { - ws.s({ - id, - data - }) - }) - .catch(err => { - ws.s({ - id, - error: { - message: err.message, - stack: err.stack - } - }) - }) -} diff --git a/src/app/server/ftp-client.js b/src/app/server/ftp-client.js deleted file mode 100644 index 5fc4d61..0000000 --- a/src/app/server/ftp-client.js +++ /dev/null @@ -1,165 +0,0 @@ -import ftp from 'basic-ftp' -import iconv from 'iconv-lite' - -export class FtpClientWrapper { - constructor () { - this.client = new ftp.Client() - this.queue = Promise.resolve() - this.encoding = 'utf-8' - } - - async access (options) { - return this.enqueue(async () => { - if (options.proxy) { - return this._accessViaProxy(options) - } - const { proxy, readyTimeout, ...ftpOptions } = options - return this.client.access(ftpOptions) - }) - } - - async _accessViaProxy (options) { - const proxySock = require('./socks') - const { FTPError } = require('basic-ftp') - const proxyResult = await proxySock({ - readyTimeout: options.readyTimeout || 10000, - host: options.host, - port: options.port || 21, - proxy: options.proxy - }) - const ftpClient = this.client - ftpClient.ftp.reset() - ftpClient.ftp.socket = proxyResult.socket - // Wait for FTP welcome response (mirrors Client._handleConnectResponse) - const welcome = await ftpClient.ftp.handle(undefined, (res, task) => { - if (res instanceof Error) { - task.reject(res) - } else if (res.code >= 200 && res.code < 300) { - task.resolve(res) - } else { - task.reject(new FTPError(res)) - } - }) - if (options.secure === true) { - const secureOptions = { ...(options.secureOptions || {}) } - secureOptions.host = secureOptions.host || options.host - await ftpClient.useTLS(secureOptions) - } - await ftpClient.sendIgnoringError('OPTS UTF8 ON') - await ftpClient.login(options.user || 'anonymous', options.password || 'guest') - await ftpClient.useDefaultSettings() - return welcome - } - - setEncoding (encoding) { - this.encoding = encoding || 'utf-8' - // When using non-UTF-8 encoding, set the FTP control connection to use latin1 (binary) - // This prevents the library from incorrectly decoding the server's response - if (this.encoding !== 'utf-8') { - this.client.ftp.encoding = 'latin1' - } - } - - decodeString (str) { - if (!str) { - return str - } - if (this.encoding === 'utf-8') { - return str - } - try { - // Convert the latin1 string back to buffer, then decode with target encoding - const buf = Buffer.from(str, 'latin1') - return iconv.decode(buf, this.encoding) - } catch (e) { - return str - } - } - - encodeString (str) { - if (!str) { - return str - } - if (this.encoding === 'utf-8') { - return str - } - try { - // Encode with target encoding, then convert to latin1 string for FTP commands - const buf = iconv.encode(str, this.encoding) - return buf.toString('latin1') - } catch (e) { - return str - } - } - - async enqueue (fn) { - this.queue = this.queue.then(() => fn(), () => fn()) - return this.queue - } - - set verbose (value) { - this.client.ftp.verbose = value - } - - get verbose () { - return this.client.ftp.verbose - } - - async pwd () { - const result = await this.enqueue(() => this.client.pwd()) - return this.decodeString(result) - } - - async removeDir (path) { - const encodedPath = this.encodeString(path) - return this.enqueue(() => this.client.removeDir(encodedPath)) - } - - async remove (path) { - const encodedPath = this.encodeString(path) - return this.enqueue(() => this.client.remove(encodedPath)) - } - - async ensureDir (path) { - const encodedPath = this.encodeString(path) - return this.enqueue(() => this.client.ensureDir(encodedPath)) - } - - async list (path) { - const encodedPath = this.encodeString(path) - const result = await this.enqueue(() => this.client.list(encodedPath)) - return result.map(item => ({ - ...item, - name: this.decodeString(item.name) - })) - } - - async rename (path, newPath) { - const encodedPath = this.encodeString(path) - const encodedNewPath = this.encodeString(newPath) - return this.enqueue(() => this.client.rename(encodedPath, encodedNewPath)) - } - - async close () { - return this.enqueue(() => this.client.close()) - } - - async uploadFrom (readable, remotePath) { - const encodedPath = this.encodeString(remotePath) - return this.enqueue(() => this.client.uploadFrom(readable, encodedPath)) - } - - async downloadTo (writable, remotePath) { - const encodedPath = this.encodeString(remotePath) - return this.enqueue(() => this.client.downloadTo(writable, encodedPath)) - } - - async cd (path) { - const encodedPath = this.encodeString(path) - return this.enqueue(() => this.client.cd(encodedPath)) - } - - trackProgress (handler) { - return this.client.trackProgress(handler) - } -} diff --git a/src/app/server/ftp-file.js b/src/app/server/ftp-file.js deleted file mode 100644 index 7b192e5..0000000 --- a/src/app/server/ftp-file.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Readable, Writable } from 'stream' - -export async function readRemoteFile (client, remotePath) { - return new Promise((resolve, reject) => { - let data = '' - const writable = new Writable({ - write (chunk, encoding, callback) { - data += chunk.toString() - callback() - } - }) - - client.downloadTo(writable, remotePath) - .then(() => resolve(data)) - .catch(reject) - }) -} - -export async function writeRemoteFile (client, remotePath, str) { - const readable = new Readable({ - read () { - this.push(str) - this.push(null) - } - }) - - return client.uploadFrom(readable, remotePath) -} diff --git a/src/app/server/ftp-transfer.js b/src/app/server/ftp-transfer.js deleted file mode 100644 index 52c6010..0000000 --- a/src/app/server/ftp-transfer.js +++ /dev/null @@ -1,130 +0,0 @@ -// ftp-transfer.js -/** - * ftp transfer class - * Note: basic-ftp only supports one active transfer per client connection - */ - -export class FtpTransfer { - constructor ({ - remotePath, - localPath, - options = {}, - id, - type = 'download', - ftpSession, - sftpId, - ws - }) { - this.id = id - this.ftpSession = ftpSession - this.ftpClient = null - this.srcPath = type === 'download' ? remotePath : localPath - this.dstPath = type === 'download' ? localPath : remotePath - this.isUpload = type !== 'download' - this.ws = ws - this.pausing = false - this.onDestroy = false - this.total = 0 - this.startPromise = null - this.src = null - this.dst = null - this.start() - } - - handleProgress = (info) => { - if (this.pausing) return - const chunk = info.bytes - this.total - this.total = info.bytes - this.onData(this.total, chunk) - } - - onData = (total, chunk) => { - if (this.pausing) return - this.ws?.s({ - id: `transfer:data:${this.id}`, - data: total - }) - } - - onEnd = () => { - this.ws?.s({ - id: `transfer:end:${this.id}`, - data: null - }) - } - - onError = (err) => { - if (!err) { - return this.onEnd() - } - this.ws?.s({ - id: `transfer:err:${this.id}`, - error: { - message: err.message, - stack: err.stack - } - }) - } - - trackProgress = () => { - this.total = 0 - this.ftpClient?.trackProgress(this.handleProgress) - } - - async start () { - if (this.startPromise) { - return this.startPromise - } - this.startPromise = this.startTransfer() - return this.startPromise - } - - async startTransfer () { - try { - if (this.onDestroy) { - return - } - const ftpClient = await this.ftpSession.createOperationClient() - this.ftpClient = ftpClient - this.trackProgress() - if (!this.isUpload) { - await this.ftpClient.downloadTo(this.dstPath, this.srcPath) - } else { - await this.ftpClient.uploadFrom(this.srcPath, this.dstPath) - } - this.onEnd() - } catch (err) { - this.onError(err) - } finally { - const ftpClient = this.ftpClient - ftpClient?.trackProgress() - if (ftpClient) { - await ftpClient.close().catch(() => {}) - } - this.ftpClient = null - } - } - - pause () { - this.pausing = true - } - - resume () { - this.pausing = false - } - - destroy () { - this.onDestroy = true - if (this.ftpClient) { - this.ftpClient.trackProgress() // Remove progress tracking - this.ftpClient.close?.().catch?.(() => {}) - } - this.ftpClient = null - this.src = null - this.dst = null - if (this.ws) { - this.ws.close() - this.ws = null - } - } -} diff --git a/src/app/server/global-state.js b/src/app/server/global-state.js deleted file mode 100644 index 2713bc0..0000000 --- a/src/app/server/global-state.js +++ /dev/null @@ -1,51 +0,0 @@ -// global-state.js -class GlobalState { - #commonWs = null - #sessions = {} - #upgradeInsts = {} - - // Common WebSocket management - getCommonWs () { - return this.#commonWs - } - - setCommonWs (ws) { - this.#commonWs = ws - } - - // Sessions management - getSession (id) { - return this.#sessions[id] - } - - setSession (id, data) { - this.#sessions[id] = data - } - - removeSession (id) { - delete this.#sessions[id] - } - - // Upgrade instances management - getUpgradeInst (id) { - return this.#upgradeInsts[id] - } - - setUpgradeInst (id, inst) { - this.#upgradeInsts[id] = inst - } - - removeUpgradeInst (id) { - delete this.#upgradeInsts[id] - } - - get data () { - return { - sessions: this.#sessions, - upgradeInsts: this.#upgradeInsts - } - } -} - -// Export a singleton instance -export default new GlobalState() diff --git a/src/app/server/rdp-proxy.js b/src/app/server/rdp-proxy.js deleted file mode 100644 index 4da1e01..0000000 --- a/src/app/server/rdp-proxy.js +++ /dev/null @@ -1,648 +0,0 @@ -import net from 'net' -import tls from 'tls' -import log from '../common/log.js' -import proxySock from './socks.js' - -// Debug prefix for all RDP proxy messages -const LOG_PREFIX = '[RDP-PROXY]' - -// We use Node.js built-in tls module with rejectUnauthorized: false -// to accept self-signed RDP server certificates. -// This works because electerm-web runs in standard Node.js (not Electron with BoringSSL). - -// RDCleanPath ASN.1 DER Constants -const VERSION_1 = 3390 // 3389 + 1 - -// ASN.1 tag constants -const TAG_SEQUENCE = 0x30 -const TAG_INTEGER = 0x02 -const TAG_OCTET_STRING = 0x04 -const TAG_UTF8STRING = 0x0c - -// Context-specific EXPLICIT tags used by RDCleanPath -const TAG_CTX = (n) => 0xa0 + n - -// ASN.1 DER Low-Level Helpers - -/** - * Encode ASN.1 DER length bytes. - */ -function derEncodeLength (length) { - if (length < 0x80) { - return Buffer.from([length]) - } - const bytes = [] - let temp = length - while (temp > 0) { - bytes.unshift(temp & 0xff) - temp >>= 8 - } - return Buffer.from([0x80 | bytes.length, ...bytes]) -} - -/** - * Wrap content with a tag and proper DER length encoding. - */ -function derWrap (tag, content) { - const len = derEncodeLength(content.length) - return Buffer.concat([Buffer.from([tag]), len, content]) -} - -/** - * Encode an integer as ASN.1 DER INTEGER. - */ -function derEncodeInteger (value) { - if (value === 0) { - return derWrap(TAG_INTEGER, Buffer.from([0])) - } - const bytes = [] - let temp = value - while (temp > 0) { - bytes.unshift(temp & 0xff) - temp >>= 8 - } - // Add leading zero if high bit set (to keep unsigned) - if (bytes[0] & 0x80) { - bytes.unshift(0) - } - return derWrap(TAG_INTEGER, Buffer.from(bytes)) -} - -/** - * Encode a UTF-8 string as ASN.1 DER UTF8String. - */ -function derEncodeUtf8String (str) { - return derWrap(TAG_UTF8STRING, Buffer.from(str, 'utf-8')) -} - -/** - * Encode raw bytes as ASN.1 DER OCTET STRING. - */ -function derEncodeOctetString (buf) { - return derWrap(TAG_OCTET_STRING, buf) -} - -/** - * Wrap content in a context-specific EXPLICIT tag [n]. - */ -function derWrapContext (tagNum, content) { - return derWrap(TAG_CTX(tagNum), content) -} - -/** - * Decode DER length at offset. Returns { length, bytesRead }. - */ -function derDecodeLength (buf, offset) { - const first = buf[offset] - if (first < 0x80) { - return { length: first, bytesRead: 1 } - } - const numBytes = first & 0x7f - let length = 0 - for (let i = 0; i < numBytes; i++) { - length = (length << 8) | buf[offset + 1 + i] - } - return { length, bytesRead: 1 + numBytes } -} - -/** - * Decode a DER TLV (Tag-Length-Value) at offset. - * Returns { tag, value: Buffer, totalLength }. - */ -function derDecodeTLV (buf, offset) { - const tag = buf[offset] - const { length, bytesRead } = derDecodeLength(buf, offset + 1) - const headerLen = 1 + bytesRead - const value = buf.slice(offset + headerLen, offset + headerLen + length) - return { tag, value, totalLength: headerLen + length } -} - -/** - * Decode an ASN.1 DER INTEGER to a JS number. - */ -function derDecodeInteger (buf) { - let val = 0 - for (let i = 0; i < buf.length; i++) { - val = (val << 8) | buf[i] - } - return val -} - -/** - * Decode all TLV elements within a constructed value (SEQUENCE, context tags, etc.). - * Returns an array of { tag, value, totalLength }. - */ -function derDecodeChildren (buf) { - const children = [] - let offset = 0 - while (offset < buf.length) { - const tlv = derDecodeTLV(buf, offset) - children.push(tlv) - offset += tlv.totalLength - } - return children -} - -// RDCleanPath PDU Parsing & Encoding - -/** - * Parse an RDCleanPath Request PDU from DER-encoded bytes. - * - * Returns: { destination, proxyAuth, x224ConnectionRequest, preconnectionBlob? } - */ -function parseRDCleanPathRequest (data) { - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - - // Outer SEQUENCE - const outer = derDecodeTLV(buf, 0) - if (outer.tag !== TAG_SEQUENCE) { - throw new Error(`Expected SEQUENCE (0x30), got 0x${outer.tag.toString(16)}`) - } - - const children = derDecodeChildren(outer.value) - - let version = null - let destination = null - let proxyAuth = null - let x224ConnectionRequest = null - let preconnectionBlob = null - - for (const child of children) { - const ctxTag = child.tag & 0x1f // strip class bits to get tag number - - switch (ctxTag) { - case 0: { // version - const intTlv = derDecodeTLV(child.value, 0) - version = derDecodeInteger(intTlv.value) - break - } - case 2: { // destination - const strTlv = derDecodeTLV(child.value, 0) - destination = strTlv.value.toString('utf-8') - break - } - case 3: { // proxy_auth - const strTlv = derDecodeTLV(child.value, 0) - proxyAuth = strTlv.value.toString('utf-8') - break - } - case 5: { // preconnection_blob - const strTlv = derDecodeTLV(child.value, 0) - preconnectionBlob = strTlv.value.toString('utf-8') - break - } - case 6: { // x224_connection_pdu - const octTlv = derDecodeTLV(child.value, 0) - x224ConnectionRequest = octTlv.value - break - } - } - } - - if (version !== VERSION_1) { - throw new Error(`Unsupported RDCleanPath version: ${version} (expected ${VERSION_1})`) - } - if (!destination) { - throw new Error('Missing destination in RDCleanPath request') - } - if (!x224ConnectionRequest) { - throw new Error('Missing x224_connection_pdu in RDCleanPath request') - } - - return { destination, proxyAuth, x224ConnectionRequest, preconnectionBlob } -} - -/** - * Build an RDCleanPath Response PDU as DER-encoded bytes. - * - * @param {string} serverAddr - Resolved server address (e.g. "192.168.2.31:3389") - * @param {Buffer} x224Response - X.224 Connection Confirm bytes - * @param {Buffer[]} certChain - Array of DER-encoded X.509 certificates - * @returns {Buffer} DER-encoded RDCleanPath response - */ -function buildRDCleanPathResponse (serverAddr, x224Response, certChain) { - const parts = [] - - // [0] version - parts.push(derWrapContext(0, derEncodeInteger(VERSION_1))) - - // [6] x224_connection_pdu - parts.push(derWrapContext(6, derEncodeOctetString(x224Response))) - - // [7] server_cert_chain - SEQUENCE OF OCTET STRING - const certOctets = certChain.map((cert) => derEncodeOctetString(cert)) - const certSeq = derWrap(TAG_SEQUENCE, Buffer.concat(certOctets)) - parts.push(derWrapContext(7, certSeq)) - - // [9] server_addr - parts.push(derWrapContext(9, derEncodeUtf8String(serverAddr))) - - return derWrap(TAG_SEQUENCE, Buffer.concat(parts)) -} - -/** - * Build an RDCleanPath Error PDU as DER-encoded bytes. - * - * @param {number} errorCode - 1=general, 2=negotiation - * @param {number} [httpStatusCode] - optional HTTP status code - * @returns {Buffer} DER-encoded RDCleanPath error response - */ -function buildRDCleanPathError (errorCode, httpStatusCode) { - const errParts = [] - - // [0] error_code - errParts.push(derWrapContext(0, derEncodeInteger(errorCode))) - - // [1] http_status_code (optional) - if (httpStatusCode != null) { - errParts.push(derWrapContext(1, derEncodeInteger(httpStatusCode))) - } - - const errSeq = derWrap(TAG_SEQUENCE, Buffer.concat(errParts)) - - const parts = [] - // [0] version - parts.push(derWrapContext(0, derEncodeInteger(VERSION_1))) - // [1] error - parts.push(derWrapContext(1, errSeq)) - - return derWrap(TAG_SEQUENCE, Buffer.concat(parts)) -} - -// Network: Destination Parsing - -/** - * Parse a destination string into { host, port }. - * Handles IPv6 "[::1]:3389" and regular "host:port" formats. - * Default port is 3389. - */ -function parseDestination (destination) { - // IPv6: [host]:port - if (destination.startsWith('[')) { - const bracketEnd = destination.indexOf(']') - if (bracketEnd === -1) throw new Error(`Invalid IPv6 destination: ${destination}`) - const host = destination.slice(1, bracketEnd) - const rest = destination.slice(bracketEnd + 1) - const port = rest.startsWith(':') ? parseInt(rest.slice(1), 10) : 3389 - return { host, port } - } - - // Regular host:port - const lastColon = destination.lastIndexOf(':') - if (lastColon === -1) { - return { host: destination, port: 3389 } - } - const host = destination.slice(0, lastColon) - const port = parseInt(destination.slice(lastColon + 1), 10) - if (isNaN(port)) { - return { host: destination, port: 3389 } - } - return { host, port } -} - -// Network: TCP + X.224 + TLS + Cert Extraction - -/** - * Create a TCP connection (direct or through proxy) - * @param {string} host - * @param {number} port - * @param {object} options - * @param {string} options.proxy - Proxy URL - * @param {number} options.readyTimeout - Connection timeout - * @param {Buffer} x224Request - X.224 Connection Request to send - * @param {function} logPrefix - Log prefix function - * @returns {Promise} - */ -async function createTcpConnection (host, port, options, x224Request, logPrefix) { - if (options.proxy) { - log.debug(`${logPrefix} Connecting through proxy: ${options.proxy}`) - const proxyResult = await proxySock({ - readyTimeout: options.readyTimeout || 15000, - host, - port, - proxy: options.proxy - }) - const tcpSocket = proxyResult.socket - log.debug(`${logPrefix} Proxy connection established`) - - // Send X.224 Connection Request over proxied connection - tcpSocket.write(x224Request, () => { - log.debug(`${logPrefix} Sent X.224 Connection Request (${x224Request.length} bytes)`) - }) - return tcpSocket - } - - return new Promise((resolve, reject) => { - const tcpSocket = net.createConnection({ host, port }, () => { - log.debug(`${logPrefix} TCP connection established`) - - // Send X.224 Connection Request over raw TCP - tcpSocket.write(x224Request, () => { - log.debug(`${logPrefix} Sent X.224 Connection Request (${x224Request.length} bytes)`) - }) - resolve(tcpSocket) - }) - tcpSocket.once('error', (err) => { - reject(new Error(`TCP connection failed: ${err.message}`)) - }) - }) -} - -/** - * Perform the RDP proxy handshake: - * 1. TCP connect to RDP server (optionally through proxy) - * 2. Send X.224 Connection Request (raw TCP) - * 3. Read X.224 Connection Confirm (raw TCP) - * 4. TLS handshake via Node.js tls module (with rejectUnauthorized: false) - * 5. Extract server certificates - * - * @param {string} host - * @param {number} port - * @param {Buffer} x224Request - X.224 Connection Request bytes - * @param {object} options - Optional settings - * @param {string} options.proxy - Proxy URL (e.g., 'socks5://127.0.0.1:1080' or 'http://proxy:8080') - * @param {number} options.readyTimeout - Connection timeout in ms - * @returns {Promise<{ x224Response: Buffer, certChain: Buffer[], tlsSocket: tls.TLSSocket, tcpSocket: net.Socket }>} - */ -async function performRDPHandshake (host, port, x224Request, options = {}) { - const logPrefix = `${LOG_PREFIX} [${host}:${port}]` - - // Step 1: TCP connect (direct or through proxy) - let tcpSocket - try { - tcpSocket = await createTcpConnection(host, port, options, x224Request, logPrefix) - } catch (err) { - throw new Error(`Connection failed: ${err.message}`) - } - - return new Promise((resolve, reject) => { - let settled = false - - function settle (err, result) { - if (settled) return - settled = true - if (err) reject(err) - else resolve(result) - } - - tcpSocket.once('error', (err) => { - settle(new Error(`TCP connection failed: ${err.message}`)) - }) - - // Step 3: Read X.224 Connection Confirm - tcpSocket.once('data', (x224Response) => { - log.debug(`${logPrefix} Received X.224 Connection Confirm (${x224Response.length} bytes)`) - - if (x224Response.length === 0) { - tcpSocket.destroy() - settle(new Error('RDP server closed connection without X.224 response')) - return - } - - // Remove all listeners before upgrading to TLS - tcpSocket.removeAllListeners('error') - tcpSocket.removeAllListeners('data') - - // Step 4: TLS handshake via Node.js tls module - log.debug(`${logPrefix} Starting TLS handshake`) - - const tlsSocket = tls.connect({ - socket: tcpSocket, - rejectUnauthorized: false // Accept self-signed RDP certificates - }, () => { - log.debug(`${logPrefix} TLS handshake completed`) - - // The handshake-deadline timer set below (in the outer function) - // is a `net.Socket` idle-inactivity timer, not a one-shot deadline - // - it re-arms on every read/write and was never cleared once the - // handshake finished. Left alone, it destroys this same socket - // (reused for the whole session relay) after any 15s stretch with - // no bytes in either direction - e.g. a static remote desktop with - // no mouse/keyboard activity - killing otherwise-healthy sessions. - // Disable it now that the handshake is done; a real dead/half-open - // connection is instead caught by the TCP keepalive enabled below. - tcpSocket.setTimeout(0) - tcpSocket.setNoDelay(true) - tcpSocket.setKeepAlive(true, 10000) - - // Step 5: Extract certificate chain - const certChain = extractCertChain(tlsSocket) - log.debug(`${logPrefix} Extracted ${certChain.length} certificate(s)`) - - settle(null, { - x224Response: Buffer.from(x224Response), - certChain, - tlsSocket, - tcpSocket - }) - }) - - tlsSocket.once('error', (err) => { - log.error(`${logPrefix} TLS error: ${err.message}`) - settle(new Error(`TLS handshake failed: ${err.message}`)) - }) - - // Timeout for the TLS handshake - tlsSocket.setTimeout(15000, () => { - tlsSocket.destroy() - settle(new Error('TLS handshake timed out')) - }) - }) - - // Timeout for the whole handshake - tcpSocket.setTimeout(15000, () => { - tcpSocket.destroy() - settle(new Error('Connection timed out')) - }) - }) -} - -/** - * Extract the certificate chain from a TLS socket. - * Returns an array of DER-encoded certificates. - */ -function extractCertChain (tlsSocket) { - const result = [] - try { - const peerCert = tlsSocket.getPeerCertificate(true) - if (peerCert) { - // The 'raw' property contains the DER-encoded certificate - if (peerCert.raw) { - result.push(peerCert.raw) - } - // Check for issuer certificate in the chain - let cert = peerCert - while (cert.issuerCertificate && cert.issuerCertificate !== cert) { - if (cert.issuerCertificate.raw) { - result.push(cert.issuerCertificate.raw) - } - cert = cert.issuerCertificate - } - } - } catch (e) { - log.error(`${LOG_PREFIX} Error extracting cert chain: ${e.message}`) - } - return result -} - -// Bidirectional Relay: WebSocket <-> TLS Socket <-> TCP - -/** - * Set up bidirectional relay between a WebSocket and a TLS socket. - * - * Browser (WASM) -> WebSocket -> Proxy -> TLS Socket -> TCP -> RDP Server - * RDP Server -> TCP -> TLS Socket -> Proxy -> WebSocket -> Browser (WASM) - * - * @param {WebSocket} ws - The WebSocket connection to the browser - * @param {tls.TLSSocket} tlsSocket - The TLS socket connected to RDP server - * @param {net.Socket} tcpSocket - The underlying TCP socket - */ -function setupTlsRelay (ws, tlsSocket, tcpSocket) { - let wsBytesForwarded = 0 - let tlsBytesForwarded = 0 - - const logPrefix = `${LOG_PREFIX} [relay]` - - // TLS Socket -> WebSocket (RDP server -> browser) - tlsSocket.on('data', (data) => { - tlsBytesForwarded += data.length - try { - if (ws.readyState === 1 /* OPEN */) { - ws.send(data) - } - } catch (err) { - log.error(`${logPrefix} TLS->WS write error:`, err.message) - } - }) - - // WebSocket -> TLS Socket (browser -> RDP server) - ws.on('message', (data) => { - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - wsBytesForwarded += buf.length - try { - tlsSocket.write(buf) - } catch (err) { - log.error(`${logPrefix} WS->TLS write error:`, err.message) - } - }) - - // Cleanup on close - const cleanup = (source) => { - log.debug(`${logPrefix} ${source} closed - WS->TLS: ${wsBytesForwarded} bytes, TLS->WS: ${tlsBytesForwarded} bytes`) - if (!tcpSocket.destroyed) tcpSocket.destroy() - if (ws.readyState === 1) { - try { ws.close() } catch (_) {} - } - } - - tlsSocket.on('end', () => cleanup('TLS')) - tlsSocket.on('error', (err) => { - log.error(`${logPrefix} TLS error:`, err.message) - cleanup('TLS (error)') - }) - - tcpSocket.on('end', () => cleanup('TCP')) - tcpSocket.on('error', (err) => { - log.error(`${logPrefix} TCP error:`, err.message) - cleanup('TCP (error)') - }) - - ws.on('close', () => cleanup('WebSocket')) - ws.on('error', (err) => { - log.error(`${logPrefix} WebSocket error:`, err.message) - cleanup('WebSocket (error)') - }) -} - -// Main Handler: Process a WebSocket connection - -/** - * Handle a new WebSocket connection from the browser's WASM RDP client. - * - * Protocol: - * 1. Receive RDCleanPath Request (ASN.1 DER binary message) - * 2. Parse destination, X.224 connection request - * 3. TCP connect to RDP server, send X.224, receive X.224 confirm - * 4. TLS handshake, extract server certificates - * 5. Send RDCleanPath Response back to browser - * 6. Bidirectional relay: WebSocket <-> TLS - * - * @param {WebSocket} ws - The WebSocket connection - * @param {object} options - Optional settings - * @param {string} options.proxy - Proxy URL (e.g., 'socks5://127.0.0.1:1080' or 'http://proxy:8080') - * @param {number} options.readyTimeout - Connection timeout in ms - */ -function handleConnection (ws, options = {}, bufferedMessages = []) { - log.debug(`${LOG_PREFIX} New WebSocket connection for RDP proxy`) - - const handleFirstMessage = async (data) => { - try { - const requestData = Buffer.isBuffer(data) ? data : Buffer.from(data) - log.debug(`${LOG_PREFIX} Received RDCleanPath request (${requestData.length} bytes)`) - - // Step 1: Parse RDCleanPath request - const request = parseRDCleanPathRequest(requestData) - log.debug(`${LOG_PREFIX} RDCleanPath Request -> destination: ${request.destination}, proxyAuth: ${request.proxyAuth}`) - - // Step 2: Parse destination - const { host, port } = parseDestination(request.destination) - log.debug(`${LOG_PREFIX} Connecting to RDP server at ${host}:${port}`) - - // Step 3-5: TCP + X.224 + TLS + Certs - const { x224Response, certChain, tlsSocket, tcpSocket } = await performRDPHandshake( - host, - port, - request.x224ConnectionRequest, - options - ) - - // Step 6: Build and send RDCleanPath response - const serverAddr = `${host}:${port}` - const responsePdu = buildRDCleanPathResponse(serverAddr, x224Response, certChain) - log.debug(`${LOG_PREFIX} Sending RDCleanPath response (${responsePdu.length} bytes) to browser`) - ws.send(responsePdu) - - log.debug(`${LOG_PREFIX} RDP proxy handshake complete - starting bidirectional relay`) - - // Step 7: Bidirectional relay - setupTlsRelay(ws, tlsSocket, tcpSocket) - } catch (err) { - log.error(`${LOG_PREFIX} RDP proxy handshake error:`, err.message) - log.error(`${LOG_PREFIX} Stack:`, err.stack) - - // Try to send error response to client - try { - const errorPdu = buildRDCleanPathError(1, 502) - ws.send(errorPdu) - } catch (_) {} - - try { ws.close() } catch (_) {} - } - } - - // If a message arrived during async setup (before this handler was registered), - // process it immediately; otherwise wait for the next message event. - if (bufferedMessages.length > 0) { - handleFirstMessage(bufferedMessages[0]) - } else { - ws.once('message', handleFirstMessage) - } - - ws.on('error', (err) => { - log.error(`${LOG_PREFIX} WebSocket error:`, err.message) - }) -} - -// Backward compatibility alias -const setupForgeRelay = setupTlsRelay - -export { - handleConnection, - parseRDCleanPathRequest, - buildRDCleanPathResponse, - buildRDCleanPathError, - parseDestination, - performRDPHandshake, - setupTlsRelay, - setupForgeRelay // backward compatibility -} diff --git a/src/app/server/remote-common.js b/src/app/server/remote-common.js deleted file mode 100644 index 506c113..0000000 --- a/src/app/server/remote-common.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * common functions for remote process handling, - * for sftp, terminal and transfer - */ - -// const _ = require('loadsh') -import globalState from './global-state.js' - -export function session (id) { - return globalState.getSession(id) -} - -export function sftp (id, inst) { - if (inst) { - globalState.setSession(id, inst) - return inst - } - return globalState.getSession(id) -} - -export function terminals (id, inst) { - if (inst) { - globalState.setSession(id, inst) - return inst - } - return globalState.getSession(id) -} - -export function transfer (id, sftpId, inst) { - const ss = sftp(sftpId) - if (!ss) { - return - } - if (inst) { - ss.transfers[id] = inst - return inst - } - return ss.transfers[id] -} - -export function onDestroySftp (id) { - const inst = sftp(id) - inst && inst.kill && inst.kill() -} - -export function onDestroyTerminal (id) { - onDestroySftp(id) -} - -export function cleanAllSessions () { - const { sessions } = globalState.data - for (const id in sessions) { - const inst = sessions[id] - inst && inst.kill && inst.kill() - } -} - -export function onDestroyTransfer (id, sftpId) { - const sftpInst = sftp(sftpId) - const inst = transfer(id, sftpId) - inst && inst.destroy && inst.destroy() - sftpInst && delete sftpInst.transfers[id] -} diff --git a/src/app/server/server.js b/src/app/server/server.js deleted file mode 100644 index fa1fff2..0000000 --- a/src/app/server/server.js +++ /dev/null @@ -1,52 +0,0 @@ -import express from 'express' -import pug from 'pug' -import { wsRoutes } from '../routes/ws.js' -import { httpRoutes } from '../routes/http.js' -import { applyExtensions } from '../lib/extensions.js' -import morgan from 'morgan' -import { - isDev, - cwd -} from '../common/runtime-constants.js' -import { resolve } from 'path' -import log from '../common/log.js' -import { applySystemCAsToGlobalAgent } from '../lib/system-ca.js' - -export async function createApp () { - const loadedCount = applySystemCAsToGlobalAgent() - if (loadedCount > 0) { - log.info(`[TLS] loaded ${loadedCount} system CA certificate(s) into main process`) - } - - const app = express() - // parse application/x-www-form-urlencoded - app.use(express.urlencoded({ extended: true })) - - // parse application/json - app.use(express.json()) - - app.use(morgan( - ':method :url :status :res[content-length] - :response-time ms' - )) - app.set('view engine', 'pug') - // Register the pug engine explicitly so Express uses the bundled pug - // directly instead of lazily `require('pug')` at render time. The lazy - // require breaks bundled builds (esbuild can't see the dynamic string - // require, so "pug" is missing at runtime -> GET / hangs forever). - app.engine('pug', pug.__express) - app.set( - 'views', - process.env.VIEW_FOLDER || - ( - !isDev - ? resolve(cwd, 'dist/views') - : resolve(cwd, 'src/app/views') - ) - ) - app.set('x-powered-by', false) - - httpRoutes(app) - wsRoutes(app) - await applyExtensions(app) - return app -} diff --git a/src/app/server/session-base.js b/src/app/server/session-base.js deleted file mode 100644 index 4047c02..0000000 --- a/src/app/server/session-base.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import uid from '../common/uid.js' -import { createLogFileName } from '../common/create-session-log-file-path.js' -import { SessionLog } from './session-log.js' -import globalState from './global-state.js' -import time from '../common/time.js' -import path from 'path' -import pkg from '@xterm/headless' -const { Terminal } = pkg - -function createVtParser (cols = 4096) { - const term = new Terminal({ cols, rows: 50, allowProposedApi: true }) - return term -} - -export class TerminalBase { - constructor (initOptions, ws, isTest) { - this.type = initOptions.termType || initOptions.type - this.pid = initOptions.uid || uid() - this.initOptions = initOptions - if (initOptions.saveTerminalLogToFile) { - this.sessionLogger = new SessionLog({ - logDir: initOptions.sessionLogPath, - fileName: createLogFileName(initOptions.logName) - }) - this._initVtParser() - } - if (ws) { - this.ws = ws - } - if (isTest) { - this.isTest = isTest - } - } - - cache = '' - prevNewLine = true - _initVtParser () { - this._vtTerm = createVtParser(this.initOptions.cols || 4096) - this._vtLastRow = 0 - this._vtTerm.onLineFeed(() => { - if (!this.sessionLogger) return - const buffer = this._vtTerm.buffer.active - const row = buffer.baseY + buffer.cursorY - 1 - if (row < 0) return - const line = buffer.getLine(row) - if (!line) return - const text = line.translateToString(true) - const dt = this.initOptions.addTimeStampToTermLog - ? `[${time()}] ` - : '' - this.sessionLogger.write(dt + text + '\n') - }) - } - - parse (rawText) { - let result = '' - const len = rawText.length - for (let i = 0; i < len; i++) { - if (rawText[i] === '\b') { - result = result.slice(0, -1) - } else { - result += rawText[i] - } - } - return result - } - - writeLog (data) { - if (!this.sessionLogger || !this._vtTerm) { - return - } - // Normalize bare \r (carriage return, not part of \r\n) to \r\n. - // Embedded devices (UART/telnet) often use \r-only line endings which - // don't trigger xterm's onLineFeed, causing timestamps to be missing - // for every line except the first. - if (Buffer.isBuffer(data)) { - const str = data.toString('binary') - const normalized = str.replace(/\r(?!\n)/g, '\r\n') - this._vtTerm.write(normalized) - } else { - const normalized = String(data).replace(/\r(?!\n)/g, '\r\n') - this._vtTerm.write(normalized) - } - } - - toggleTerminalLogTimestamp () { - this.initOptions.addTimeStampToTermLog = !this.initOptions.addTimeStampToTermLog - } - - setTerminalLogPath (logPath) { - if (!logPath) { return } - this.initOptions.sessionLogPath = logPath - if (this.sessionLogger) { - this.sessionLogger.destroy() - if (this._vtTerm) { - this._vtTerm.dispose() - delete this._vtTerm - } - this.sessionLogger = new SessionLog({ - logDir: this.initOptions.sessionLogPath, - fileName: createLogFileName(this.initOptions.logName) - }) - this._initVtParser() - } - } - - startTerminalLogFile (logFilePath, addTimeStamp) { - if (!logFilePath) { - return - } - const { dirname, basename } = path - const logDir = dirname(logFilePath) - const fileName = basename(logFilePath) - if (this.sessionLogger) { - this.sessionLogger.destroy() - delete this.sessionLogger - } - if (this._vtTerm) { - this._vtTerm.dispose() - delete this._vtTerm - } - this.initOptions.addTimeStampToTermLog = !!addTimeStamp - this.sessionLogger = new SessionLog({ logDir, fileName }) - this._initVtParser() - } - - toggleTerminalLog () { - if (this.sessionLogger) { - this.sessionLogger.destroy() - delete this.sessionLogger - if (this._vtTerm) { - this._vtTerm.dispose() - delete this._vtTerm - } - } else { - this.sessionLogger = new SessionLog({ - logDir: this.initOptions.sessionLogPath, - fileName: createLogFileName(this.initOptions.logName) - }) - this._initVtParser() - } - } - - onEndConn () { - const { - pid - } = this - const inst = globalState.getSession(pid) - if (!inst) { - return - } - if (this.ws) { - delete this.ws - } - if (this._vtTerm) { - this._vtTerm.dispose() - delete this._vtTerm - } - if (this.server && this.server.end) { - this.server.end() - } - globalState.removeSession(pid) - } -} diff --git a/src/app/server/session-common.js b/src/app/server/session-common.js deleted file mode 100644 index f10b65d..0000000 --- a/src/app/server/session-common.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * terminal/sftp/serial class - */ - -export function commonExtends (Cls) { - Cls.prototype.customEnv = function (envs) { - if (!envs) { - return {} - } - return envs.split(' ').reduce((p, k) => { - const [key, value] = k.split('=') - if (key && value) { - p[key] = value - } - return p - }, {}) - } - - Cls.prototype.getEnv = function (initOptions = this.initOptions) { - return { - LANG: initOptions.envLang || 'en_US.UTF-8', - ...this.customEnv(initOptions.setEnv) - } - } - - Cls.prototype.getExecOpts = function () { - return { - env: this.getEnv() - } - } - - Cls.prototype.runCmd = function (cmd, conn) { - return new Promise((resolve, reject) => { - const client = conn || this.conn || this.client - client.exec(cmd, this.getExecOpts(), (err, stream) => { - if (err) reject(err) - if (stream) { - let r = '' - stream - .on('data', function (data) { - const d = data.toString() - r = r + d - }) - .on('close', (code, signal) => { - resolve(r) - }) - } else { - resolve('') - } - }) - }) - } - - // Structured command execution over an SSH exec channel. - // Unlike runCmd (which merges stdout/stderr and drops the exit code), - // execCommand captures both streams separately and resolves the real - // exit code. Optional timeoutMs closes the channel early and resolves - // partial output with timedOut: true. - Cls.prototype.execCommand = function (cmd, options = {}, conn) { - return new Promise((resolve, reject) => { - const { timeoutMs = 0 } = options || {} - const client = conn || this.conn || this.client - if (!client || typeof client.exec !== 'function') { - reject(new Error('Exec channel not supported for this session type')) - return - } - let timer = null - client.exec(cmd, this.getExecOpts(), (err, stream) => { - if (err) { - reject(err) - return - } - if (!stream) { - resolve({ stdout: '', stderr: '', exitCode: null, timedOut: false }) - return - } - let stdout = '' - let stderr = '' - let exitCode = null - let settled = false - const done = (timedOut) => { - if (settled) return - settled = true - if (timer) { - clearTimeout(timer) - timer = null - } - resolve({ stdout, stderr, exitCode, timedOut }) - } - if (timeoutMs > 0) { - timer = setTimeout(() => { - try { - stream.close() - } catch (_) { - // ignore — best effort channel close - } - done(true) - }, timeoutMs) - } - stream.on('data', (data) => { - stdout += data.toString() - }) - if (stream.stderr) { - stream.stderr.on('data', (data) => { - stderr += data.toString() - }) - } - stream.on('exit', (code) => { - exitCode = typeof code === 'number' ? code : null - }) - stream.on('close', () => done(false)) - stream.on('error', (e) => { - if (timer) { - clearTimeout(timer) - timer = null - } - if (!settled) { - settled = true - reject(e) - } - }) - }) - }) - } - return Cls -} diff --git a/src/app/server/session-ftp.js b/src/app/server/session-ftp.js deleted file mode 100644 index 368662c..0000000 --- a/src/app/server/session-ftp.js +++ /dev/null @@ -1,306 +0,0 @@ -import { FtpClientWrapper } from './ftp-client.js' -import { TerminalBase } from './session-base.js' -import { commonExtends } from './session-common.js' -import { readRemoteFile, writeRemoteFile } from './ftp-file.js' -import { Readable, PassThrough } from 'stream' -import { posix as path } from 'path' -import globalState from './global-state.js' - -export class FtpSession extends TerminalBase { - constructor (initOptions) { - super({ - ...initOptions, - type: 'ftp' // Explicitly set the type - }) - this.transfers = {} - } - - getClientAccessOptions (initOptions = this.initOptions) { - return { - host: initOptions.host, - port: initOptions.port || 21, - user: initOptions.user, - password: initOptions.password, - secure: initOptions.secure, - proxy: initOptions.proxy, - readyTimeout: initOptions.readyTimeout - } - } - - async createConnectedClient (initOptions = this.initOptions) { - const client = new FtpClientWrapper() - client.verbose = initOptions.debug - client.setEncoding(initOptions.encode || 'utf-8') - await client.access(this.getClientAccessOptions(initOptions)) - return client - } - - async createOperationClient () { - return this.createConnectedClient() - } - - async withOperationClient (handler, client) { - if (client) { - return handler(client) - } - const operationClient = await this.createOperationClient() - try { - return await handler(operationClient) - } finally { - await operationClient.close().catch(() => {}) - } - } - - async connect (initOptions) { - this.initOptions = { - ...this.initOptions, - ...initOptions - } - const client = await this.createConnectedClient(this.initOptions) - await client.close().catch(() => {}) - globalState.setSession(this.pid, this) - return 'ok' - } - - kill () { - Object.values(this.transfers).forEach(transfer => { - transfer?.destroy?.() - }) - this.transfers = {} - super.onEndConn() - } - - async getHomeDir () { - return this.withOperationClient(client => client.pwd()) - } - - async rmdir (remotePath) { - await this.withOperationClient(client => { - return this.removeDirectoryRecursively(remotePath, client) - }) - return 1 - } - - async mv (from, to) { - await this.rename(from, to) - return 1 - } - - async cp (from, to) { - const sourceStat = await this.stat(from) - const targetStat = await this.tryStat(to) - const targetPath = targetStat?.isDirectory - ? path.join(to, path.basename(from)) - : to - const sourceClient = await this.createOperationClient() - const targetClient = await this.createOperationClient() - - try { - if (sourceStat.isDirectory) { - await this.copyDirectory(from, targetPath, sourceClient, targetClient) - } else { - await this.copyFile(from, targetPath, sourceClient, targetClient) - } - return 1 - } finally { - await sourceClient.close().catch(() => {}) - await targetClient.close().catch(() => {}) - } - } - - async tryStat (remotePath, client) { - try { - return await this.stat(remotePath, client) - } catch (error) { - return null - } - } - - async ensureDirSafe (remotePath, client) { - const currentPath = await client.pwd() - try { - await client.ensureDir(remotePath) - } finally { - if (currentPath) { - await client.cd(currentPath).catch(() => {}) - } - } - } - - async copyDirectory (sourcePath, targetPath, sourceClient, targetClient) { - await this.ensureDirSafe(targetPath, targetClient) - const list = await this.list(sourcePath, sourceClient) - for (const item of list) { - const nextSourcePath = path.join(sourcePath, item.name) - const nextTargetPath = path.join(targetPath, item.name) - if (item.type === 'd') { - await this.copyDirectory(nextSourcePath, nextTargetPath, sourceClient, targetClient) - } else { - await this.copyFile(nextSourcePath, nextTargetPath, sourceClient, targetClient) - } - } - } - - async copyFile (sourcePath, targetPath, sourceClient, targetClient) { - const transferStream = new PassThrough() - const downloadPromise = sourceClient.downloadTo(transferStream, sourcePath) - .catch(error => { - transferStream.destroy(error) - throw error - }) - const uploadPromise = targetClient.uploadFrom(transferStream, targetPath) - .catch(error => { - transferStream.destroy(error) - throw error - }) - - await Promise.all([downloadPromise, uploadPromise]) - } - - async removeDirectoryRecursively (remotePath, client) { - const contents = await this.list(remotePath, client) - for (const item of contents) { - const itemPath = `${remotePath}/${item.name}` - if (item.type === 'd') { - await this.removeDirectoryRecursively(itemPath, client) - } else { - await this.rm(itemPath, client) - } - } - await this.rmFolder(remotePath, client) - } - - async touch (remotePath) { - const emptyStream = new Readable({ - read () { - this.push(null) - } - }) - await this.withOperationClient(client => client.uploadFrom(emptyStream, remotePath)) - return 1 - } - - async mkdir (remotePath) { - await this.withOperationClient(client => client.ensureDir(remotePath)) - return 1 - } - - async stat (remotePath, client) { - return this.withOperationClient(async currentClient => { - const pathParts = remotePath.split('/') - const fileName = pathParts.pop() - const parentPath = pathParts.join('/') || '/' - const list = await currentClient.list(parentPath) - if (!list || !list.length) { - throw new Error('stat failed: parent directory listing empty') - } - - const item = list.find(item => item.name === fileName) - if (!item) { - throw new Error(`stat failed: ${fileName} not found in ${parentPath}`) - } - return { - size: item.size, - accessTime: new Date(item.modifiedAt).getTime(), - modifyTime: new Date(item.modifiedAt).getTime(), - mode: 0o777, // Default permissions since FTP doesn't provide this - isDirectory: item.type === 2 - } - }, client) - } - - async readlink (remotePath) { - return remotePath - } - - async realpath (remotePath, client) { - return this.withOperationClient(async currentClient => { - const currentPath = await currentClient.pwd() - await currentClient.cd(remotePath) - const realPath = await currentClient.pwd() - await currentClient.cd(currentPath) - return realPath - }, client) - } - - async lstat (remotePath, client) { - return this.stat(remotePath, client) - } - - async chmod () { - // FTP doesn't support chmod - return 1 - } - - async rename (remotePath, remotePathNew) { - await this.withOperationClient(client => client.rename(remotePath, remotePathNew)) - return 1 - } - - async rmFolder (remotePath, client) { - await this.withOperationClient(currentClient => currentClient.removeDir(remotePath), client) - return 1 - } - - async rm (remotePath, client) { - await this.withOperationClient(currentClient => currentClient.remove(remotePath), client) - return 1 - } - - async list (remotePath, client) { - return this.withOperationClient(async currentClient => { - const list = await currentClient.list(remotePath) - return list.map(item => { - const dt = new Date(item.rawModifiedAt).getTime() - return { - type: item.type === 2 ? 'd' : '-', - name: item.name, - size: item.size, - modifyTime: dt, - accessTime: dt, - mode: 0o777, // Default permissions since FTP doesn't provide this - rights: { - user: 'rwx', - group: 'rwx', - other: 'rwx' - }, - owner: 'owner', - group: 'group' - } - }) - }, client) - } - - async readFile (remotePath, client) { - return this.withOperationClient(currentClient => readRemoteFile(currentClient, remotePath), client) - } - - async writeFile (remotePath, str, mode, client) { - return this.withOperationClient(currentClient => { - return writeRemoteFile(currentClient, remotePath, str, mode) - }, client) - } - - async getFolderSize (folderPath, client) { - let size = 0 - let count = 0 - const processDir = async (dirPath) => { - const list = await this.list(dirPath, client) - for (const item of list) { - if (item.type === 'd') { - await processDir(`${dirPath}/${item.name}`) - } else { - size += item.size - count++ - } - } - } - return this.withOperationClient(async currentClient => { - client = currentClient - await processDir(folderPath) - return { size, count } - }, client) - } -} - -export const Ftp = commonExtends(FtpSession) diff --git a/src/app/server/session-hop.js b/src/app/server/session-hop.js deleted file mode 100644 index 9426960..0000000 --- a/src/app/server/session-hop.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Shared SSH connection-hopping utility. - * - * Creates a dynamic-SOCKS5 SSH tunnel through one or more hop servers - * and returns the proxy URL to use for the final connection. - * - * Used by both VNC and RDP sessions. - */ - -import uid from '../common/uid.js' -import { terminalSsh } from './session-ssh.js' -import findFreePort from 'find-free-port' - -function getPort (fromPort = 12023) { - return new Promise((resolve, reject) => { - findFreePort(fromPort, '127.0.0.1', function (err, freePort) { - if (err) { - reject(err) - } else { - resolve(freePort) - } - }) - }) -} - -/** - * Set up an SSH hop tunnel if connectionHoppings are configured. - * - * @param {object} initOptions - Session init options - * @param {Array} initOptions.connectionHoppings - Hop server definitions (mutated: last item is popped) - * @param {string} [initOptions.proxy] - Existing proxy URL to chain through - * @returns {Promise<{ proxyUrl: string|null, ssh: object|null }>} - * proxyUrl - SOCKS5 URL to use for the final connection, or original proxy, or null - * ssh - SSH session that must be killed on cleanup, or null - */ -async function createHopProxy (initOptions) { - const { - proxy, - connectionHoppings - } = initOptions - - if (!connectionHoppings || !connectionHoppings.length) { - return { proxyUrl: proxy || null, ssh: null } - } - - const hop = connectionHoppings.pop() - const fp = await getPort() - - const initOpts = { - connectionHoppings, - ...hop, - hasHopping: true, - cols: 80, - rows: 24, - term: 'xterm-256color', - saveTerminalLogToFile: false, - id: uid(), - enableSsh: true, - encode: 'utf-8', - envLang: 'en_US.UTF-8', - proxy, - sshTunnels: [ - { - sshTunnel: 'dynamicForward', - sshTunnelLocalHost: '127.0.0.1', - sshTunnelLocalPort: fp, - id: uid() - } - ] - } - - const ssh = await terminalSsh(initOpts) - return { proxyUrl: `socks5://127.0.0.1:${fp}`, ssh } -} - -export { createHopProxy, getPort } diff --git a/src/app/server/session-local.js b/src/app/server/session-local.js deleted file mode 100644 index 4e559c1..0000000 --- a/src/app/server/session-local.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import { resolve as pathResolve } from 'path' -import globalState from './global-state.js' -import { TerminalBase } from './session-base.js' -import log from '../common/log.js' - -// `node-pty` is a native module that is not built for Android yet. Load it -// lazily and tolerate its absence so the server can still start; the local -// terminal is also disabled via DISABLE_LOCAL_TERMINAL. -let nodePtyPromise = null -function loadNodePty () { - if (!nodePtyPromise) { - nodePtyPromise = import('node-pty') - .then(m => m.default) - .catch(err => { - log.warn('node-pty is not available, local terminal disabled:', err.message) - return null - }) - } - return nodePtyPromise -} - -// const { MockBinding } = require('@serialport/binding-mock') -// MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) - -class TerminalLocal extends TerminalBase { - async init () { - const pty = await loadNodePty() - if (!pty) { - return Promise.reject(new Error('Local terminal is not available on this platform')) - } - const { - cols, - rows, - execWindows, - execMac, - execLinux, - execWindowsArgs, - execMacArgs, - execLinuxArgs, - termType, - term - } = this.initOptions - this.isLocal = true - const { platform } = process - const isWin = platform.startsWith('win') - const exec = isWin - ? pathResolve( - process.env.windir, - execWindows - ) - : platform === 'darwin' ? execMac : execLinux - if ((exec || '').includes('..')) { - return Promise.reject(new Error('execWindows should not contain ".."')) - } - const arg = isWin - ? execWindowsArgs - : platform === 'darwin' ? execMacArgs : execLinuxArgs - const cwd = process.env[platform === 'win32' ? 'USERPROFILE' : 'HOME'] - const argv = platform.startsWith('darwin') ? ['--login', ...(arg || [])] : arg - const env = Object.assign({}, process.env) - delete env.ELECTRON_RUN_AS_NODE - delete env.NODE_OPTIONS - delete env.ELECTRON_NO_ATTACH_CONSOLE - // temp PEM of system CAs for the server process (WebDAV sync, #4347) — - // not meant for user shells, and a bad keychain cert makes any Node/bun - // tool in the terminal print "ignoring extra certs ... load failed" - delete env.NODE_EXTRA_CA_CERTS - this.term = pty.spawn(exec, argv, { - name: term, - encoding: null, - cols: cols || 80, - rows: rows || 24, - cwd, - env, - // Use the OpenConsole conpty.dll shipped with node-pty instead of the - // legacy Windows Console Host (kernel32 CreatePseudoConsole) conpty. - // The legacy console-host conpty can stall output and deliver Ctrl+C to - // the whole process group (killing the shell too) after a full-screen - // TUI like opencode exits, leaving the terminal tab unresponsive. - // The OpenConsole conpty.dll does not have this problem. - useConptyDll: true - }) - this.term.termType = termType - globalState.setSession(this.pid, this) - return Promise.resolve(this) - } - - resize (cols, rows) { - this.term.resize(cols, rows) - } - - on (event, cb) { - this.term.on(event, cb) - } - - write (data) { - this.term.write(data) - } - - kill () { - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - this.term && this.term.kill() - this.onEndConn() - } -} - -export const terminalLocal = function (initOptions, ws) { - if (process.env.DISABLE_LOCAL_TERMINAL) { - return Promise.reject(new Error('Local terminal is disabled')) - } - return (new TerminalLocal(initOptions, ws)).init() -} - -/** - * test ssh connection - * @param {object} options - */ -export const testConnectionLocal = (initOptions) => { - if (process.env.DISABLE_LOCAL_TERMINAL) { - return Promise.reject(new Error('Local terminal is disabled')) - } - return Promise.resolve(true) -} - -export const terminal = terminalLocal -export const testConnection = testConnectionLocal diff --git a/src/app/server/session-log.js b/src/app/server/session-log.js deleted file mode 100644 index 67e3c4e..0000000 --- a/src/app/server/session-log.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * log ssh output to file - */ - -import { resolve, dirname } from 'path' -import { createWriteStream, existsSync, mkdirSync } from 'fs' -import { cwd } from '../common/runtime-constants.js' - -function mkdirP (resolvedPath) { - if (!existsSync(resolvedPath)) { - mkdirP(dirname(resolvedPath)) - mkdirSync(resolvedPath) - } -} - -const { DB_PATH } = process.env -const dataPath = DB_PATH || resolve(cwd, 'data') - -export const logDir = resolve(dataPath, 'electerm_session_logs') - -export class SessionLog { - constructor (options) { - const { logDir } = options - const logPath = resolve(logDir, options.fileName) - mkdirP(logDir) - this.stream = createWriteStream(logPath, { flags: 'a' }) - } - - write (text) { - this.stream.write(text) - } - - destroy () { - this.stream.destroy() - } -} diff --git a/src/app/server/session-rdp.js b/src/app/server/session-rdp.js deleted file mode 100644 index 4524f7d..0000000 --- a/src/app/server/session-rdp.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import log from '../common/log.js' -import { TerminalBase } from './session-base.js' -import globalState from './global-state.js' -import { - handleConnection -} from './rdp-proxy.js' -import { createHopProxy } from './session-hop.js' -import proxySock from './socks.js' -import net from 'net' - -class TerminalRdp extends TerminalBase { - init = async () => { - globalState.setSession(this.pid, this) - return Promise.resolve(this) - } - - /** - * Start the RDCleanPath proxy for this session. - * Called when the WebSocket connects from the browser. - * The WASM client will send an RDCleanPath Request as the first message. - */ - start = async (width, height) => { - if (!this.ws) { - log.error(`[RDP:${this.pid}] No WebSocket available`) - return - } - this.width = width - this.height = height - - // Buffer any messages that arrive during the async hop setup so they - // are not dropped before handleConnection sets up its own listener. - const bufferedMessages = [] - const bufferMsg = (data) => bufferedMessages.push(data) - this.ws.on('message', bufferMsg) - - const { readyTimeout } = this.initOptions - - const { proxyUrl, ssh } = await createHopProxy(this.initOptions) - if (ssh) { - this.ssh = ssh - } - - // Hand off to the proxy handler, replaying any buffered messages. - this.ws.off('message', bufferMsg) - handleConnection(this.ws, { - proxy: proxyUrl, - readyTimeout - }, bufferedMessages) - } - - resize () { - // IronRDP handles resize via the WASM session.resize() method - // which sends resize PDUs through the existing relay - } - - test = async () => { - const { - host, - port = 3389, - readyTimeout = 10000 - } = this.initOptions - - const { proxyUrl, ssh } = await createHopProxy(this.initOptions) - if (ssh) { - this.ssh = ssh - } - - try { - if (proxyUrl) { - const proxyResult = await proxySock({ readyTimeout, host, port, proxy: proxyUrl }) - proxyResult.socket.destroy() - return true - } - - return await new Promise((resolve, reject) => { - const socket = net.createConnection({ host, port }, () => { - socket.destroy() - resolve(true) - }) - socket.on('error', (err) => reject(err)) - socket.setTimeout(readyTimeout, () => { - socket.destroy() - reject(new Error('Connection timed out')) - }) - }) - } finally { - if (this.ssh) { - this.ssh.kill() - delete this.ssh - } - } - } - - kill = () => { - if (this.ws) { - try { - this.ws.close() - } catch (e) { - log.debug(`[RDP:${this.pid}] ws.close() error: ${e.message}`) - } - delete this.ws - } - if (this.ssh) { - this.ssh.kill() - delete this.ssh - } - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - const { - pid - } = this - const inst = globalState.getSession(pid) - if (!inst) { - return - } - globalState.removeSession(pid) - } -} - -export const terminalRdp = async function (initOptions, ws) { - const term = new TerminalRdp(initOptions, ws) - await term.init() - return term -} - -/** - * test ssh connection - * @param {object} options - */ -export const testConnectionRdp = (options) => { - return (new TerminalRdp(options, undefined, true)) - .test() - .then((res) => { - res.close() - return true - }) - .catch(() => { - return false - }) -} - -export const terminal = terminalRdp -export const testConnection = testConnectionRdp diff --git a/src/app/server/session-serial.js b/src/app/server/session-serial.js deleted file mode 100644 index 7fcb09c..0000000 --- a/src/app/server/session-serial.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import { TerminalBase } from './session-base.js' -import log from '../common/log.js' -import globalState from './global-state.js' - -// `serialport` is a native module that is not built for Android yet. Load it -// lazily and tolerate its absence so the server can still start. -let serialPortPromise = null -function loadSerialPort () { - if (!serialPortPromise) { - serialPortPromise = import('serialport') - .then(m => m.SerialPort) - .catch(err => { - log.warn('serialport is not available, serial terminals disabled:', err.message) - return null - }) - } - return serialPortPromise -} -// const { MockBinding } = require('@serialport/binding-mock') -// MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) - -class TerminalSerial extends TerminalBase { - async init () { - // https://serialport.io/docs/api-stream - const { - autoOpen = true, - baudRate = 9600, - dataBits = 8, - lock = true, - stopBits = 1, - parity = 'none', - rtscts = false, - xon = false, - xoff = false, - xany = false, - txLineEnding = '\r', - rxLineEnding = 'none', - path - } = this.initOptions - const SerialPort = await loadSerialPort() - if (!SerialPort) { - return Promise.reject(new Error('Serial port support is not available on this platform')) - } - this.txLineEnding = txLineEnding - this.rxLineEnding = rxLineEnding - await new Promise((resolve, reject) => { - this.port = new SerialPort({ - // binding: MockBinding, - path, - autoOpen, - baudRate, - dataBits, - lock, - stopBits, - parity, - rtscts, - xon, - xoff, - xany - }, (err) => { - if (err) { - reject(err) - } else { - resolve('ok') - } - }) - }) - if (this.isTest) { - this.kill() - return true - } - globalState.setSession(this.pid, this) - } - - resize () { - - } - - on (event, cb) { - if (event === 'data' && this.rxLineEnding && this.rxLineEnding !== 'none') { - this.port.on('data', (data) => { - const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) - let processed - if (this.rxLineEnding === 'lf_to_crlf') { - processed = str.replace(/\r?\n/g, '\r\n') - } else if (this.rxLineEnding === 'cr_to_crlf') { - processed = str.replace(/\r(?!\n)/g, '\r\n') - } else { - processed = str - } - cb(Buffer.isBuffer(data) ? Buffer.from(processed, 'latin1') : processed) - }) - } else { - this.port.on(event, cb) - } - } - - write (data) { - try { - const str = Buffer.isBuffer(data) ? data.toString('latin1') : String(data) - let out = str - if (this.txLineEnding && this.txLineEnding !== '\r') { - out = str.replace(/\r\n|\r|\n/g, this.txLineEnding) - } - this.port.write(Buffer.isBuffer(data) ? Buffer.from(out, 'latin1') : out) - if (this.sessionLogger) { - this.sessionLogger.write(data) - } - } catch (e) { - log.error(e) - } - } - - /** - * Write raw bytes directly to the serial port, bypassing txLineEnding transformation. - * Used by binary protocols (XMODEM) to avoid corruption of protocol bytes. - */ - writeRaw (data) { - try { - this.port.write(data) - } catch (e) { - log.error(e) - } - } - - kill () { - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - this.port && this.port.isOpen && this.port.close() - delete this.port - this.onEndConn() - } -} - -export async function terminalSerial (initOptions, ws) { - const term = new TerminalSerial(initOptions, ws) - await term.init() - return term -} - -/** - * test ssh connection - * @param {object} options - */ -export function testConnectionSerial (initOptions) { - return (new TerminalSerial(initOptions, undefined, true)) - .init() - .then(() => true) - .catch(() => { - return false - }) -} - -export const terminal = terminalSerial -export const testConnection = testConnectionSerial diff --git a/src/app/server/session-sftp.js b/src/app/server/session-sftp.js deleted file mode 100644 index 0700b61..0000000 --- a/src/app/server/session-sftp.js +++ /dev/null @@ -1,632 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import { - readRemoteFile, - writeRemoteFile -} from './sftp-file.js' -import { commonExtends } from './session-common.js' -import { TerminalBase } from './session-base.js' -import { getSizeCount, getSizeCountWin } from '../common/count-folder-data.js' -import globalState from './global-state.js' -import { SshFs } from 'ssh2-scp' -import iconv from 'iconv-lite' - -class SftpBase extends TerminalBase { - connect (initOptions) { - return this.remoteInitSftp(initOptions) - } - - applySshFsOverride = (sshFs) => { - sshFs.isSshFsFallback = true - this.sftp = sshFs - this.isSshFsFallback = true - const proto = Object.getPrototypeOf(sshFs) - const keys = Object.getOwnPropertyNames(proto) - for (const method of keys) { - if (method === 'constructor') { - continue - } - if (typeof sshFs[method] === 'function') { - this[method] = sshFs[method].bind(sshFs) - } - } - } - - initSshFsFallback = (conn) => { - const opts = {} - const encode = this.initOptions?.encode || 'utf8' - if (encode !== 'utf8') { - opts.encoding = encode - opts.iconv = iconv - } - const sshFs = new SshFs(conn, opts) - this.applySshFsOverride(sshFs) - } - - async remoteInitSftp (initOptions) { - this.initOptions = initOptions - this.transfers = {} - const terminalInst = globalState.getSession(initOptions.terminalId) - const { - conn - } = terminalInst - this.client = conn - this.enableSsh = initOptions.enableSsh - try { - const sftp = await new Promise((resolve, reject) => { - conn.sftp((err, sftp) => { - if (err) { - return reject(err) - } - resolve(sftp) - }) - }) - this.sftp = sftp - } catch (err) { - this.initSshFsFallback(conn) - } - - globalState.setSession(this.pid, this) - return 'ok' - } - - kill () { - const keys = Object.keys(this.transfers || {}) - for (const k of keys) { - const jj = this.transfers[k] - jj && jj.destroy && jj.destroy() - delete this.transfers[k] - } - this.sftp && this.sftp.end && this.sftp.end() - delete this.sftp - delete this.initOptions - this.onEndConn() - } - - escapePosixPath = (value) => { - return `"${String(value).replace(/["\\$`]/g, '\\$&')}"` - } - - escapePowerShellPath = (value) => { - return `'${String(value).replace(/'/g, "''")}'` - } - - normalizeWindowsExecPath = (value) => { - return String(value).replace(/^\/([a-zA-Z]:)/, '$1') - } - - buildPowerShellCommand = (script) => { - return `powershell.exe -NoLogo -NonInteractive -NoProfile -Command "${script}"` - } - - execBuffered (cmd) { - return new Promise((resolve, reject) => { - if (!this.enableSsh) { - return reject(new Error(`do not support ${cmd.split(' ')[0]} operation in sftp mode`)) - } - const { client } = this - client.exec(cmd, this.getExecOpts(), (err, stream) => { - if (err) { - return reject(err) - } - let stdout = Buffer.from('') - let stderr = Buffer.from('') - let settled = false - const settle = (result) => { - if (settled) { - return - } - settled = true - resolve(result) - } - stream.on('close', (code) => { - settle({ - code, - stdout: stdout.toString(), - stderr: stderr.toString() - }) - }).on('end', () => { - settle({ - code: 0, - stdout: stdout.toString(), - stderr: stderr.toString() - }) - }).on('data', (data) => { - stdout = Buffer.concat([stdout, data]) - }) - stream.stderr.on('data', (data) => { - stderr = Buffer.concat([stderr, data]) - }) - }) - }) - } - - async getRemoteExecPlatform () { - if (this.remoteExecPlatform) { - return this.remoteExecPlatform - } - if (!this.remoteExecPlatformPromise) { - this.remoteExecPlatformPromise = this.execBuffered('cmd.exe /d /s /c ver') - .then(({ code, stdout, stderr }) => { - const output = `${stdout}\n${stderr}`.toLowerCase() - return code === 0 && output.includes('windows') - ? 'windows' - : 'posix' - }) - .catch(() => 'posix') - .then((platform) => { - this.remoteExecPlatform = platform - return platform - }) - } - return this.remoteExecPlatformPromise - } - - async buildRemoteCommand (type, ...paths) { - const platform = await this.getRemoteExecPlatform() - if (platform === 'windows') { - const args = paths - .map(this.normalizeWindowsExecPath) - .map(this.escapePowerShellPath) - if (type === 'rmrf') { - return this.buildPowerShellCommand(`Remove-Item -LiteralPath ${args[0]} -Force -Recurse`) - } - if (type === 'cp') { - return this.buildPowerShellCommand(`Copy-Item -LiteralPath ${args[0]} -Destination ${args[1]} -Recurse -Force`) - } - if (type === 'mv') { - return this.buildPowerShellCommand(`Move-Item -LiteralPath ${args[0]} -Destination ${args[1]} -Force`) - } - if (type === 'folder-size') { - return this.buildPowerShellCommand(`Get-ChildItem -LiteralPath ${args[0]} -Recurse -File | Measure-Object -Property Length -Sum`) - } - } - const posixArgs = paths.map(this.escapePosixPath) - if (type === 'rmrf') { - return `rm -rf ${posixArgs[0]}` - } - if (type === 'cp') { - return `cp -r ${posixArgs[0]} ${posixArgs[1]}` - } - if (type === 'mv') { - return `mv ${posixArgs[0]} ${posixArgs[1]}` - } - if (type === 'folder-size') { - return `du -sh ${posixArgs[0]} && find ${posixArgs[0]} -type f | wc -l` - } - throw new Error(`unsupported remote command type: ${type}`) - } - - /** - * getHomeDir - * - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * only support linux / mac - * @return {Promise} - */ - getHomeDir () { - // return this.runCmd('eval echo "~$different_user"') - // ext_home_dir - return this.realpath('') - } - - // getSftpHomeDir () { - // // return this.runCmd('eval echo "~$different_user"') - // // ext_home_dir - // return new Promise((resolve, reject) => { - // this.sftp.ext_home_dir('', (err, path) => { - // if (err) { - // return reject(err) - // } - // resolve(path) - // }) - // }) - // } - - /** - * rmdir - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * only support rm -rf - * @return {Promise} - */ - rmdir (remotePath) { - return this.rmrf(remotePath) - .then(r => { - return r - }) - .catch(err => { - console.error('rm -rf dir error', err) - return this.removeDirectoryRecursively(remotePath) - }) - } - - rmrf (remotePath) { - return this.buildRemoteCommand('rmrf', remotePath) - .then(cmd => this.runExec(cmd)) - // return new Promise((resolve, reject) => { - // const { client } = this - // const cmd = `rm -rf "${remotePath}"` - // this.runExec(cmd, this.getExecOpts(), (err, stream) => { - // if (err) { - // return reject(err) - // } else { - // console.log('rm -rf done', stream) - // resolve(1) - // } - // }) - // }) - } - - async removeDirectoryRecursively (remotePath) { - const contents = await this.list(remotePath) - for (const item of contents) { - const itemPath = `${remotePath}/${item.name}` - if (item.type === 'd') { - // Recursively delete subdirectories - await this.removeDirectoryRecursively(itemPath) - } else { - // Delete files - await this.rm(itemPath) - } - } - // Finally, remove the directory itself - await this.rmFolder(remotePath) - } - - /** - * touch a file - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - touch (remotePath) { - // if (this.enableSsh) { - // return new Promise((resolve, reject) => { - // const { client } = this - // const cmd = `touch "${remotePath}"` - // client.exec(cmd, this.getExecOpts(), err => { - // if (err) reject(err) - // else resolve(1) - // }) - // }) - // } - return this.touchFile(remotePath) - } - - openFile = (remotePath) => { - return new Promise((resolve, reject) => { - this.sftp.open(remotePath, 'w', (err, fd) => { - if (err) { - return reject(err) - } - resolve(fd) - }) - }) - } - - closeFile = (fd) => { - return new Promise((resolve, reject) => { - this.sftp.close(fd, err => { - if (err) { - return reject(err) - } - resolve(true) - }) - }) - } - - touchFile = (remotePath) => { - return this.openFile(remotePath) - .then(this.closeFile) - } - - /** - * cp - * - * @param {String} from - * @param {String} to - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - cp (from, to) { - return this.buildRemoteCommand('cp', from, to) - .then(cmd => this.runExec(cmd)) - .then(() => 1) - } - - /** - * mv - * - * @param {String} from - * @param {String} to - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - mv (from, to) { - return this.buildRemoteCommand('mv', from, to) - .then(cmd => this.runExec(cmd)) - .then(() => 1) - } - - runExec (cmd) { - return this.execBuffered(cmd) - .then(({ code, stdout, stderr }) => { - if (stderr) { - throw new Error(stderr.trim()) - } - if (typeof code === 'number' && code !== 0) { - throw new Error(stdout.trim() || `Command exited with code ${code}`) - } - return stdout - }) - } - - async getFolderSize (folderPath) { - const platform = await this.getRemoteExecPlatform() - const cmd = await this.buildRemoteCommand('folder-size', folderPath) - const output = await this.runExec(cmd) - return platform === 'windows' - ? getSizeCountWin(output) - : getSizeCount(output) - } - - /** - * list remote directory - * - * @param {String} remotePath - * @return {Promise} list - */ - list (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - const reg = /-/g - - sftp.readdir(remotePath, (err, list) => { - if (err) { - return reject(err) - } - resolve(list.map(item => { - const { - filename, - longname, - attrs: { - size, mtime, atime, uid, gid, mode - } - } = item - // from https://github.com/jyu213/ssh2-sftp-client/blob/master/src/index.js - return { - type: longname.substr(0, 1), - name: filename, - size, - modifyTime: mtime * 1000, - accessTime: atime * 1000, - mode, - rights: { - user: longname.substr(1, 3).replace(reg, ''), - group: longname.substr(4, 3).replace(reg, ''), - other: longname.substr(7, 3).replace(reg, '') - }, - owner: uid, - group: gid - } - })) - }) - }) - } - - /** - * mkdir - * - * @param {String} remotePath - * @param {Object} attributes - * An object with the following valid properties: - - mode - integer - Mode/permissions for the resource. - uid - integer - User ID of the resource. - gid - integer - Group ID of the resource. - size - integer - Resource size in bytes. - atime - integer - UNIX timestamp of the access time of the resource. - mtime - integer - UNIX timestamp of the modified time of the resource. - - When supplying an ATTRS object to one of the SFTP methods: - atime and mtime can be either a Date instance or a UNIX timestamp. - mode can either be an integer or a string containing an octal number. - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - mkdir (remotePath, options = {}) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.mkdir(remotePath, options, err => { - if (err) reject(err) - else resolve(1) - }) - }) - } - - /** - * stat - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} stat - * stats.isDirectory() - stats.isFile() - stats.isBlockDevice() - stats.isCharacterDevice() - stats.isSymbolicLink() - stats.isFIFO() - stats.isSocket() - */ - stat (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.stat(remotePath, (err, stat) => { - if (err) reject(err) - else { - resolve( - Object.assign(stat, { - isDirectory: stat.isDirectory() - }) - ) - } - }) - }) - } - - /** - * readlink - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} target - */ - readlink (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.readlink(remotePath, (err, target) => { - if (err) reject(err) - else resolve(target) - }) - }) - } - - /** - * realpath - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} target - */ - realpath (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.realpath(remotePath, (err, target) => { - if (err) reject(err) - else resolve(target) - }) - }) - } - - /** - * lstat - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} stat - * stats.isDirectory() - stats.isFile() - stats.isBlockDevice() - stats.isCharacterDevice() - stats.isSymbolicLink() - stats.isFIFO() - stats.isSocket() - */ - lstat (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.lstat(remotePath, (err, stat) => { - if (err) reject(err) - else resolve(stat) - }) - }) - } - - /** - * chmod - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - chmod (remotePath, mode) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.chmod(remotePath, mode, (err) => { - if (err) reject(err) - else resolve(1) - }) - }) - } - - /** - * rename - * - * @param {String} remotePath - * @param {String} remotePathNew - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - rename (remotePath, remotePathNew) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.rename(remotePath, remotePathNew, (err) => { - if (err) reject(err) - else resolve(1) - }) - }) - } - - /** - * rm delete single file - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - rmFolder (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.rmdir(remotePath, (err) => { - if (err) reject(err) - else resolve(1) - }) - }) - } - - /** - * rm delete single file - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - rm (remotePath) { - return new Promise((resolve, reject) => { - const { sftp } = this - sftp.unlink(remotePath, (err) => { - if (err) reject(err) - else resolve(1) - }) - }) - } - - /** - * readFile single file - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - readFile (remotePath) { - return readRemoteFile(this.sftp, remotePath) - } - - /** - * writeFile single file - * - * @param {String} remotePath - * https://github.com/mscdex/ssh2/blob/master/SFTP.md - * @return {Promise} - */ - writeFile (remotePath, str, mode) { - return writeRemoteFile(this.sftp, remotePath, str, mode) - } - // end -} - -export const Sftp = commonExtends(SftpBase) diff --git a/src/app/server/session-spice.js b/src/app/server/session-spice.js deleted file mode 100644 index 429c4dd..0000000 --- a/src/app/server/session-spice.js +++ /dev/null @@ -1,136 +0,0 @@ -import log from '../common/log.js' -import { TerminalBase } from './session-base.js' -import globalState from './global-state.js' -import { handleConnection } from './spice-proxy.js' -import net from 'net' -import proxySock from './socks.js' - -class TerminalSpice extends TerminalBase { - init = async () => { - this.wsMap = new Map() - this.channelCounter = 0 - globalState.setSession(this.pid, this) - return Promise.resolve(this) - } - - start = async (query = {}, ws) => { - if (!ws) { - log.error(`[SPICE:${this.pid}] No WebSocket provided`) - return - } - - const { - host, - port = 5900, - proxy, - readyTimeout = 10000 - } = this.initOptions - - this.channelCounter++ - const connId = `${this.channelCounter}` - this.wsMap.set(connId, ws) - - log.debug(`[SPICE:${this.pid}] Starting SPICE channel #${connId} to ${host}:${port}, total channels: ${this.wsMap.size}`) - - const cleanup = () => { - this.wsMap.delete(connId) - log.debug(`[SPICE:${this.pid}] Channel #${connId} closed, remaining: ${this.wsMap.size}`) - if (this.wsMap.size === 0) { - this.kill() - } - } - - handleConnection(ws, { - host, - port, - proxy, - readyTimeout, - onCleanup: cleanup, - channelId: `#${connId}` - }) - } - - resize = () => { - } - - test = async () => { - const { - host, - port = 5900, - proxy, - readyTimeout = 10000 - } = this.initOptions - - if (proxy) { - const proxyResult = await proxySock({ - readyTimeout, - host, - port, - proxy - }) - const socket = proxyResult.socket - socket.destroy() - return true - } - - return new Promise((resolve, reject) => { - const socket = net.createConnection({ host, port }, () => { - socket.destroy() - resolve(true) - }) - socket.on('error', (err) => { - reject(err) - }) - socket.setTimeout(readyTimeout, () => { - socket.destroy() - reject(new Error('Connection timed out')) - }) - }) - } - - kill = () => { - log.debug('Closed SPICE session ' + this.pid + ', remaining connections: ' + this.wsMap.size) - for (const ws of this.wsMap.values()) { - try { - ws.close() - } catch (e) { - log.debug(`[SPICE:${this.pid}] ws.close() error:`, e.message) - } - } - this.wsMap.clear() - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - const { pid } = this - const inst = globalState.getSession(pid) - if (!inst) { - return - } - globalState.removeSession(pid) - } -} - -export const terminalSpice = async function (initOptions, ws) { - const term = new TerminalSpice(initOptions, ws) - await term.init() - return term -} - -/** - * test spice connection - * @param {object} options - */ -export const testConnectionSpice = (options) => { - return (new TerminalSpice(options, undefined, true)) - .test() - .then((res) => { - res.close() - return true - }) - .catch(() => { - return false - }) -} - -export const terminal = terminalSpice -export const testConnection = testConnectionSpice diff --git a/src/app/server/session-ssh.js b/src/app/server/session-ssh.js deleted file mode 100644 index 1b9da2c..0000000 --- a/src/app/server/session-ssh.js +++ /dev/null @@ -1,1084 +0,0 @@ -/** - * terminal/sftp/serial class - */ - -import { Client } from '@electerm/ssh2' -import proxySock from './socks.js' -import _ from 'lodash' -import generate from '../common/uid.js' -import { resolve as pathResolve } from 'path' -import net from 'net' -import { exec } from 'child_process' -import log from '../common/log.js' -import fs from 'fs' -import { algDefault, algAlt } from './ssh2-alg.js' -import * as sshTunnelFuncs from './ssh-tunnel.js' -import deepCopy from 'json-deep-copy' -import { TerminalBase } from './session-base.js' -import { commonExtends } from './session-common.js' -import globalState from './global-state.js' -import { - sshKeysPath -} from '../common/runtime-constants.js' -import { createHostVerifier } from './ssh-known-hosts.js' -import iconv from 'iconv-lite' -import { maybeProxyCommand } from './ssh-proxy-command.js' - -// Encodings that are equivalent to UTF-8 (no conversion needed) -const utf8Aliases = new Set(['utf-8', 'utf8', 'utf-8-strict']) - -const failMsg = 'All configured authentication methods failed' -const csFailMsg = 'no matching C->S cipher' - -class TerminalSshBase extends TerminalBase { - async remoteInitProcess () { - this.adjustConnectionOrder() - const { - initOptions - } = this - const hasX11 = initOptions.x11 === true - this.display = hasX11 ? await this.getDisplay() : undefined - this.x11Cookie = hasX11 ? await this.getX11Cookie() : undefined - return this.sshConnect() - } - - reTryAltAlg () { - log.log('retry with default ciphers/server hosts') - this.doKill() - this.connectOptions.algorithms = algAlt() - this.altAlg = true - return this.sshConnect() - } - - getShellWindow (initOptions = this.initOptions) { - return _.pick(initOptions, [ - 'rows', 'cols', 'term' - ]) - } - - getAgent () { - const { initOptions } = this - return initOptions.useSshAgent !== false ? (initOptions.sshAgent || process.env.SSH_AUTH_SOCK) : undefined - } - - getAuthOrder (connectOptions) { - const authOrder = ['none'] - if (connectOptions.password !== undefined) { - authOrder.push('password') - } - if (connectOptions.privateKey !== undefined) { - authOrder.push('publickey') - } - if (connectOptions.agent !== undefined) { - authOrder.push('agent') - } - if (connectOptions.tryKeyboard) { - authOrder.push('keyboard-interactive') - } - if ( - connectOptions.privateKey !== undefined && - connectOptions.localHostname !== undefined && - connectOptions.localUsername !== undefined - ) { - authOrder.push('hostbased') - } - return authOrder - } - - createAuthHandler (connectOptions) { - const authOrder = this.getAuthOrder(connectOptions) - let attemptedMethods = new Set() - - const isMethodAllowed = (type, allowedSet) => { - if (type === 'agent') { - return allowedSet.has('agent') || allowedSet.has('publickey') - } - return allowedSet.has(type) - } - - return (authsLeft, partialSuccess) => { - if (partialSuccess) { - this.authPartiallySucceeded = true - attemptedMethods = new Set() - } - - const allowedMethods = Array.isArray(authsLeft) && authsLeft.length - ? authsLeft - : authOrder - const allowedSet = new Set(allowedMethods) - const nextAuth = authOrder.find(type => { - return isMethodAllowed(type, allowedSet) && (partialSuccess || !attemptedMethods.has(type)) - }) - - if (!nextAuth) { - return false - } - - attemptedMethods.add(nextAuth) - return nextAuth - } - } - - adjustConnectionOrder () { - const { initOptions } = this - if (!initOptions.hasHopping || !initOptions.connectionHoppings || initOptions.connectionHoppings.length === 0) { - return - } - - const currentHostHopping = { - host: initOptions.host, - port: initOptions.port, - username: initOptions.username, - password: initOptions.password, - privateKey: initOptions.privateKey, - passphrase: initOptions.passphrase - } - - const [firstHopping, ...restHoppings] = initOptions.connectionHoppings - const pickProps = _.pick(firstHopping, [ - 'host', 'port', 'username', 'password', 'privateKey', 'passphrase', 'certificate' - ]) - Object.assign(initOptions, pickProps) - initOptions.connectionHoppings = [...restHoppings, currentHostHopping] - } - - isLikely2FAPrompts (prompts) { - if (!prompts || !prompts.length) return false - const defaultKeywords = [ - 'verification code', - 'otp', - 'one-time', - 'two-factor', - '2fa', - 'totp', - 'authenticator', - 'duo', - 'yubikey', - 'security code', - 'mfa', - 'passcode' - ] - const rawKeywords = this.initOptions?.keyword2FA - const twofaKeywords = Array.isArray(rawKeywords) - ? rawKeywords - : typeof rawKeywords === 'string' - ? rawKeywords.split(/[,\n]/).map(s => s.trim()).filter(Boolean) - : [] - const finalKeywords = twofaKeywords.length - ? twofaKeywords.map(s => s.toLowerCase()) - : defaultKeywords - return prompts.some(p => { - const text = (p.prompt || '').toLowerCase() - return finalKeywords.some(kw => text.includes(kw)) - }) - } - - onKeyboardEvent (options, passwordOverride) { - if (options?.mode !== 'confirm' && this.initOptions?.interactiveValues) { - return Promise.resolve(this.initOptions.interactiveValues.split('\n')) - } - // Auto-fill password prompt if we have a saved password - // passwordOverride is used during SSH connection hopping, where - // this.initOptions.password is the jump host's password (after - // adjustConnectionOrder swaps the options), not the target's. - // The caller passes connectOptions.password which is correct for - // the current connection being established. - const { prompts } = options - const savedPassword = passwordOverride !== undefined - ? passwordOverride - : this.initOptions?.password - if (prompts && prompts.length === 1 && savedPassword) { - const prompt = prompts[0] - const promptText = (prompt.prompt || '').toLowerCase() - // Check if this is a password prompt (hidden input, contains "password" or is empty) - if (!prompt.echo && (promptText.includes('password') || promptText === '')) { - return Promise.resolve([savedPassword]) - } - } - - const id = generate() - this.ws?.s({ - id, - action: 'session-interactive', - ..._.pick(this.initOptions, [ - 'interactiveValues', - 'tabId' - ]), - options - }) - return new Promise((resolve, reject) => { - this.ws?.once((arg) => { - const { results } = arg - if (_.isEmpty(results)) { - return reject(new Error('User cancel')) - } - resolve(results) - }, id) - }) - } - - async getPrivateKeysInJumpServer (conn) { - const r = await this.runCmd('ls ~/.ssh', conn) - .catch(err => { - log.error(err) - }) - return r - ? r.split('\n') - .filter(d => d.endsWith('.pub')) - .map(d => `~/.ssh/${d}`.replace('.pub', '')) - : [] - } - - catPrivateKeyInJumpServer (conn, filePath) { - return this.runCmd(`cat ${filePath}`, conn) - } - - async readPrivateKeyInJumpServer (conn) { - const { hoppingOptions } = this - if (this.jumpSshKeys) { - if (this.jumpSshKeys.length > 0) { - const p = this.jumpSshKeys.shift() - this.jumpPrivateKeyPathFrom = p - hoppingOptions.privateKey = await this.catPrivateKeyInJumpServer(conn, p) - } else if (this.jumpSshKeys.length === 0) { - delete hoppingOptions.privateKey - delete this.jumpSshKeys - hoppingOptions.sshKeysDrain = true - } - return - } - if (hoppingOptions.sshKeysDrain || hoppingOptions.password || hoppingOptions.privateKey) { - return null - } - const list = await this.getPrivateKeysInJumpServer(conn) - if (list.length) { - const p = list.shift() - this.jumpPrivateKeyPathFrom = p - hoppingOptions.privateKey = await this.catPrivateKeyInJumpServer(conn, p) - this.jumpSshKeys = list - } else { - // No private keys found in jump server, mark as drained so we can prompt for password - hoppingOptions.sshKeysDrain = true - } - } - - handleKeyboardEventForRetryJump (options) { - return this.onKeyboardEvent(options) - .then(data => { - if (data && data[0]) { - this.hoppingOptions.passphrase = data[0] - this.jumpSshKeys && this.jumpSshKeys.unshift(this.jumpPrivateKeyPathFrom) - } - return this.jumpConnect(true, true) - }) - .catch(e => { - log.error('errored get passphrase for', this.jumpHostFrom, this.jumpPrivateKeyPathFrom, e) - return this.jumpConnect(true, false) - }) - } - - async retryJump () { - const next = await this.doSshConnect( - undefined, - this.nextConn, - this.hoppingOptions, - !this.isLast - ) - .then(() => { - this.jumpHostFrom = this.initHoppingOptions.host - this.jumpPortFrom = this.initHoppingOptions.port - return this.nextConn - }) - .catch(err => err) - - const isError = next instanceof Error - if (!isError) { - return next - } - const err = next - log.error('error when do jump connect', this.nextHost, this.nextPort) - if (err.message.includes('passphrase')) { - const options = { - name: `passphase for ${this.jumpHostFrom}/${this.jumpPrivateKeyPathFrom}`, - instructions: [''], - prompts: [{ - echo: false, - prompt: 'passphase' - }] - } - return this.handleKeyboardEventForRetryJump(options) - } else if ( - !this.jumpSshKeys && - !this.hoppingOptions.sshKeysDrain && - !this.hoppingOptions.password && - !this.hoppingOptions.privateKey && - err.message.includes(failMsg) - ) { - // SSH agent failed or no agent, try reading private keys from jump server - // This will read ~/.ssh keys and retry - return this.jumpConnect(true, false) - } else if ( - this.hoppingOptions.sshKeysDrain && - !this.hoppingOptions.password && - err.message.includes(failMsg) - ) { - // All private keys exhausted, ask for password - const options = { - name: `password for ${this.hoppingOptions.username}@${this.initHoppingOptions.host}`, - instructions: [''], - prompts: [{ - echo: false, - prompt: 'password' - }] - } - return this.onKeyboardEvent(options) - .then(data => { - if (data && data[0]) { - this.hoppingOptions.password = data[0] - return this.jumpConnect(true, true) - } else if (data && data[0] === '') { - throw err - } - }) - .catch(err => { - log.error('errored get password for', err) - throw err - }) - } else if ( - this.jumpSshKeys - ) { - return this.jumpConnect(true, false) - } else { - throw err - } - } - - async jumpConnect (reBuildSock = false, skipReadKeys = false) { - if (reBuildSock) { - this.hoppingOptions.sock.end() - this.hoppingOptions.sock = await this.forwardOut(this.conn, this.initHoppingOptions) - } - // Only read private keys if skipReadKeys is false - // On first connect, we skip reading keys to let SSH agent try first - // If SSH agent fails, we then read and try private keys - if (!skipReadKeys) { - await this.readPrivateKeyInJumpServer(this.conn) - } - return this.retryJump() - } - - forwardOut (conn, hopping) { - return new Promise((resolve, reject) => { - conn.forwardOut('127.0.0.1', 0, hopping.host, hopping.port, async (err, stream) => { - if (err) { - log.error(`forwardOut to ${hopping.host}:${hopping.port} error: ` + err) - this.endConns() - return reject(err) - } - resolve(stream) - }) - }) - } - - async jump () { - const sock = await this.forwardOut(this.conn, this.initHoppingOptions) - const hopping = deepCopy(this.initHoppingOptions) - delete hopping.host - delete hopping.port - this.nextHost = hopping.host - this.nextPort = hopping.port - this.hoppingOptions = { - sock, - ...hopping - } - this.nextConn = new Client() - // If we have an agent and no explicit privateKey/password, try agent first - // by skipping reading private keys from jump server - const hasAgent = !!this.hoppingOptions.agent - const hasExplicitAuth = this.hoppingOptions.password || this.hoppingOptions.privateKey - const skipReadKeys = hasAgent && !hasExplicitAuth - await this.jumpConnect(false, skipReadKeys) - return this.nextConn - } - - async hopping (connectionHoppings) { - this.conns = [] - this.jumpHostFrom = this.initOptions.host - this.jumpPortFrom = this.initOptions.port - const len = connectionHoppings.length - for (let i = 0; i < len; i++) { - const hopping = connectionHoppings[i] - this.conns.push(this.conn) - this.initHoppingOptions = { - ...hopping, - agent: this.getAgent(), - ...this.getShareOptions() - } - this.isLast = i === len - 1 - const conn = await this.jump() - if (conn) { - this.conn = conn - } - } - } - - endConns () { - this.conn && this.conn.end && this.conn.end() - while (this.conns && this.conns.length) { - const conn = this.conns.shift() - conn && conn.end() - } - } - - async runTunnel (sshTunnel) { - return sshTunnelFuncs[sshTunnel.sshTunnel]({ - ...sshTunnel, - conn: this.conn - }) - .then(r => { - return { - sshTunnel - } - }) - .catch(err => { - log.error('error when do sshTunnel', err) - return { - error: err.message, - sshTunnel - } - }) - } - - async onInitSshReady () { - const { - initOptions, - isTest, - shellOpts, - shellWindow - } = this - if ( - initOptions.connectionHoppings?.length - ) { - await this.hopping(initOptions.connectionHoppings) - } - if (isTest) { - this.endConns() - return - } else if (initOptions.enableSsh === false) { - globalState.setSession(this.pid, this) - return this - } - const { sshTunnels = [] } = initOptions - const sshTunnelResults = [] - for (const sshTunnel of sshTunnels) { - if ( - sshTunnel && - sshTunnel.sshTunnel && - sshTunnel.sshTunnelLocalPort - ) { - const result = await this.runTunnel(sshTunnel) - sshTunnelResults.push(result) - } - } - if (!this.ws) { - this.sshTunnelResults = sshTunnelResults - } else { - this.ws?.s({ - update: { - sshTunnelResults - }, - action: 'ssh-tunnel-result', - tabId: this.initOptions.srcTabId - }) - } - return new Promise((resolve, reject) => { - this.conn.shell( - shellWindow, - shellOpts, - (err, channel) => { - if (err) { - return reject(err) - } - this.channel = channel - this.conn.setNoDelay(true) - globalState.setSession(this.pid, this) - resolve(this) - } - ) - }) - } - - shell (conn, shellWindow, shellOpts) { - return new Promise((resolve, reject) => { - conn.shell( - shellWindow, - shellOpts, - (err, channel) => { - if (err) { - return reject(err) - } - resolve(channel) - } - ) - }) - } - - getSSHKeys () { - try { - return fs.readdirSync(sshKeysPath) - .filter(file => file.endsWith('.pub')) - .map(file => pathResolve(sshKeysPath, file.replace('.pub', ''))) - } catch (e) { - log.error(e) - return [] - } - } - - getPrivateKey (connectOptions) { - if (this.sshKeys) { - if (this.sshKeys.length > 0) { - const p = this.sshKeys.shift() - this.privateKeyPath = p - connectOptions.privateKey = fs.readFileSync(p, 'utf8') - } else if (this.sshKeys.length === 0) { - this.connectOptions.passphrase = this.initOptions.passphrase - delete this.connectOptions.privateKey - delete this.sshKeys - } - return - } - const list = this.getSSHKeys() - if (list.length) { - const p = list.shift() - this.privateKeyPath = p - connectOptions.privateKey = fs.readFileSync(p, 'utf8') - this.sshKeys = list - } - } - - doSshConnect = ( - info, - conn = this.conn, - connectOptions = this.connectOptions, - skipX11 = false - ) => { - const { - initOptions - } = this - if (info && info.socket) { - delete connectOptions.host - delete connectOptions.port - connectOptions.sock = info.socket - } - this.hostVerificationError = null - const verifyTarget = this.getHostVerificationTarget(connectOptions) - if (this.skipHostVerification && connectOptions.sock) { - // proxied connection (netbird ssh proxy / proxyCommand): - // the child serves its own endpoint with an ephemeral host key - delete connectOptions.hostVerifier - } else { - connectOptions.hostVerifier = createHostVerifier({ - ...verifyTarget, - confirm: async (options) => { - const results = await this.onKeyboardEvent(options) - return results && results[0] === (options.confirmResult || 'trust') - }, - onError: (err) => { - this.hostVerificationError = err - } - }) - } - this.authPartiallySucceeded = false - connectOptions.authHandler = this.createAuthHandler(connectOptions) - return new Promise((resolve, reject) => { - conn.on('keyboard-interactive', async ( - name, - instructions, - instructionsLang, - prompts, - finish - ) => { - if (initOptions.ignoreKeyboardInteractive) { - return finish( - (prompts || []).map((n, i) => { - return i ? '' : (connectOptions.password || '') - }) - ) - } - // Detect 2FA: if we connected with password and prompts look like 2FA, - // disconnect and retry without password so keyboard-interactive handles both - if ( - !this.retry2FA && - !this.authPartiallySucceeded && - connectOptions.password && - this.isLikely2FAPrompts(prompts) - ) { - this.retry2FA = true - conn.end() - return reject(new Error('2FA_RETRY')) - } - const options = { - name, - instructions, - instructionsLang, - prompts - } - this.onKeyboardEvent(options, connectOptions.password ?? this.initOptions?.password ?? null) - .then(finish) - .catch(reject) - }) - if (!skipX11) { - conn.on('x11', (inf, accept) => { - let start = 0 - const maxRetry = 100 - const portStart = 6000 - const maxPort = portStart + maxRetry - const retry = () => { - if (start >= maxPort) { - return - } - const xserversock = new net.Socket() - let xclientsock - xserversock - .on('connect', function () { - xclientsock = accept() - xclientsock.pipe(xserversock).pipe(xclientsock) - }) - .on('error', (e) => { - log.error(e) - xserversock.destroy() - start = start === maxRetry ? portStart : start + 1 - retry() - }) - .on('close', () => { - xserversock.destroy() - xclientsock && xclientsock.destroy() - }) - if (start < portStart) { - const addr = (this.display || '').includes('/tmp') - ? this.display - : `/tmp/.X11-unix/X${start}` - xserversock.connect(addr) - } else { - xserversock.connect(start, '127.0.0.1') - } - } - retry() - }) - } - conn - .on('ready', () => resolve(true)) - .on('error', err => { - reject(this.hostVerificationError || err) - }) - .connect(connectOptions) - }) - } - - /** - * when connecting through a proxy command (netbird ssh proxy or - * user-defined proxyCommand option), surface the command's stderr - * (netbird prints the SSO login URL there) to the user - */ - onProxyCommandMessage (text) { - log.log('ssh proxy command:', text.trim()) - const url = text.match(/https?:\/\/\S+/) - if (url && this.ws && !this.proxyCommandUrlShown) { - this.proxyCommandUrlShown = true - this.ws.s({ - action: 'ssh-proxy-command-message', - message: text.trim(), - url: url[0], - tabId: this.initOptions.srcTabId - }) - } - } - - /** - * if a proxy command applies (netbird auto-detect or explicit - * proxyCommand option), spawn it and return the bridged socket - */ - async maybeProxyCommandSock () { - if (this.initOptions?.connectionHoppings?.length) { - return undefined - } - const info = await maybeProxyCommand( - this.initOptions, - this.connectOptions, - { onMessage: (text) => this.onProxyCommandMessage(text) } - ) - if (!info) { - return undefined - } - this.proxyCommandDispose = info.dispose - // the proxy command serves its own ssh endpoint (random host key - // per run for netbird), known_hosts verification can not apply - this.skipHostVerification = true - return { socket: info.socket } - } - - getShareOptions () { - const { initOptions } = this - const all = { - tryKeyboard: true, - readyTimeout: initOptions.readyTimeout, - keepaliveCountMax: initOptions.keepaliveCountMax, - keepaliveInterval: initOptions.keepaliveInterval, - algorithms: algDefault() - } - if (initOptions.serverHostKey && initOptions.serverHostKey.length) { - all.algorithms.serverHostKey = deepCopy(initOptions.serverHostKey) - } - if (initOptions.cipher && initOptions.cipher.length) { - all.algorithms.cipher = deepCopy(initOptions.cipher) - } - if (initOptions.compress && initOptions.compress.length) { - all.algorithms.compress = deepCopy(initOptions.compress) - } - return all - } - - getHostVerificationTarget (connectOptions = this.connectOptions) { - if (connectOptions === this.hoppingOptions && this.initHoppingOptions) { - return { host: this.initHoppingOptions.host, port: this.initHoppingOptions.port } - } - return { - host: connectOptions.host || this.initOptions.host, - port: connectOptions.port || this.initOptions.port - } - } - - buildConnectOptions () { - const { initOptions } = this - const connectOptions = Object.assign( - this.getShareOptions(), - { - agent: this.getAgent() - }, - _.pick(initOptions, [ - 'host', - 'port', - 'username', - 'password', - 'privateKey', - 'passphrase', - 'certificate', - 'encode' - ]) - ) - if (initOptions.isMFA) { - this.retry2FA = true - delete connectOptions.password - } - if (initOptions.debug) { - connectOptions.debug = log.log - } - if (!connectOptions.passphrase) { - delete connectOptions.passphrase - } - return connectOptions - } - - buildShellOpts () { - const { initOptions } = this - let x11 - if (initOptions.x11 === true) { - x11 = { - cookie: this.x11Cookie - } - } - const shellOpts = { - x11 - } - shellOpts.env = this.getEnv(initOptions) - return shellOpts - } - - getUserName (connectOptions) { - const options = { - name: 'username', - instructions: [''], - prompts: [{ - echo: false, - prompt: '' - }] - } - return this.onKeyboardEvent(options) - .then(data => { - const username = data ? data[0] : '' - if (username) { - this.connectOptions.username = data[0] - } - return this.sshConnect() - }) - .catch(e => { - log.error('errored get username for', e) - return this.nextTry(e) - }) - } - - async sshConnect () { - const { initOptions } = this - this.conn = new Client() - this.connectOptions = this.connectOptions || this.buildConnectOptions() - const { - connectOptions - } = this - if (!connectOptions.username) { - return this.getUserName(connectOptions) - } - if ( - this.sshKeys || - (!connectOptions.privateKey && !connectOptions.password && !initOptions.password) - ) { - this.getPrivateKey(this.connectOptions) - } - this.shellWindow = this.shellWindow || this.getShellWindow() - this.shellOpts = this.shellOpts || this.buildShellOpts() - // dispose proxy command child from a previous attempt (retries re-enter here) - if (this.proxyCommandDispose) { - this.proxyCommandDispose() - this.proxyCommandDispose = null - } - const info = initOptions.proxy - ? await proxySock({ - readyTimeout: initOptions.readyTimeout, - host: initOptions.host, - port: initOptions.port, - proxy: initOptions.proxy - }) - : await this.maybeProxyCommandSock() - const skipX11 = !!initOptions.connectionHoppings?.length - const result = await this.doSshConnect( - info, - undefined, - undefined, - skipX11 - ).catch(err => err) - if (!(result instanceof Error)) { - return this.onInitSshReady() - } - const err = result - log.error('error when do sshConnect', err, this.privateKeyPath) - if ( - err.message.includes(csFailMsg) && - !this.altAlg - ) { - return this.reTryAltAlg() - } else if (err.message === '2FA_RETRY') { - log.log('2FA detected, retrying without password in auth') - delete this.connectOptions.password - return this.sshConnect() - } else if (err.message.includes('passphrase')) { - const options = { - name: `passphase for ${this.privateKeyPath || 'privateKey'}`, - instructions: [''], - prompts: [{ - echo: false, - prompt: 'passphase' - }] - } - return this.onKeyboardEvent(options) - .then(data => { - const pass = data ? data[0] : '' - if (pass) { - this.connectOptions.passphrase = data[0] - this.sshKeys && this.sshKeys.unshift(this.privateKeyPath) - } - return this.nextTry(err, !!pass) - }) - .catch(e => { - log.error('errored get passphrase for', this.privateKeyPath, e) - return this.nextTry(err) - }) - } else if ( - this.sshKeys && - err.message.includes(failMsg) - ) { - return this.nextTry(err) - } else if ( - !this.retry2FA && - !this.connectOptions.password && - this.initOptions.password - ) { - this.connectOptions.password = this.initOptions.password - return this.sshConnect() - } else if ( - err.message.includes(failMsg) && - !this.connectOptions.password - ) { - const options = { - name: `password for ${this.initOptions.username}@${this.initOptions.host}`, - instructions: [''], - prompts: [{ - echo: false, - prompt: 'password' - }] - } - return this.onKeyboardEvent(options) - .then(data => { - if (data && data[0]) { - this.connectOptions.password = data[0] - return this.sshConnect() - } else if (data && data[0] === '') { - throw err - } - }) - .catch(err => { - log.error('errored get password for', err) - throw err - }) - } - return this.nextTry(err) - } - - nextTry (err, forceRetry = false) { - if ( - this.sshKeys || forceRetry - ) { - log.log('retry with next ssh key') - if (this.conn) { - this.conn.end() - } - return this.sshConnect() - } else { - throw err - } - } - - resize (cols, rows) { - this.channel?.setWindow(rows, cols) - } - - on (event, cb) { - this.channel.on(event, cb) - this.channel.stderr.on(event, cb) - } - - write (data) { - const encode = this.connectOptions?.encode || this.initOptions?.encode - if (encode && !utf8Aliases.has(encode.toLowerCase()) && typeof data === 'string') { - try { - const buf = iconv.encode(data, encode) - this.channel?.write(buf) - return - } catch (e) { - log.warn('iconv encode failed, falling back to raw write:', e.message) - } - } - this.channel?.write(data) - } - - setNoDelay (noDelay = true) { - try { - if (this.conn && typeof this.conn.setNoDelay === 'function') { - this.conn.setNoDelay(noDelay) - } - } catch (e) { - log.warn('failed to set ssh noDelay', e) - } - } - - kill () { - this.initOptions = null - this.connectOptions = null - this.proxyCommandDispose = null - this.skipHostVerification = null - this.proxyCommandUrlShown = null - this.alg = null - this.shellWindow = null - this.shellOpts = null - this.conn = null - this.sshKeys = null - this.privateKeyPath = null - this.display = null - this.x11Cookie = null - this.conns = null - this.jumpSshKeys = null - this.jumpPrivateKeyPathFrom = null - this.hoppingOptions = null - this.initHoppingOptions = null - this.nextConn = null - this.doKill() - } - - doKill () { - if (this.proxyCommandDispose) { - this.proxyCommandDispose() - this.proxyCommandDispose = null - } - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - this.channel && this.channel.end() - delete this.channel - this.onEndConn() - // Clean up any remaining connection - if (this.conn) { - this.conn.end() - this.conn = null - } - } - - getLocalEnv () { - return { - env: process.env - } - } - - getDisplay () { - return new Promise((resolve) => { - exec('echo $DISPLAY', this.getLocalEnv(), (err, out, e) => { - if (err || e) { - resolve('') - } else { - resolve((out || '').trim()) - } - }) - }) - } - - getX11Cookie () { - return new Promise((resolve) => { - exec('xauth list :0', this.getLocalEnv(), (err, out, e) => { - if (err || e) { - resolve('') - } else { - const s = out || '' - const reg = /MIT-MAGIC-COOKIE-1 +([\d\w]{1,38})/ - const arr = s.match(reg) - resolve( - arr ? arr[1] || '' : '' - ) - } - }) - }) - } - - init () { - return this.remoteInitProcess() - } -} - -const TerminalSsh = commonExtends(TerminalSshBase) - -export const terminalSsh = function (initOptions, ws) { - return (new TerminalSsh(initOptions, ws)).init() -} - -/** - * test ssh connection - * @param {object} options - */ -export const testConnectionSsh = (options, ws) => { - return (new TerminalSsh(options, ws, true)) - .init() - .then(() => true) - .catch((err) => { - console.log('test ssh error', err) - return false - }) -} - -export const terminal = terminalSsh -export const testConnection = testConnectionSsh diff --git a/src/app/server/session-telnet.js b/src/app/server/session-telnet.js deleted file mode 100644 index 7e3a08a..0000000 --- a/src/app/server/session-telnet.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * terminal/sftp/serial class - */ -import _ from 'lodash' -import log from '../common/log.js' -import { Telnet } from './telnet.js' -import { TerminalBase } from './session-base.js' -import globalState from './global-state.js' -import iconv from 'iconv-lite' - -// Encodings that are equivalent to UTF-8 (no conversion needed) -const utf8Aliases = new Set(['utf-8', 'utf8', 'utf-8-strict']) - -// Helper function to convert regex string to RegExp object -function stringToRegExp (regexString) { - // Check if it's already a RegExp - if (regexString instanceof RegExp) { - return regexString - } - - // Parse string format like /pattern/flags - const match = regexString.match(/^\/(.+)\/([gimsuy]*)$/) - if (match) { - const [, pattern, flags] = match - return new RegExp(pattern, flags) - } - - // If no slashes, treat as plain pattern - return new RegExp(regexString) -} - -class TerminalTelnet extends TerminalBase { - async init () { - const connection = new Telnet() - - const { initOptions } = this - const shellOpts = { - highWaterMark: 64 * 1024 * 16 - } - const params = _.pick( - initOptions, - [ - 'host', - 'port', - 'timeout', - 'username', - 'password', - 'terminalWidth', - 'terminalHeight', - 'proxy' - ] - ) - // Convert string regex patterns to RegExp objects - if (typeof initOptions.loginPrompt === 'string') { - params.loginPrompt = stringToRegExp(initOptions.loginPrompt) - } - if (typeof initOptions.passwordPrompt === 'string') { - params.passwordPrompt = stringToRegExp(initOptions.passwordPrompt) - } - Object.assign( - params, - { - negotiationMandatory: false, - // terminalWidth: initOptions.cols, - // terminalHeight: initOptions.rows, - timeout: initOptions.readyTimeout, - sendTimeout: initOptions.readyTimeout, - socketConnectOptions: shellOpts - } - ) - await connection.connect(params) - this.port = connection.shell(shellOpts) - this.channel = connection - if (this.isTest) { - this.kill() - return true - } - globalState.setSession(this.pid, this) - } - - resize (cols, rows) { - Object.assign(this.channel.options, { - terminalWidth: cols, - terminalHeight: rows - }) - this.channel.sendWindowSize() - } - - on (event, cb) { - this.port.on(event, cb) - } - - write (data) { - try { - const encode = this.initOptions?.encode - if (encode && !utf8Aliases.has(encode.toLowerCase()) && typeof data === 'string') { - try { - const buf = iconv.encode(data, encode) - this.port.write(buf) - if (this.sessionLogger) { - this.sessionLogger.write(data) - } - return - } catch (e) { - log.warn('iconv encode failed, falling back to raw write:', e.message) - } - } - this.port.write(data) - if (this.sessionLogger) { - this.sessionLogger.write(data) - } - } catch (e) { - log.error(e) - } - } - - kill = () => { - this.channel && this.channel.end() - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - globalState.removeSession(this.pid) - } -} - -export const terminalTelnet = async function (initOptions, ws) { - const term = new TerminalTelnet(initOptions, ws) - await term.init() - return term -} - -/** - * test ssh connection - * @param {object} options - */ -export const testConnectionTelnet = (options) => { - return (new TerminalTelnet(options, undefined, true)) - .init() - .then(() => true) - .catch(() => { - return false - }) -} - -export const terminal = terminalTelnet -export const testConnection = testConnectionTelnet diff --git a/src/app/server/session-vnc.js b/src/app/server/session-vnc.js deleted file mode 100644 index 126a270..0000000 --- a/src/app/server/session-vnc.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * terminal/sftp/serial class - */ - -import log from '../common/log.js' -import { TerminalBase } from './session-base.js' -import net from 'net' -import proxySock from './socks.js' -import { createHopProxy } from './session-hop.js' -import globalState from './global-state.js' - -class TerminalVnc extends TerminalBase { - init = async () => { - globalState.setSession(this.pid, this) - return Promise.resolve(this) - } - - start = async (width, height) => { - if (this.isRunning) { - return - } - this.isRunning = true - if (this.channel) { - this.channel.close() - delete this.channel - } - const { - host, - port - } = this.initOptions - const info = await this.hop() - const target = net.createConnection({ - port, - host, - ...info - }) - this.channel = target - target.on('data', this.onData) - target.on('end', this.kill) - target.on('error', this.onError) - - this.ws.on('message', this.onMsg) - this.ws.on('close', this.kill) - this.width = width - this.height = height - } - - hop = async () => { - const { - host, - port, - readyTimeout - } = this.initOptions - const { proxyUrl, ssh } = await createHopProxy(this.initOptions) - if (ssh) { - this.ssh = ssh - } - return proxyUrl - ? proxySock({ readyTimeout, host, port, proxy: proxyUrl }) - : undefined - } - - onMsg = (msg) => { - this.channel.write(msg) - } - - onData = (data) => { - try { - this.ws?.send(data) - } catch (e) { - log.error('vnc connection send data error', e) - } - } - - resize () { - - } - - onError = (err) => { - log.error('vnc error', err) - this.kill() - } - - test = async () => { - return new Promise((resolve, reject) => { - const { - host, - port - } = this.initOptions - return this.hop() - .then(info => { - net.createConnection({ - port, - host, - ...info - }, () => { - resolve(true) - }) - }) - .catch(err => reject(err)) - }) - } - - kill = () => { - log.debug('Closed vnc session ' + this.pid) - if (this.ws) { - this.ws.close() - delete this.ws - } - if (this.ssh) { - this.ssh.kill() - delete this.ssh - } - this.channel && this.channel.end() - if (this.sessionLogger) { - this.sessionLogger.destroy() - } - globalState.removeSession(this.pid) - } -} - -export const terminalVnc = async function (initOptions, ws) { - const term = new TerminalVnc(initOptions, ws) - await term.init() - return term -} - -/** - * test ssh connection - * @param {object} options - */ -export const testConnectionVnc = (options) => { - const inst = new TerminalVnc(options, undefined, true) - return inst.test() - .then(() => { - inst.kill() - return true - }) - .catch(() => { - inst.kill() - return false - }) -} - -export const terminal = terminalVnc -export const testConnection = testConnectionVnc diff --git a/src/app/server/session.js b/src/app/server/session.js deleted file mode 100644 index 130787b..0000000 --- a/src/app/server/session.js +++ /dev/null @@ -1,48 +0,0 @@ -// Static imports so bundlers (esbuild) can discover and include all session -// modules. Dynamic import() with a computed path (e.g. `./session-${type}.js`) -// is opaque to bundlers — the files are never included in the bundle and the -// import fails at runtime. A plain dispatch table is the standard fix. -import * as sessionSsh from './session-ssh.js' -import * as sessionTelnet from './session-telnet.js' -import * as sessionSerial from './session-serial.js' -import * as sessionLocal from './session-local.js' -import * as sessionRdp from './session-rdp.js' -import * as sessionVnc from './session-vnc.js' -import * as sessionSpice from './session-spice.js' - -const sessionModules = { - ssh: sessionSsh, - telnet: sessionTelnet, - serial: sessionSerial, - local: sessionLocal, - rdp: sessionRdp, - vnc: sessionVnc, - spice: sessionSpice -} - -function getType (initOptions) { - const type = initOptions.termType || initOptions.type - const tail = [ - 'telnet', - 'serial', - 'local', - 'rdp', - 'vnc', - 'spice' - ].includes(type) - ? type - : 'ssh' - return tail -} - -export const terminal = async function (initOptions, ws) { - const type = getType(initOptions) - const { terminal } = sessionModules[type] - return terminal(initOptions, ws) -} - -export const testConnection = async (initOptions, ws) => { - const type = getType(initOptions) - const { testConnection } = sessionModules[type] - return testConnection(initOptions, ws) -} diff --git a/src/app/server/sftp-file.js b/src/app/server/sftp-file.js deleted file mode 100644 index f56d8a7..0000000 --- a/src/app/server/sftp-file.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * sftp read/write file - */ - -import { Readable, Writable } from 'stream' - -function createReadStreamFromString (str) { - const s = new Readable() - s._read = () => {} - s.push(str) - s.push(null) - return s -} - -class FakeWrite extends Writable { - constructor (opts) { - super(opts) - this.opts = opts - } - - _write (data, encoding, done) { - this.opts.onData(data) - done() - } -} - -export function writeRemoteFile (sftp, path, str, mode) { - return new Promise((resolve, reject) => { - const writeStream = sftp.createWriteStream(path, { - highWaterMark: 64 * 1024 * 4 * 4, - mode - }) - writeStream.on('close', () => { - resolve('ok') - }) - writeStream.on('error', (e) => { - reject(e) - }) - createReadStreamFromString(str).pipe(writeStream) - }) -} - -export function readRemoteFile (sftp, path) { - return new Promise((resolve, reject) => { - let final = Buffer.alloc(0) - const writeStream = new FakeWrite({ - onData: data => { - final = Buffer.concat( - [final, data] - ) - } - }) - writeStream.on('finish', () => { - resolve(final.toString()) - }) - writeStream.on('error', (e) => { - reject(e) - }) - sftp.createReadStream(path, { - highWaterMark: 64 * 1024 * 4 * 4 - }).pipe(writeStream) - }) -} diff --git a/src/app/server/socks.js b/src/app/server/socks.js deleted file mode 100644 index 997140b..0000000 --- a/src/app/server/socks.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * socks proxy wrapper - */ -import { SocksClient } from 'socks' -import { request } from 'http' - -function isValidIP (input) { - // Check IPv4 format - const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ - if (ipv4Pattern.test(input)) { - return true - } - - // Check IPv6 format - const ipv6Pattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i - if (ipv6Pattern.test(input)) { - return true - } - - // If input doesn't match IPv4 or IPv6 patterns, it's not a valid IP - return false -} - -function parseUrl (str) { - try { - return new URL(str) - } catch (e) { - console.log(`parse url error: ${e.message}, url: ${str}`) - } -} - -export default (initOptions) => { - const { - readyTimeout, - host, - port, - proxy - } = initOptions - const proxyURL = parseUrl(proxy) - if (!proxyURL) { - throw new Error('proxy format not right:', proxy) - } - // use http proxy - const { - protocol, - hostname, - username, - password - } = proxyURL - const proxyPort = Number(proxyURL.port) - const proxyHost = proxyURL.host - if (protocol === 'http:' || protocol === 'https:') { - return new Promise((resolve, reject) => { - const opts = { - agent: false, - protocol, - hostname, - port: proxyPort, - host: proxyHost, - path: `${host}:${port}`, - method: 'CONNECT', - timeout: readyTimeout, - headers: {} - } - if (username) { - const auth = Buffer.from(`${username}:${password}`).toString('base64') - opts.headers['Proxy-Authorization'] = `Basic ${auth}` - } - request(opts) - .on('error', (e) => { - console.error(`fail to connect proxy: ${e.message}`) - reject(e) - }) - .on('connect', (res, socket) => { - resolve({ socket }) - }) - .end() - }) - } - const type = protocol.includes('5') ? 5 : 4 - const isIp = isValidIP(hostname) - const options = { - proxy: { - port: proxyPort, - type, - userId: username, - password - }, - - command: 'connect', - timeout: readyTimeout, - - destination: { - host, - port - } - } - if (isIp) { - options.proxy.ipaddress = hostname - } else { - options.proxy.host = hostname - } - - // use socks proxy - return SocksClient.createConnection(options) -} diff --git a/src/app/server/spice-proxy.js b/src/app/server/spice-proxy.js deleted file mode 100644 index 718a6ac..0000000 --- a/src/app/server/spice-proxy.js +++ /dev/null @@ -1,218 +0,0 @@ -import net from 'net' -import log from '../common/log.js' -import proxySock from './socks.js' - -const LOG_PREFIX = '[SPICE-PROXY]' - -async function createTcpConnection (host, port, options = {}) { - const { proxy, readyTimeout = 15000 } = options - - if (proxy) { - log.debug(`${LOG_PREFIX} Connecting through proxy: ${proxy}`) - const proxyResult = await proxySock({ - readyTimeout, - host, - port, - proxy - }) - log.debug(`${LOG_PREFIX} Proxy connection established`) - return proxyResult.socket - } - - return new Promise((resolve, reject) => { - const tcpSocket = net.createConnection({ host, port }, () => { - log.debug(`${LOG_PREFIX} TCP connection established to ${host}:${port}`) - tcpSocket.setKeepAlive(true, 5000) - tcpSocket.setTimeout(0) - resolve(tcpSocket) - }) - tcpSocket.once('error', (err) => { - reject(new Error(`TCP connection failed: ${err.message}`)) - }) - tcpSocket.setTimeout(readyTimeout, () => { - tcpSocket.destroy() - reject(new Error('Connection timed out')) - }) - }) -} - -async function handleConnection (ws, options = {}) { - const { host, port, proxy, readyTimeout = 15000, onCleanup, channelId } = options - const id = channelId || 'unknown' - - log.debug(`${LOG_PREFIX}[${id}] New WebSocket connection for SPICE proxy`) - - if (!host || !port) { - log.error(`${LOG_PREFIX}[${id}] Missing host or port`) - ws.close() - if (onCleanup) onCleanup() - return - } - - const messageBuffer = [] - let wsClosed = false - let tcpClosed = false - let tcpSocket = null - - const cleanup = (source) => { - if (wsClosed && tcpClosed) return - log.debug(`${LOG_PREFIX}[${id}] Cleanup triggered by: ${source}`) - wsClosed = true - tcpClosed = true - - try { - if (ws && ws.readyState !== ws.CLOSED) { - ws.close() - } - } catch (e) { - log.debug(`${LOG_PREFIX}[${id}] WebSocket close error:`, e.message) - } - - try { - if (tcpSocket) { - tcpSocket.destroy() - } - } catch (e) { - log.debug(`${LOG_PREFIX}[${id}] TCP socket destroy error:`, e.message) - } - - if (onCleanup) { - onCleanup() - } - } - - ws.on('message', (data) => { - if (tcpClosed) return - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - - if (tcpSocket) { - try { - tcpSocket.write(buf) - } catch (e) { - log.error(`${LOG_PREFIX}[${id}] TCP write error:`, e.message) - cleanup('TCP write error') - } - } else { - messageBuffer.push(buf) - } - }) - - ws.on('close', () => cleanup('WebSocket')) - ws.on('error', (err) => { - log.error(`${LOG_PREFIX}[${id}] WebSocket error:`, err.message) - cleanup('WebSocket error') - }) - - try { - tcpSocket = await createTcpConnection(host, port, { proxy, readyTimeout }) - log.debug(`${LOG_PREFIX}[${id}] Connected to SPICE server at ${host}:${port}`) - - tcpSocket.on('data', (data) => { - if (wsClosed) return - try { - ws.send(data) - } catch (e) { - log.error(`${LOG_PREFIX}[${id}] WebSocket send error:`, e.message) - cleanup('WebSocket send error') - } - }) - - tcpSocket.on('close', () => cleanup('TCP close')) - tcpSocket.on('end', () => cleanup('TCP end')) - tcpSocket.on('error', (err) => { - log.error(`${LOG_PREFIX}[${id}] TCP error:`, err.message) - cleanup('TCP error') - }) - - if (messageBuffer.length > 0) { - for (const buf of messageBuffer) { - try { - tcpSocket.write(buf) - } catch (e) { - log.error(`${LOG_PREFIX}[${id}] TCP write error:`, e.message) - cleanup('TCP write error') - return - } - } - messageBuffer.length = 0 - } - } catch (err) { - log.error(`${LOG_PREFIX}[${id}] Connection failed:`, err.message) - try { - ws.close() - } catch (e) {} - if (onCleanup) onCleanup() - } -} - -function setupRelay (ws, tcpSocket, options = {}) { - const { onCleanup, channelId } = options - let wsClosed = false - let tcpClosed = false - const id = channelId || 'unknown' - - const cleanup = (source) => { - if (wsClosed && tcpClosed) return - log.debug(`${LOG_PREFIX}[${id}] Cleanup triggered by: ${source}`) - wsClosed = true - tcpClosed = true - - try { - if (ws && ws.readyState !== ws.CLOSED) { - ws.close() - } - } catch (e) { - log.debug(`${LOG_PREFIX}[${id}] WebSocket close error:`, e.message) - } - - try { - tcpSocket.destroy() - } catch (e) { - log.debug(`${LOG_PREFIX}[${id}] TCP socket destroy error:`, e.message) - } - - if (onCleanup) { - onCleanup() - } - } - - tcpSocket.on('data', (data) => { - if (wsClosed) return - try { - ws.send(data) - } catch (e) { - log.error(`${LOG_PREFIX}[${id}] WebSocket send error:`, e.message) - cleanup('WebSocket send error') - } - }) - - tcpSocket.on('close', () => cleanup('TCP close')) - tcpSocket.on('end', () => cleanup('TCP end')) - tcpSocket.on('error', (err) => { - log.error(`${LOG_PREFIX}[${id}] TCP error:`, err.message) - cleanup('TCP error') - }) - - ws.on('message', (data) => { - if (tcpClosed) return - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - try { - tcpSocket.write(buf) - } catch (e) { - log.error(`${LOG_PREFIX}[${id}] TCP write error:`, e.message) - cleanup('TCP write error') - } - }) - - ws.on('close', () => cleanup('WebSocket')) - ws.on('error', (err) => { - log.error(`${LOG_PREFIX}[${id}] WebSocket error:`, err.message) - cleanup('WebSocket error') - }) -} - -export { - handleConnection, - createTcpConnection, - setupRelay -} diff --git a/src/app/server/ssh-known-hosts.js b/src/app/server/ssh-known-hosts.js deleted file mode 100644 index ce362a3..0000000 --- a/src/app/server/ssh-known-hosts.js +++ /dev/null @@ -1,453 +0,0 @@ -import crypto from 'crypto' -import fs from 'fs' -import os from 'os' -import { dirname, join } from 'path' -import keyParserModule from '@electerm/ssh2/lib/protocol/keyParser.js' -const { parseKey } = keyParserModule - -function normalizeHost (host = '') { - if (typeof host !== 'string') { - return '' - } - if (host.startsWith('[') && host.endsWith(']')) { - return host.slice(1, -1) - } - return host -} - -function getKnownHostsPath () { - return join(os.homedir(), '.ssh', 'known_hosts') -} - -function getKnownHostCandidates (host, port) { - const normalizedHost = normalizeHost(host) - const normalizedPort = Number(port) || 22 - const candidates = new Set([normalizedHost]) - candidates.add(`[${normalizedHost}]:${normalizedPort}`) - return [...candidates].filter(Boolean) -} - -function escapeRegExp (value) { - return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&') -} - -function wildcardToRegExp (value) { - const pattern = escapeRegExp(value) - .replace(/\\\*/g, '.*') - .replace(/\\\?/g, '.') - return new RegExp(`^${pattern}$`) -} - -function matchesHashedHost (entry, candidate) { - const parts = entry.split('|') - if (parts.length !== 4 || parts[1] !== '1') { - return false - } - try { - const salt = Buffer.from(parts[2], 'base64') - const hash = Buffer.from(parts[3], 'base64') - const digest = crypto - .createHmac('sha1', salt) - .update(candidate) - .digest() - return digest.equals(hash) - } catch { - return false - } -} - -function matchesHostToken (token, candidates) { - if (!token) { - return false - } - if (token.startsWith('|1|')) { - return candidates.some(candidate => matchesHashedHost(token, candidate)) - } - const matcher = token.includes('*') || token.includes('?') - ? wildcardToRegExp(token) - : null - return candidates.some(candidate => { - if (matcher) { - return matcher.test(candidate) - } - return token === candidate - }) -} - -function matchesKnownHostField (hostField, host, port) { - const candidates = getKnownHostCandidates(host, port) - const tokens = hostField.split(',').map(token => token.trim()).filter(Boolean) - let matched = false - for (const token of tokens) { - const isNegative = token.startsWith('!') - const cleanToken = isNegative ? token.slice(1) : token - if (!matchesHostToken(cleanToken, candidates)) { - continue - } - if (isNegative) { - return false - } - matched = true - } - return matched -} - -function parseKnownHostsLine (line) { - const trimmed = line.trim() - if (!trimmed || trimmed.startsWith('#')) { - return null - } - const parts = trimmed.split(/\s+/) - if (parts.length < 3) { - return null - } - let marker - if (parts[0].startsWith('@')) { - if (parts.length < 4) { - return null - } - marker = parts.shift() - } - const [hosts, keyType, keyData] = parts - if (!hosts || !keyType || !keyData) { - return null - } - return { - marker, - hosts, - keyType, - keyData - } -} - -function getHostKeyMeta (hostKey) { - const parsed = parseKey(hostKey) - if (parsed instanceof Error) { - throw parsed - } - return { - keyType: parsed.type, - keyData: parsed.getPublicSSH().toString('base64'), - sha256: crypto.createHash('sha256').update(hostKey).digest('base64') - } -} - -function formatSha256Fingerprint (sha256) { - return `SHA256:${sha256}` -} - -async function readKnownHostsFile (knownHostsPath = getKnownHostsPath()) { - try { - return await fs.promises.readFile(knownHostsPath, 'utf8') - } catch (err) { - if (err && err.code === 'ENOENT') { - return '' - } - throw err - } -} - -async function checkKnownHosts (options) { - const { - host, - port, - hostKey, - knownHostsPath = getKnownHostsPath() - } = options - const knownHosts = await readKnownHostsFile(knownHostsPath) - const meta = getHostKeyMeta(hostKey) - const lines = knownHosts.split(/\r?\n/) - const matchingEntries = [] - for (const line of lines) { - const entry = parseKnownHostsLine(line) - if (!entry) { - continue - } - if (!matchesKnownHostField(entry.hosts, host, port)) { - continue - } - matchingEntries.push(entry) - } - const sameTypeEntries = matchingEntries.filter(entry => entry.keyType === meta.keyType) - const exactMatch = sameTypeEntries.find(entry => entry.keyData === meta.keyData) - if (exactMatch) { - if (exactMatch.marker === '@revoked') { - return { - status: 'revoked', - meta, - knownHostsPath - } - } - return { - status: 'match', - meta, - knownHostsPath - } - } - if (sameTypeEntries.length) { - return { - status: 'mismatch', - meta, - knownHostsPath, - entries: sameTypeEntries - } - } - return { - status: 'not-found', - meta, - knownHostsPath, - entries: matchingEntries - } -} - -async function appendKnownHost (options) { - const { - host, - port, - hostKey, - knownHostsPath = getKnownHostsPath() - } = options - const meta = getHostKeyMeta(hostKey) - await fs.promises.mkdir(dirname(knownHostsPath), { - recursive: true, - mode: 0o700 - }) - const hostToken = Number(port) && Number(port) !== 22 - ? `[${normalizeHost(host)}]:${Number(port)}` - : normalizeHost(host) - const prefix = await readKnownHostsFile(knownHostsPath) - const needsNewline = prefix && !prefix.endsWith('\n') - const line = `${hostToken} ${meta.keyType} ${meta.keyData}\n` - await fs.promises.appendFile(knownHostsPath, `${needsNewline ? '\n' : ''}${line}`, { - mode: 0o600 - }) - return meta -} - -async function removeKnownHost (options) { - const { - host, - port, - keyType, - knownHostsPath = getKnownHostsPath() - } = options - const content = await readKnownHostsFile(knownHostsPath) - if (!content) { - return - } - const lines = content.split(/\r?\n/) - const filtered = lines.filter(line => { - const entry = parseKnownHostsLine(line) - if (!entry) { - return true - } - if (!matchesKnownHostField(entry.hosts, host, port)) { - return true - } - if (entry.keyType !== keyType) { - return true - } - return false - }) - await fs.promises.writeFile(knownHostsPath, filtered.join('\n'), { - mode: 0o600 - }) -} - -async function replaceKnownHost (options) { - const { - host, - port, - hostKey, - knownHostsPath = getKnownHostsPath() - } = options - const meta = getHostKeyMeta(hostKey) - await removeKnownHost({ - host, - port, - keyType: meta.keyType, - knownHostsPath - }) - return appendKnownHost({ - host, - port, - hostKey, - knownHostsPath - }) -} - -function buildUnknownHostPrompt (options) { - const { - host, - port, - meta, - knownHostsPath = getKnownHostsPath() - } = options - const target = Number(port) && Number(port) !== 22 - ? `[${normalizeHost(host)}]:${Number(port)}` - : normalizeHost(host) - return { - mode: 'confirm', - name: `Trust SSH host key for ${target}?`, - instructions: [ - `The authenticity of host '${target}' can't be established.`, - `Key type: ${meta.keyType}`, - `Fingerprint: ${formatSha256Fingerprint(meta.sha256)}`, - `Known hosts file: ${knownHostsPath}`, - 'Trust this host key and add it to known_hosts?' - ], - prompts: [], - submitText: 'Trust and Save', - cancelText: 'Reject', - confirmResult: 'trust' - } -} - -function buildHostMismatchError (options) { - const { - host, - port, - meta, - knownHostsPath = getKnownHostsPath() - } = options - const target = Number(port) && Number(port) !== 22 - ? `[${normalizeHost(host)}]:${Number(port)}` - : normalizeHost(host) - return new Error( - [ - `SSH host key verification failed for ${target}.`, - `Presented ${meta.keyType} fingerprint ${formatSha256Fingerprint(meta.sha256)} does not match ${knownHostsPath}.`, - 'Remove the old known_hosts entry if you trust the new host key.' - ].join(' ') - ) -} - -function buildHostMismatchPrompt (options) { - const { - host, - port, - meta, - knownHostsPath = getKnownHostsPath() - } = options - const target = Number(port) && Number(port) !== 22 - ? `[${normalizeHost(host)}]:${Number(port)}` - : normalizeHost(host) - return { - mode: 'confirm', - name: `SSH host key changed for ${target}`, - instructions: [ - 'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!', - `The host key for '${target}' has changed.`, - `New key type: ${meta.keyType}`, - `New fingerprint: ${formatSha256Fingerprint(meta.sha256)}`, - `Known hosts file: ${knownHostsPath}`, - 'This could indicate a man-in-the-middle attack, or the remote host was reinstalled (e.g. router reboot).', - 'Update the known_hosts entry with the new key?' - ], - prompts: [], - submitText: 'Update Key', - cancelText: 'Reject', - confirmResult: 'trust' - } -} - -function createHostVerifier (options) { - const { - host, - port, - knownHostsPath = getKnownHostsPath(), - confirm, - onError - } = options - return (hostKey, verify) => { - checkKnownHosts({ - host, - port, - hostKey, - knownHostsPath - }) - .then(async (result) => { - if (result.status === 'match') { - verify(true) - return - } - if (result.status === 'revoked') { - onError && onError(buildHostMismatchError({ - host, - port, - meta: result.meta, - knownHostsPath - })) - verify(false) - return - } - if (result.status === 'mismatch') { - const accepted = await confirm(buildHostMismatchPrompt({ - host, - port, - meta: result.meta, - knownHostsPath - })) - if (!accepted) { - onError && onError(buildHostMismatchError({ - host, - port, - meta: result.meta, - knownHostsPath - })) - verify(false) - return - } - await replaceKnownHost({ - host, - port, - hostKey, - knownHostsPath - }) - verify(true) - return - } - const accepted = await confirm(buildUnknownHostPrompt({ - host, - port, - meta: result.meta, - knownHostsPath - })) - if (!accepted) { - onError && onError(new Error('SSH host key verification was canceled by the user.')) - verify(false) - return - } - await appendKnownHost({ - host, - port, - hostKey, - knownHostsPath - }) - verify(true) - }) - .catch((err) => { - onError && onError(err) - verify(false) - }) - } -} - -export { - appendKnownHost, - buildHostMismatchError, - buildHostMismatchPrompt, - buildUnknownHostPrompt, - checkKnownHosts, - createHostVerifier, - formatSha256Fingerprint, - getHostKeyMeta, - getKnownHostCandidates, - getKnownHostsPath, - matchesHashedHost, - matchesKnownHostField, - normalizeHost, - parseKnownHostsLine, - removeKnownHost, - replaceKnownHost -} diff --git a/src/app/server/ssh-proxy-command.js b/src/app/server/ssh-proxy-command.js deleted file mode 100644 index df6b2b3..0000000 --- a/src/app/server/ssh-proxy-command.js +++ /dev/null @@ -1,283 +0,0 @@ -/** - * ssh proxy command support - * - * Connects through an external stdio proxy command (like OpenSSH ProxyCommand): - * the command is expected to speak the SSH protocol on its stdin/stdout. - * - * Used for: - * - netbird ssh proxy (auto-detected via `netbird ssh detect`, see - * https://github.com/electerm/electerm/issues/4500) - * - generic user-defined proxyCommand option (supports %h %p %r placeholders, - * e.g. `cloudflared access ssh --hostname %h`) - * - * Because @electerm/ssh2 requires a real net.Socket with full semantics - * (setKeepAlive/destroy/connecting), we bridge the child stdio through a - * loopback socketpair instead of patching a fake socket. - */ - -import { spawn } from 'child_process' -import net from 'net' -import log from '../common/log.js' - -// resolved lazily so tests (and users) can override via env at any time -function getNetbirdBin () { - return process.env.ELECTERM_NETBIRD_BIN || 'netbird' -} - -// how long to wait for `netbird ssh detect` before giving up and connecting directly -const detectTimeout = 5 * 1000 - -// netbird CGNAT range 100.64.0.0/10 -function isNetbirdLikeHost (host) { - if (typeof host !== 'string' || !host) { - return false - } - const h = host.startsWith('[') && host.endsWith(']') - ? host.slice(1, -1) - : host - const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) - if (!m) { - // netbird also registers dns names, but avoid spawning a process - // for every arbitrary hostname connection - return false - } - const a = Number(m[1]) - const b = Number(m[2]) - return a === 100 && b >= 64 && b <= 127 -} - -/** - * detect if target is a netbird JWT ssh server - * `netbird ssh detect` exits 0 when the server requires netbird JWT auth - */ -function detectNetbird (host, port) { - return new Promise((resolve) => { - let child - try { - child = spawn(getNetbirdBin(), ['ssh', 'detect', host, String(port)], { - stdio: 'ignore' - }) - } catch (e) { - log.warn('spawn netbird detect failed', e.message) - return resolve(false) - } - const timer = setTimeout(() => { - child.kill() - resolve(false) - }, detectTimeout) - child.on('error', () => { - clearTimeout(timer) - resolve(false) - }) - child.on('close', (code) => { - clearTimeout(timer) - resolve(code === 0) - }) - }) -} - -// cache detection result per host:port for the process lifetime, -// so reconnects do not spawn `netbird ssh detect` again -const detectCache = new Map() - -/** - * build proxy command command/args from template string with %h %p %r placeholders - */ -function expandProxyCommand (command, { host, port, username }) { - const expanded = command - .replace(/%h/g, host) - .replace(/%p/g, String(port)) - .replace(/%r/g, username || '') - return expanded.trim().split(/\s+/).filter(Boolean) -} - -/** - * create a loopback socketpair bridging to child stdio: - * ssh2 client connects a normal tcp socket to 127.0.0.1:, - * the listener pipes both directions to the child process - */ -function bridgeChildStdio (child) { - return new Promise((resolve, reject) => { - const server = net.createServer() - let settled = false - server.once('error', (err) => { - if (settled) { - return - } - settled = true - reject(err) - }) - server.listen(0, '127.0.0.1', () => { - if (settled) { - return - } - settled = true - const { port } = server.address() - resolve({ server, port }) - }) - server.once('connection', (socket) => { - server.close() - socket.pipe(child.stdin) - child.stdout.pipe(socket) - const cleanup = () => { - socket.destroy() - try { - child.kill() - } catch { - // ignore - } - } - child.stdout.once('end', cleanup) - child.stdout.once('error', cleanup) - child.once('exit', () => socket.destroy()) - socket.once('error', () => { - log.log('proxy command bridge socket error') - try { - child.kill() - } catch { - // ignore - } - }) - }) - }) -} - -/** - * spawn proxy command and resolve { socket, dispose } - * socket is an ordinary net.Socket connected to the bridge - */ -async function runProxyCommand (command, args, { onMessage } = {}) { - const child = spawn(command, args, { - stdio: ['pipe', 'pipe', 'pipe'], - windowsHide: true - }) - const stderrBuf = [] - child.stderr?.on('data', (d) => { - const text = d.toString() - stderrBuf.push(text) - onMessage && onMessage(text) - }) - const [bridge, socket] = await new Promise((resolve, reject) => { - let settled = false - const onSpawnError = (err) => { - if (settled) { - return - } - settled = true - reject(new Error(`proxy command failed to start: ${command}: ${err.message}`)) - } - child.once('error', onSpawnError) - bridgeChildStdio(child).then( - ({ server, port }) => { - if (settled) { - return - } - settled = true - child.removeListener('error', onSpawnError) - const socket = net.connect(port, '127.0.0.1') - socket.once('connect', () => resolve([server, socket])) - socket.once('error', (err) => { - reject(err) - }) - }, - (err) => { - if (settled) { - return - } - settled = true - reject(err) - } - ) - }) - let disposed = false - const dispose = () => { - if (disposed) { - return - } - disposed = true - socket.destroy() - try { - child.stdin?.end() - } catch { - // ignore - } - child.kill() - bridge.close() - log.log('proxy command disposed:', command, args.join(' ')) - } - child.once('exit', () => { - socket.destroy() - }) - socket.once('close', () => { - if (!disposed) { - disposed = true - try { - child.stdin?.end() - } catch { - // ignore - } - child.kill() - bridge.close() - } - }) - return { - socket, - dispose, - stderr: () => stderrBuf.join('') - } -} - -/** - * main entry: decide whether to connect through a proxy command - * returns { socket, dispose, stderr } or null when not applicable - */ -async function maybeProxyCommand (initOptions, connectOptions, { onMessage } = {}) { - const host = connectOptions.host || initOptions.host - const port = connectOptions.port || initOptions.port || 22 - let command - let args - if (initOptions.proxyCommand) { - const expanded = expandProxyCommand(initOptions.proxyCommand, { - host, - port, - username: connectOptions.username || initOptions.username - }) - if (!expanded.length) { - return null - } - command = expanded.shift() - args = expanded - } else if ( - !initOptions.proxy && - !connectOptions.sock && - isNetbirdLikeHost(host) - ) { - const key = `${host}:${port}` - let detected = detectCache.get(key) - if (detected === undefined) { - detected = await detectNetbird(host, port) - detectCache.set(key, detected) - } - if (!detected) { - return null - } - command = getNetbirdBin() - args = ['ssh', 'proxy', host, String(port)] - } else { - return null - } - log.log('using ssh proxy command:', command, args.join(' ')) - return runProxyCommand(command, args, { onMessage }) -} - -function clearDetectCache () { - detectCache.clear() -} - -export { - maybeProxyCommand, - expandProxyCommand, - detectNetbird, - isNetbirdLikeHost, - clearDetectCache -} diff --git a/src/app/server/ssh-tunnel.js b/src/app/server/ssh-tunnel.js deleted file mode 100644 index d2978c2..0000000 --- a/src/app/server/ssh-tunnel.js +++ /dev/null @@ -1,204 +0,0 @@ -import log from '../common/log.js' -import * as socks from 'socksv5-server' -import net from 'net' - -export function forwardRemoteToLocal ({ - conn, - sshTunnelRemotePort, - sshTunnelLocalPort, - sshTunnelRemoteHost = '127.0.0.1', - sshTunnelLocalHost = '127.0.0.1' -}) { - return new Promise((resolve, reject) => { - const result = `remote:${sshTunnelRemoteHost}:${sshTunnelRemotePort} => local:${sshTunnelLocalHost}:${sshTunnelLocalPort}` - - const handleTcpConnection = (info, accept, rejectConn) => { - // Check if this connection is for this tunnel - if (info.destPort !== sshTunnelRemotePort && info.destPort !== Number(sshTunnelRemotePort)) { - return - } - - const srcStream = accept() // Source stream for forwarding - - if (!srcStream) { - log.error(`Failed to accept connection for tunnel ${result}`) - return - } - - // Add error handling for source stream immediately - srcStream.on('error', (err) => { - log.error(`Source stream error for tunnel ${result}:`, err) - }) - - // Connect the local machine source stream to the local port - // Create a NEW server connection for each forwarded connection - const server = net.connect(sshTunnelLocalPort, sshTunnelLocalHost) - - // CRITICAL: Add error handling IMMEDIATELY before any async operations - // This prevents unhandled errors from crashing the SSH session - server.on('error', (err) => { - log.error(`Server connection error for tunnel ${result}:`, err.message) - // Just close this specific connection, don't break the tunnel - srcStream.destroy() - server.destroy() - }) - - server.on('close', () => { - log.log(`Local server connection closed for tunnel ${result}`) - srcStream.end() - }) - - srcStream.on('close', () => { - server.destroy() - }) - - srcStream.pipe(server).pipe(srcStream) - } - - conn.on('tcp connection', handleTcpConnection) - - const handleClose = () => { - log.log(`SSH connection closed for tunnel ${result}`) - conn.removeListener('tcp connection', handleTcpConnection) - conn.removeListener('close', handleClose) - } - - conn.on('close', handleClose) - - // Forward the remote server's port to the local machine's port - conn.forwardIn(sshTunnelRemoteHost, sshTunnelRemotePort, (err) => { - if (err) { - log.error('Error forwarding port:', err) - return reject(err) - } - log.log(`Port forwarded: ${result}`) - resolve(1) - }) - }) -} - -export function forwardLocalToRemote ({ - conn, - sshTunnelRemotePort, - sshTunnelLocalPort, - sshTunnelRemoteHost = '127.0.0.1', - sshTunnelLocalHost = '127.0.0.1' -}) { - return new Promise((resolve, reject) => { - const activeSockets = new Set() - const localServer = net.createServer((socket) => { - // ⬇️ 2. Add new sockets to the set and remove them when they close - activeSockets.add(socket) - socket.on('close', () => { - activeSockets.delete(socket) - }) - - socket.on('error', (err) => { - log.error('Client socket error:', err) - socket.end() - }) - - conn.forwardOut(sshTunnelLocalHost, sshTunnelLocalPort, sshTunnelRemoteHost, sshTunnelRemotePort, (err, remoteSocket) => { - if (err) { - log.error('Error forwarding connection:', err) - socket.destroy() - // Don't reject - just close this connection - // Rejecting would break the entire tunnel - return - } - - // Add error handlers immediately - remoteSocket.on('error', (err) => { - log.error('Remote socket error:', err) - socket.destroy() - }) - - socket.on('close', () => { - remoteSocket.destroy() - }) - - socket.pipe(remoteSocket).pipe(socket) - }) - }) - - localServer.listen(sshTunnelLocalPort, sshTunnelLocalHost, () => { - log.log(`Local server listening on port ${sshTunnelLocalPort}`) - resolve(1) - }) - localServer.on('error', (err) => { - log.error('Error listening for local connections:', err) - reject(err) - }) - - conn.on('close', () => { - log.log('SSH connection closed, closing local server.') - // ⬇️ 3. Destroy all active sockets before closing the server - for (const socket of activeSockets) { - socket.destroy() - } - localServer && localServer.close() - }) - }) -} - -export function dynamicForward ({ - conn, - sshTunnelLocalPort, - sshTunnelLocalHost = '127.0.0.1' -}) { - return new Promise((resolve, reject) => { - const dproxyServer = socks.createServer((info, accept, deny) => { - conn.forwardOut( - info.srcAddr, - info.srcPort, - info.dstAddr, - info.dstPort, - (err, stream) => { - if (err) { - log.error('SOCKS forward error:', err) - deny() - // Don't reject - just deny this connection - // Rejecting would break the entire tunnel - return - } - const clientSocket = accept(true) - if (clientSocket) { - // Add error handling for stream immediately - stream.on('error', (err) => { - log.error('SOCKS stream error:', err) - clientSocket.destroy() - }) - - // Add error handling for client socket immediately - clientSocket.on('error', (err) => { - log.error('SOCKS client socket error:', err) - stream.destroy() - }) - - stream.on('close', () => { - clientSocket.destroy() - }) - - clientSocket.on('close', () => { - stream.destroy() - }) - - stream.pipe(clientSocket).pipe(stream) - } - }) - }) - dproxyServer.on('error', (err) => { - log.error('Error listening for local connections:', err) - reject(err) - }) - dproxyServer.listen(sshTunnelLocalPort, sshTunnelLocalHost, () => { - log.log(`SOCKS server listening on ${sshTunnelLocalHost}:${sshTunnelLocalPort}`) - resolve(1) - }).useAuth(socks.auth.None()) - - // close socks proxy when ssh connection is closed. - conn.on('close', () => { - dproxyServer && dproxyServer.close() - }) - }) -} diff --git a/src/app/server/ssh2-alg.js b/src/app/server/ssh2-alg.js deleted file mode 100644 index 8c2755b..0000000 --- a/src/app/server/ssh2-alg.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * all supported ssh2 algorithms config - */ - -import nodeCrypto from 'crypto' -import browserDH from 'diffie-hellman/browser.js' - -nodeCrypto.createDiffieHellmanGroup = browserDH.createDiffieHellmanGroup -nodeCrypto.createDiffieHellman = browserDH.createDiffieHellman -nodeCrypto.ddd = 1 - -export const algDefault = () => ({ - kex: [ - 'curve25519-sha256', // (node v13.9.0 or newer) - 'curve25519-sha256@libssh.org', // (node v13.9.0 or newer) - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group15-sha512', - 'diffie-hellman-group16-sha512', - 'diffie-hellman-group17-sha512', - 'diffie-hellman-group18-sha512', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group1-sha1' - ], - hmac: [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5', - 'hmac-sha2-256-96', - 'hmac-sha2-512-96', - 'hmac-ripemd160', - 'hmac-sha1-96', - 'hmac-md5-96', - 'hmac-sha2-256-etm@openssh.com', - 'hmac-sha2-512-etm@openssh.com', - 'hmac-sha1-etm@openssh.com' - ], - compress: [ - 'zlib@openssh.com', - 'zlib', - 'none' - ] -}) - -export const algAlt = () => ({ - ...exports.algDefault(), - cipher: [ - // 'chacha20-poly1305@openssh.com', - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm', - 'aes128-gcm@openssh.com', - 'aes256-gcm', - 'aes256-gcm@openssh.com', - 'aes256-cbc', - 'aes192-cbc', - 'aes128-cbc', - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'blowfish-cbc', - '3des-cbc', - 'arcfour256', - 'arcfour128', - // 'cast128-cbc', - 'arcfour' - ], - serverHostKey: [ - 'ssh-rsa', - 'ssh-ed25519', - 'ecdsa-sha2-nistp256', - 'ecdsa-sha2-nistp384', - 'ecdsa-sha2-nistp521', - 'ssh-dss', - 'rsa-sha2-512', - 'rsa-sha2-256' - ] -}) diff --git a/src/app/server/sync.js b/src/app/server/sync.js deleted file mode 100644 index 2289a99..0000000 --- a/src/app/server/sync.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * handle sync with github/gitee - */ - -import { - electermSync -} from 'electerm-sync' -import log from '../common/log.js' -import rp from 'axios' -import { createProxyAgent } from '../lib/proxy-agent.js' -import { doWebdavSync } from './webdav-sync.js' - -rp.defaults.proxy = false - -async function doSync (type, func, args, token, proxy) { - // Handle WebDAV sync separately - if (type === 'webdav') { - return doWebdavSync(func, args, token, proxy) - } - const agent = createProxyAgent(proxy) - const conf = agent - ? { - httpAgent: agent, - httpsAgent: agent - } - : { - proxy: false - } - const axiosInst = rp.create(conf) - if (type === 'cloud') { - args[0] = '' - } - return electermSync(axiosInst, type, func, args, token) - .then(r => { - return r - }) - .catch(e => { - log.error('sync error') - log.error(e.message) - return { - error: e - } - }) -} - -export default async function wsSyncHandler (ws, msg) { - const { id, type, args, func, token, proxy } = msg - const res = await doSync(type, func, args, token, proxy) - if (res.error) { - ws.s({ - error: { - message: 'sync error: ' + res.error.message - }, - id - }) - } else { - ws.s({ - data: res, - id - }) - } -} diff --git a/src/app/server/telnet.js b/src/app/server/telnet.js deleted file mode 100644 index 0bc3372..0000000 --- a/src/app/server/telnet.js +++ /dev/null @@ -1,367 +0,0 @@ -// used code from https://github.com/Eugeny/tabby/blob/master/tabby-telnet/src/session.ts and from https://github.com/mkozjak/node-telnet-client - -import { EventEmitter } from 'events' -import { Socket } from 'net' -import { Duplex } from 'stream' -import proxySock from './socks.js' - -const TelnetCommands = { - SUBOPTION_END: 240, - GA: 249, - SUBOPTION: 250, - WILL: 251, - WONT: 252, - DO: 253, - DONT: 254, - IAC: 255 -} - -const TelnetOptions = { - ECHO: 1, - SUPPRESS_GO_AHEAD: 3, - STATUS: 5, - TERMINAL_TYPE: 24, - NEGO_WINDOW_SIZE: 31, - NEGO_TERMINAL_SPEED: 32, - REMOTE_FLOW_CONTROL: 33, - X_DISPLAY_LOCATION: 35, - NEW_ENVIRON: 39 -} - -class Stream extends Duplex { - constructor (socket, options) { - super(options) - this.socket = socket - this.socket.on('data', data => this.push(data)) - } - - _write (data, encoding, callback) { - if (!this.socket.writable && callback) { - callback(new Error('socket not writable')) - return - } - this.socket.write(data, encoding, callback) - } - - _read () {} -} - -export class Telnet extends EventEmitter { - constructor (options = {}) { - super() - this.options = { - host: '127.0.0.1', - port: 23, - timeout: 5000, - negotiationMandatory: false, - username: '', - password: '', - terminalWidth: 80, - terminalHeight: 24, - loginPrompt: /login[: ]*$/i, - passwordPrompt: /password[: ]*$/i, - failedLoginMatch: /failed|incorrect|denied/i, - ...options - } - this.socket = null - this.telnetProtocol = false - this.state = 'init' - this.buffer = Buffer.alloc(0) - this.dataBuffer = '' - this.authenticated = false - this.loginAttempted = false - this.passwordAttempted = false - } - - async connect (options = {}) { - Object.assign(this.options, options) - - // If proxy is specified, establish proxied connection first - if (this.options.proxy) { - try { - const info = await proxySock({ - readyTimeout: this.options.timeout, - host: this.options.host, - port: this.options.port, - proxy: this.options.proxy - }) - this.options.sock = info.socket - } catch (error) { - this.emit('error', error) - throw error - } - } - - return new Promise((resolve, reject) => { - if (this.options.sock) { - this.socket = this.options.sock - } else { - this.socket = new Socket() - } - - this.socket.setTimeout(this.options.timeout || 0) - - this.socket.on('connect', () => { - this.state = 'connected' - this.emit('connect') - if (!this.options.negotiationMandatory) { - resolve() - } - }) - - this.socket.on('timeout', () => { - this.emit('timeout') - reject(new Error('Connection timeout')) - }) - - this.socket.on('error', (error) => { - this.emit('error', error) - reject(error) - }) - - this.socket.on('end', () => { - this.emit('end') - }) - - this.socket.on('close', () => { - this.emit('close') - }) - - this.socket.on('data', (data) => { - const processedData = this.processData(data) - if (processedData && processedData.length > 0) { - this.handleLoginSequence(processedData) - } - }) - - // If sock was provided (including from proxy), emit connect event - // Otherwise, create a new connection - if (this.options.sock) { - // Socket already connected via proxy - this.state = 'connected' - this.emit('connect') - if (!this.options.negotiationMandatory) { - resolve() - } - } else { - this.socket.connect({ - host: this.options.host, - port: this.options.port - }) - } - - this.once('telnetProtocol', () => { - this.emitTelnet(TelnetCommands.DO, TelnetOptions.SUPPRESS_GO_AHEAD) - this.emitTelnet(TelnetCommands.WILL, TelnetOptions.TERMINAL_TYPE) - this.emitTelnet(TelnetCommands.WILL, TelnetOptions.NEGO_WINDOW_SIZE) - if (this.options.negotiationMandatory) { - resolve() - } - }) - }) - } - - handleLoginSequence (data) { - if (this.authenticated) { - this.emit('data', data) - return - } - - const str = data.toString() - this.dataBuffer += str - - // Check for failed login - if (this.options.failedLoginMatch.test(this.dataBuffer)) { - this.emit('failedlogin') - this.dataBuffer = '' - return - } - - // Check for login prompt - if (!this.loginAttempted && - this.options.username && - this.options.loginPrompt.test(this.dataBuffer)) { - setTimeout(() => { - this.socket.write(this.options.username + '\n') - }, 100) - this.loginAttempted = true - this.dataBuffer = '' - return - } - - // Check for password prompt - if (!this.passwordAttempted && - this.options.password && - this.options.passwordPrompt.test(this.dataBuffer)) { - setTimeout(() => { - this.socket.write(this.options.password + '\n') - }, 100) - this.passwordAttempted = true - this.dataBuffer = '' - return - } - - // If both login and password were attempted, consider it authenticated - if (this.loginAttempted && this.passwordAttempted) { - this.authenticated = true - this.emit('data', data) - } - - // Keep only last chunk in buffer for prompt detection - if (this.dataBuffer.length > 1024) { - this.dataBuffer = this.dataBuffer.slice(-1024) - } - } - - processData (data) { - if (!this.telnetProtocol && data[0] === TelnetCommands.IAC) { - this.telnetProtocol = true - this.emit('telnetProtocol') - } - - if (this.telnetProtocol) { - data = this.processTelnetProtocol(data) - } - - if (data && data.length > 0) { - return data - } - return null - } - - processTelnetProtocol (data) { - let position = 0 - let resultBuffer = Buffer.alloc(0) - - while (position < data.length) { - if (data[position] === TelnetCommands.IAC) { - if (position + 1 >= data.length) { - this.buffer = data.slice(position) - return Buffer.concat([resultBuffer, data.slice(0, position)]) - } - - const command = data[position + 1] - - if (command === TelnetCommands.IAC) { - resultBuffer = Buffer.concat([resultBuffer, Buffer.from([TelnetCommands.IAC])]) - position += 2 - } else if ([TelnetCommands.WILL, TelnetCommands.WONT, TelnetCommands.DO, TelnetCommands.DONT].includes(command)) { - if (position + 2 >= data.length) { - this.buffer = data.slice(position) - return Buffer.concat([resultBuffer, data.slice(0, position)]) - } - - const option = data[position + 2] - this.handleTelnetCommand(command, option) - position += 3 - } else if (command === TelnetCommands.SUBOPTION) { - let endPos = position + 2 - while (endPos < data.length - 1) { - if (data[endPos] === TelnetCommands.IAC && data[endPos + 1] === TelnetCommands.SUBOPTION_END) { - break - } - endPos++ - } - - if (endPos >= data.length - 1) { - this.buffer = data.slice(position) - return Buffer.concat([resultBuffer, data.slice(0, position)]) - } - - this.handleSuboption(data.slice(position + 2, endPos)) - position = endPos + 2 - } else { - position += 2 - } - } else { - const nextIAC = data.indexOf(TelnetCommands.IAC, position) - if (nextIAC === -1) { - resultBuffer = Buffer.concat([resultBuffer, data.slice(position)]) - break - } else { - resultBuffer = Buffer.concat([resultBuffer, data.slice(position, nextIAC)]) - position = nextIAC - } - } - } - - return resultBuffer - } - - handleTelnetCommand (command, option) { - switch (command) { - case TelnetCommands.WILL: - if ([TelnetOptions.SUPPRESS_GO_AHEAD, TelnetOptions.ECHO].includes(option)) { - this.emitTelnet(TelnetCommands.DO, option) - } else { - this.emitTelnet(TelnetCommands.DONT, option) - } - break - - case TelnetCommands.DO: - if (option === TelnetOptions.NEGO_WINDOW_SIZE) { - this.emitTelnet(TelnetCommands.WILL, option) - this.sendWindowSize() - } else if (option === TelnetOptions.TERMINAL_TYPE) { - this.emitTelnet(TelnetCommands.WILL, option) - } else { - this.emitTelnet(TelnetCommands.WONT, option) - } - break - - case TelnetCommands.WONT: - case TelnetCommands.DONT: - // Do nothing - break - } - } - - handleSuboption (data) { - const option = data[0] - if (option === TelnetOptions.TERMINAL_TYPE) { - if (data[1] === 1) { // SEND - this.emitTelnetSuboption(TelnetOptions.TERMINAL_TYPE, - Buffer.from([0, ...Buffer.from('xterm')])) - } - } - } - - emitTelnet (command, option) { - this.socket.write(Buffer.from([TelnetCommands.IAC, command, option])) - } - - emitTelnetSuboption (option, value) { - this.socket.write(Buffer.from([ - TelnetCommands.IAC, - TelnetCommands.SUBOPTION, - option, - ...value, - TelnetCommands.IAC, - TelnetCommands.SUBOPTION_END - ])) - } - - sendWindowSize () { - const { terminalWidth, terminalHeight } = this.options - this.emitTelnetSuboption(TelnetOptions.NEGO_WINDOW_SIZE, Buffer.from([ - terminalWidth >> 8, terminalWidth & 0xff, - terminalHeight >> 8, terminalHeight & 0xff - ])) - } - - shell (options = {}) { - return new Stream(this.socket, options) - } - - end () { - if (this.socket) { - this.socket.end() - } - } - - destroy () { - if (this.socket) { - this.socket.destroy() - } - } -} diff --git a/src/app/server/terminal-api.js b/src/app/server/terminal-api.js deleted file mode 100644 index 56a5982..0000000 --- a/src/app/server/terminal-api.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * run cmd with terminal - */ - -import { terminals } from './remote-common.js' -import { terminal, testConnection } from './session.js' -import { isDev } from '../common/runtime-constants.js' - -export async function runCmd (ws, msg) { - const { id, pid, cmd } = msg - const term = terminals(pid) - let txt = '' - if (term) { - txt = await term.runCmd(cmd) - } - ws.s({ - id, - data: txt - }) -} - -// Structured command execution: unlike runCmd, returns -// { stdout, stderr, exitCode, timedOut } from a dedicated exec channel. -// In electerm-web sessions live in-process (see remote-common.js), so the -// session's execCommand(cmd, options) is called directly instead of going -// through a child-process proxy. -export async function execCmd (ws, msg) { - const { id, pid, cmd, timeoutMs } = msg - const term = terminals(pid) - if (!term || typeof term.execCommand !== 'function') { - ws.s({ - id, - error: { - message: 'Exec channel not supported for this session type' - } - }) - return - } - try { - const result = await term.execCommand(cmd, { timeoutMs }) - ws.s({ - id, - data: result - }) - } catch (err) { - ws.s({ - id, - error: { - message: err.message, - stack: err.stack - } - }) - } -} - -export function resize (ws, msg) { - const { id, pid, cols, rows } = msg - const term = terminals(pid) - if (term) { - term.resize(cols, rows) - } - ws.s({ - id, - data: 'ok' - }) -} - -export function toggleTerminalLog (ws, msg) { - const { id, pid } = msg - const term = terminals(pid) - if (term) { - term.toggleTerminalLog() - } - ws.s({ - id, - data: 'ok' - }) -} - -export function toggleTerminalLogTimestamp (ws, msg) { - const { id, pid } = msg - const term = terminals(pid) - if (term) { - term.toggleTerminalLogTimestamp() - } - ws.s({ - id, - data: 'ok' - }) -} - -export function setTerminalLogPath (ws, msg) { - const { id, pid, logPath } = msg - const term = terminals(pid) - if (term) { - term.setTerminalLogPath(logPath) - } - ws.s({ - id, - data: 'ok' - }) -} - -export function startTerminalLogFile (ws, msg) { - const { id, pid, logFilePath, addTimeStampToTermLog } = msg - const term = terminals(pid) - if (term) { - term.startTerminalLogFile(logFilePath, addTimeStampToTermLog) - } - ws.s({ - id, - data: 'ok' - }) -} - -export function createTerm (ws, msg) { - const { id, body } = msg - terminal(body, ws) - .then(r => { - const data = isDev - ? { - pid: r.pid, - port: process.env.PORT - } - : { - pid: r.pid - } - ws.s({ - id, - data - }) - }) - .catch(err => { - ws.s({ - id, - error: { - message: err.message, - stack: err.stack - } - }) - }) -} - -export function testTerm (ws, msg) { - const { id, body } = msg - testConnection(body, ws) - .then(data => { - if (data) { - ws.s({ - id, - data - }) - } else { - ws.s({ - id, - error: { - message: 'test failed', - stack: 'test failed' - } - }) - } - }) - .catch(err => { - ws.s({ - id, - error: { - message: err.message || 'test failed', - stack: err.stack || 'test failed' - } - }) - }) -} diff --git a/src/app/server/transfer.js b/src/app/server/transfer.js deleted file mode 100644 index 463b599..0000000 --- a/src/app/server/transfer.js +++ /dev/null @@ -1,475 +0,0 @@ -/** - * transfer class - */ - -import fs from 'fs' -import _ from 'lodash' -import log from '../common/log.js' -import * as tar from 'tar' -import { Transfer as Ssh2ScpTransfer } from 'ssh2-scp/transfer' -import { FolderTransfer } from 'ssh2-scp/folder-transfer' -import iconv from 'iconv-lite' - -export class Transfer { - constructor ({ - remotePath, - localPath, - options = {}, - id, - type = 'download', - sftp, - conn, - sftpId, - isDirectory = false, - ws, - encode = 'utf8' - }) { - this.id = id - const isd = type === 'download' - this.src = isd ? sftp : fs - this.dst = isd ? fs : sftp - this.sftp = sftp - this.ownsSftp = false - this.sftpId = sftpId - this.srcPath = isd ? remotePath : localPath - this.dstPath = !isd ? remotePath : localPath - this.pausing = false - this.hadError = false - this.isUpload = isd - this.options = options - this.conn = conn - this.isDirectory = isDirectory - this.concurrency = options.concurrency || 64 - this.chunkSize = options.chunkSize || 32768 - this.mode = options.mode - this.encode = encode - this.onData = _.throttle((count) => { - ws.s({ - id: 'transfer:data:' + id, - data: count - }) - }, 3000) - this.timers = {} - - this.ws = ws - this.initTransfer(type) - } - - shouldUseSsh2ScpTransfer = () => { - if ((this.src && this.src.isSshFsFallback) || (this.dst && this.dst.isSshFsFallback)) { - return true - } - return false - } - - initTransfer = async (type) => { - // For regular file transfers (not folder transfers, not SSH FS fallback), - // create a separate SFTP channel on the same SSH connection so that - // directory listing and other SFTP operations remain responsive. - // Each transfer gets its own dedicated channel and closes it when done. - if ( - !this.isDirectory && - this.conn && - !this.shouldUseSsh2ScpTransfer() - ) { - try { - const separateSftp = await new Promise((resolve, reject) => { - this.conn.sftp((err, sftp) => { - if (err) { - return reject(err) - } - resolve(sftp) - }) - }) - this.sftp = separateSftp - this.ownsSftp = true - const isd = type === 'download' - this.src = isd ? separateSftp : fs - this.dst = isd ? fs : separateSftp - } catch (e) { - // Fallback to the shared SFTP channel (src/dst already set in constructor) - } - } - - if (this.shouldUseFolderTransfer(type)) { - return this.ssh2ScpFolderTransfer(type) - } - if (this.shouldUseSsh2ScpTransfer()) { - return this.ssh2ScpTransfer(type) - } - this.fastXfer(type) - } - - shouldUseFolderTransfer = (type) => { - return this.isDirectory && - this.conn - } - - ssh2ScpFolderTransfer = async (type) => { - try { - const remotePath = type === 'download' ? this.srcPath : this.dstPath - const localPath = type === 'download' ? this.dstPath : this.srcPath - const folderOpts = { - type, - remotePath, - localPath, - chunkSize: this.chunkSize, - onProgress: (transferred, total) => { - this.onData({ - transferred, - total - }) - } - } - if (this.encode !== 'utf8') { - folderOpts.iconv = iconv - folderOpts.encoding = this.encode - } - this.scpTransfer = new FolderTransfer(this.conn, tar, folderOpts) - await this.scpTransfer.startTransfer() - const state = this.scpTransfer.getState - ? this.scpTransfer.getState() - : {} - this.onEnd({ - transferred: state.transferred, - size: state.total - }) - } catch (err) { - this.onError(err) - } - } - - ssh2ScpTransfer = async (type) => { - try { - const sshFs = type === 'download' ? this.src : this.dst - const remotePath = type === 'download' ? this.srcPath : this.dstPath - const localPath = type === 'download' ? this.dstPath : this.srcPath - - this.scpTransfer = new Ssh2ScpTransfer(sshFs, { - type, - remotePath, - localPath, - chunkSize: this.chunkSize, - onProgress: (transferred) => { - this.onData(transferred) - } - }) - await this.scpTransfer.startTransfer() - this.onEnd() - } catch (err) { - this.onError(err) - } - } - - tryCreateBuffer = (size) => { - try { - return Buffer.allocUnsafe(size) - } catch (ex) { - return ex - } - } - - // from https://github.com/mscdex/ssh2-streams/blob/master/lib/sftp.js - fastXfer () { - const { src, srcPath } = this - src.open(srcPath, 'r', this.onSrcOpen) - } - - onSrcOpen = (err, sourceHandle) => { - if (err) { - return this.onError(err) - } - if (this.onDestroy) { - return - } - const { src } = this - const th = this - - th.srcHandle = sourceHandle - - src.fstat(th.srcHandle, this.tryStat) - } - - tryStat = (err, attrs) => { - const { src, dst, srcPath, dstPath } = this - const th = this - if (err) { - if (src !== fs) { - // Try stat() for sftp servers that may not support fstat() for - // whatever reason - src.stat(srcPath, (err_, attrs_) => { - if (err_) { - return th.onError(err_) - } - this.tryStat(null, attrs_) - }) - return - } - return th.onError(err) - } - this.fsize = attrs.size - dst.open(dstPath, 'w', this.onDstOpen) - } - - onDstOpen = (err, destHandle) => { - if (err) { - return this.onError(err) - } - - if (this.onDestroy) { - return - } - - let { - concurrency, - chunkSize, - mode - } = this - const onstep = this.onData - const { src, dst, dstPath } = this - const th = this - - // internal state variables - let pdst = 0 - let total = 0 - let bufsize = chunkSize * concurrency - - const { fsize } = this - - th.dstHandle = destHandle - - let hadError = false - - function onerror (err) { - if (hadError) return - hadError = true - const canCloseSrc = th.srcHandle && (src === fs || (src.outgoing && src.outgoing.state === 'open')) - const canCloseDst = th.dstHandle && (dst === fs || (dst.outgoing && dst.outgoing.state === 'open')) - - const closeHandles = () => { - let left = 0 - if (canCloseSrc) ++left - if (canCloseDst) ++left - const finish = () => { - if (--left === 0) { - if (err) th.onError(err) - else th.onEnd() - } - } - if (left === 0) { - if (err) th.onError(err) - else th.onEnd() - return - } - if (canCloseSrc) { - src.close(th.srcHandle, () => { - th.srcHandle = undefined - finish() - }) - } - if (canCloseDst) { - dst.close(th.dstHandle, () => { - th.dstHandle = undefined - finish() - }) - } - } - - // Do not preserve source file mtime on destination after transfer. - // The uploaded/downloaded file should have the transfer time as both - // create time and modify time, which is the common practice for - // SFTP/FTP clients (e.g. OpenSSH sftp, FileZilla). - closeHandles() - } - - if (fsize <= 0) { - return onerror() - } - - // Use less memory where possible - while (bufsize > fsize) { - if (concurrency === 1) { - bufsize = fsize - break - } - bufsize -= chunkSize - --concurrency - } - - const readbuf = th.tryCreateBuffer(bufsize) - if (readbuf instanceof Error) { - return th.onError(readbuf) - } - - if (mode !== undefined) { - dst.fchmod(th.dstHandle, mode, function tryAgain (err) { - if (err) { - // Try chmod() for sftp servers that may not support fchmod() for - // whatever reason - dst.chmod(dstPath, mode, function (err_) { - tryAgain() - }) - return - } - startReads() - }) - } else { - startReads() - } - - function onread (err, nb, data, dstpos, datapos, origChunkLen) { - if (hadError) { - return - } - if (err) { - return onerror(err) - } - - if (th.onDestroy) { - return - } - - datapos = datapos || 0 - - dst.write(th.dstHandle, readbuf, datapos, nb, dstpos, writeCb) - - function writeCb (err) { - if (hadError) { - return - } - if (err) { - return onerror(err) - } - - total += nb - onstep && onstep(total, nb, fsize) - - if (nb < origChunkLen) { - return singleRead(datapos, dstpos + nb, origChunkLen - nb) - } - - if (total === fsize) { - return onerror() - } - - if (pdst >= fsize) { - return - } - - const chunk = (pdst + chunkSize > fsize ? fsize - pdst : chunkSize) - singleRead(datapos, pdst, chunk) - pdst += chunk - } - } - - function makeCb (psrc, pdst, chunk) { - return function (err, nb, data) { - onread(err, nb, data, pdst, psrc, chunk) - } - } - - function singleRead (psrc, pdst, chunk) { - if (th.onDestroy || hadError) { - return - } - if (th.pausing) { - th.timers[psrc + ':' + pdst] = setTimeout(() => { - singleRead(psrc, pdst, chunk) - }, 2) - return - } - src.read( - th.srcHandle, - readbuf, - psrc, - chunk, - pdst, - makeCb(psrc, pdst, chunk) - ) - } - - function startReads () { - let reads = 0 - let psrc = 0 - while (pdst < fsize && reads < concurrency) { - const chunk = (pdst + chunkSize > fsize ? fsize - pdst : chunkSize) - singleRead(psrc, pdst, chunk) - psrc += chunk - pdst += chunk - ++reads - } - } - } - - onEnd = (data = null, id = this.id, ws = this.ws) => { - ws?.s({ - id: 'transfer:end:' + id, - data - }) - } - - onError = (err = '', id = this.id, ws = this.ws) => { - if (!err) { - return this.onEnd() - } - ws && ws.s({ - id: 'transfer:err:' + id, - error: { - message: err.message, - stack: err.stack - } - }) - } - - pause = () => { - this.pausing = true - this.scpTransfer && this.scpTransfer.pause && this.scpTransfer.pause() - } - - resume = () => { - this.pausing = false - this.scpTransfer && this.scpTransfer.resume && this.scpTransfer.resume() - } - - kill = () => { - if (this.src && this.srcHandle && this.src.close) { - this.src.close(this.srcHandle, log.error) - } - if (this.dst && this.dstHandle && this.dst.close) { - this.dst.close(this.dstHandle, log.error) - } - // Close the transfer-specific SFTP channel if we created one - if (this.ownsSftp && this.sftp && this.sftp.end) { - this.sftp.end() - } - this.src = null - this.dst = null - this.srcHandle = null - this.dstHandle = null - } - - destroy = () => { - this.onDestroy = true - this.scpTransfer && this.scpTransfer.destroy && this.scpTransfer.destroy() - setTimeout(this.kill, 200) - if (this.ws) { - this.ws.close() - this.ws = null - } - if (this.timers) { - Object.keys(this.timers).forEach(k => { - clearTimeout(this.timers[k]) - this.timers[k] = null - }) - this.timers = null - } - } - - // end -} - -export const transferKeys = [ - 'pause', - 'resume', - 'destroy' -] diff --git a/src/app/server/trzsz.js b/src/app/server/trzsz.js deleted file mode 100644 index c597e81..0000000 --- a/src/app/server/trzsz.js +++ /dev/null @@ -1,730 +0,0 @@ -/** - * Optimized Trzsz protocol handler for server-side terminal sessions - */ -import fs from 'fs' -import { open } from 'fs/promises' -import path from 'path' -import log from '../common/log.js' -import sanitizeFilename from '../common/sanitize-filename.js' -import { TrzszTransfer } from 'trzsz2' - -const TRZSZ_STATE = { - IDLE: 'idle', - RECEIVING: 'receiving', - SENDING: 'sending', - WAITING_SAVE_PATH: 'waiting_save_path' -} -const TRZSZ_MAGIC_KEY_PREFIX_BUFFER = Buffer.from('::TRZSZ:TRANSFER:') -const TRZSZ_GO_MAGIC_KEY_PREFIX_BUFFER = Buffer.from('::TRZSZGO:TRANSFER:') -const TRZSZ_SUCCESS_BUFFER = Buffer.from('Success') -const TRZSZ_SAVED_BUFFER = Buffer.from('Saved') -const TRZSZ_SAVED_FILE_BUFFER = Buffer.from('Saved file') -const TRZSZ_SAVED_DIR_BUFFER = Buffer.from('Saved directory') -const READ_CHUNK_SIZE = 10 * 1024 * 1024 -const WRITE_HIGH_WATER_MARK = 10 * 1024 * 1024 -const PROGRESS_INTERVAL_MS = 300 -const COMPLETION_TIMEOUT_MS = 5000 -/** - * Optimized FileReader - uses async I/O with adaptive buffer sizing - */ -class FileReader { - constructor (filePath, fileName) { - this.filePath = filePath - this.fileName = fileName - this.fileHandle = null - this.size = 0 - this.offset = 0 - this.pathId = 0 - this.relPath = [fileName] - this.isDirectory = false - this._cacheBuffer = null - this._cacheOffset = 0 - this._cacheEnd = 0 - } - - async open () { - const stats = fs.statSync(this.filePath) - this.size = stats.size - this.fileHandle = await open(this.filePath, 'r') - } - - getPathId () { return this.pathId } - getRelPath () { return this.relPath } - isDir () { return this.isDirectory } - getSize () { return this.size } - async readFile (buf) { - if (this._cacheBuffer && this._cacheOffset < this._cacheEnd) { - const available = this._cacheEnd - this._cacheOffset - const result = new Uint8Array(this._cacheBuffer.buffer, this._cacheBuffer.byteOffset + this._cacheOffset, available) - this._cacheOffset = this._cacheEnd - this.offset += available - return result - } - - const remaining = this.size - this.offset - if (remaining <= 0) return new Uint8Array(0) - - const readSize = Math.min(READ_CHUNK_SIZE, remaining) - if (!this._cacheBuffer || this._cacheBuffer.byteLength < readSize) { - this._cacheBuffer = Buffer.allocUnsafe(readSize) - } - const { bytesRead } = await this.fileHandle.read(this._cacheBuffer, 0, readSize, this.offset) - if (bytesRead === 0) return new Uint8Array(0) - - this.offset += bytesRead - this._cacheOffset = bytesRead - this._cacheEnd = bytesRead - return new Uint8Array(this._cacheBuffer.buffer, this._cacheBuffer.byteOffset, bytesRead) - } - - async closeFile () { - if (this.fileHandle !== null) { - await this.fileHandle.close().catch(() => {}) - this.fileHandle = null - } - this._cacheBuffer = null - this._cacheOffset = 0 - this._cacheEnd = 0 - } -} - -/** - * Optimized FileWriter - buffered writes with backpressure handling - */ -class FileWriter { - constructor (filePath, fileName) { - this.filePath = filePath - this.fileName = fileName - this.localName = fileName - this.isDirectory = false - this.writeStream = null - this._drainPromise = null - } - - getFileName () { return this.fileName } - getLocalName () { return this.localName } - isDir () { return this.isDirectory } - _ensureStream () { - if (this.writeStream === null) { - this.writeStream = fs.createWriteStream(this.filePath, { - highWaterMark: WRITE_HIGH_WATER_MARK, - flags: 'w' - }) - this.writeStream.on('error', (err) => { - log.error('FileWriter stream error:', err) - }) - } - } - - async writeFile (buf) { - this._ensureStream() - const canContinue = this.writeStream.write(buf) - if (!canContinue) { - if (!this._drainPromise) { - this._drainPromise = new Promise((resolve) => { - this.writeStream.once('drain', () => { - this._drainPromise = null - resolve() - }) - }) - } - await this._drainPromise - } - } - - async deleteFile () { - await this._closeStream() - return this.filePath - } - - async _closeStream () { - if (this.writeStream !== null) { - return new Promise((resolve) => { - this.writeStream.end(() => { - this.writeStream = null - resolve() - }) - }) - } - } - - async closeFile () { - await this._closeStream() - } -} -/** - * Optimized TrzszSession - event-driven, minimal allocations - */ -class TrzszSession { - constructor (term, ws) { - this.term = term - this.ws = ws - this.state = TRZSZ_STATE.IDLE - this.transfer = null - this.currentTransfer = null - this.downloadPath = null - this.uploadPath = null - this.transferSize = 0 - this.transferredBytes = 0 - this.startTime = 0 - this.savePath = null - this.pendingFiles = [] - this.fileReaders = [] - this.fileWriters = [] - this.lastProgressUpdate = 0 - this._pendingComplete = null - this.completedFiles = [] - this.totalBytes = 0 - this.transferStartTime = 0 - this._completionTimeout = null - this._filesResolve = null - this._savePathResolve = null - this._noDelayEnabled = false - this._cancelSuppressUntil = 0 - this._cancelSuppressTimeout = null - this._cancelling = false - } - - _setNoDelay (enabled) { - const canToggle = this.term && typeof this.term.setNoDelay === 'function' - if (!canToggle) return - if (enabled && !this._noDelayEnabled) { - this.term.setNoDelay(true) - this._noDelayEnabled = true - return - } - if (!enabled && this._noDelayEnabled) { - this.term.setNoDelay(false) - this._noDelayEnabled = false - } - } - - detectTrzszStart (data) { - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - let idx = buf.indexOf(TRZSZ_MAGIC_KEY_PREFIX_BUFFER) - let prefixLen = TRZSZ_MAGIC_KEY_PREFIX_BUFFER.length - - if (idx < 0) { - idx = buf.indexOf(TRZSZ_GO_MAGIC_KEY_PREFIX_BUFFER) - prefixLen = TRZSZ_GO_MAGIC_KEY_PREFIX_BUFFER.length - } - - if (idx < 0) return null - const afterPrefix = idx + prefixLen - if (afterPrefix >= buf.length) return null - const direction = buf[afterPrefix] - if (direction === 82) return { type: 'send', offset: idx } - if (direction === 83) return { type: 'receive', offset: idx } - return null - } - - /** - * Send message to client via websocket - */ - sendToClient (msg) { - if (this.ws && this.ws.s) { - this.ws.s({ action: 'trzsz-event', ...msg }) - } - } - - /** - * Handle incoming data from terminal - * Returns true if data was consumed by trzsz - */ - handleData (data) { - // During cancel suppression window, absorb all data to prevent - // protocol garbage from leaking to the terminal - if (this._cancelSuppressUntil > 0) { - if (Date.now() < this._cancelSuppressUntil) { - return true - } - this._cancelSuppressUntil = 0 - } - if (this._pendingComplete) { - const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) - if (buf.indexOf(TRZSZ_SUCCESS_BUFFER) >= 0) { - this.sendToClient(this._pendingComplete) - this._pendingComplete = null - this.endSession() - return true - } - if (buf.indexOf(TRZSZ_SAVED_BUFFER) >= 0) { - this.endSession() - return true - } - if (this.transfer) { - this.transfer.addReceivedData(data) - return true - } - } - if (this.state === TRZSZ_STATE.RECEIVING || this.state === TRZSZ_STATE.SENDING) { - this.transfer.addReceivedData(data) - return true - } - const detected = this.detectTrzszStart(data) - if (detected) { - if (detected.type === 'receive') { - this.startReceiver() - this.transfer.addReceivedData(data) - } else if (detected.type === 'send') { - this.createTransfer() - this.state = TRZSZ_STATE.SENDING - this.transfer.addReceivedData(data) - this.startUploadProcess() - } - return true - } - return false - } - - _waitForFiles () { - if (this.pendingFiles.length > 0) { - return Promise.resolve(this.pendingFiles) - } - return new Promise((resolve) => { - this._filesResolve = resolve - }) - } - - async startUploadProcess () { - try { - this._cancelling = false - this._setNoDelay(true) - this.completedFiles = [] - this.totalBytes = 0 - this.transferStartTime = 0 - await this.transfer.sendAction(true, false) - - this.sendToClient({ - event: 'send-start', - message: 'TRZSZ send session started, please select files' - }) - await this.transfer.recvConfig() - - const files = await this._waitForFiles() - if (this.state !== TRZSZ_STATE.SENDING) return - this.fileReaders = files.map(file => { - const filePath = typeof file === 'string' ? file : file.path - return new FileReader(filePath, path.basename(filePath)) - }) - await Promise.all(this.fileReaders.map(r => r.open())) - this.totalBytes = this.fileReaders.reduce((sum, r) => sum + r.size, 0) - this.transferStartTime = Date.now() - - const progressCallback = this._createProgressCallback('upload') - const remoteNames = await this.transfer.sendFiles(this.fileReaders, progressCallback) - const totalElapsed = this.transferStartTime > 0 - ? (Date.now() - this.transferStartTime) / 1000 - : 0 - - this._pendingComplete = { - event: 'session-complete', - message: 'Upload complete', - files: remoteNames, - totalBytes: this.totalBytes, - totalElapsed, - avgSpeed: totalElapsed > 0 ? Math.round(this.totalBytes / totalElapsed) : 0, - completedFiles: this.completedFiles - } - this._completionTimeout = setTimeout(() => { - if (this._pendingComplete) { - log.warn('Trzsz upload: timeout waiting for server response, auto-ending session') - this.sendToClient(this._pendingComplete) - this._pendingComplete = null - this.endSession() - } - }, COMPLETION_TIMEOUT_MS) - await this.transfer.clientExit('Success') - await Promise.all(this.fileReaders.map(r => r.closeFile())) - this.state = TRZSZ_STATE.IDLE - } catch (err) { - if (this._cancelling) { - log.info('Trzsz upload cancelled by user') - } else { - log.error('Trzsz upload error:', err) - this.sendToClient({ event: 'session-error', error: err.message }) - this.endSession() - } - } - } - - _createProgressCallback (type) { - return { - onNum: (num) => { - this.sendToClient({ event: 'file-count', count: num }) - }, - onName: (name) => { - if (this.currentTransfer && this.currentTransfer.size > 0) { - this.completedFiles.push({ - name: this.currentTransfer.name, - size: this.currentTransfer.size, - ...(type === 'download' ? { path: this.downloadPath } : {}) - }) - if (type === 'download') { - this.totalBytes += this.currentTransfer.size - } - } - this.currentTransfer = { name, size: 0 } - if (!this.transferStartTime) { - this.transferStartTime = Date.now() - } - this.startTime = Date.now() - this.sendToClient({ event: 'file-start', name, size: 0 }) - }, - onSize: (size) => { - if (this.currentTransfer) this.currentTransfer.size = size - this.transferSize = size - this.sendToClient({ event: 'file-size', name: this.currentTransfer?.name, size }) - }, - onStep: (step) => { - this.transferredBytes = step - const now = Date.now() - if (now - this.lastProgressUpdate > PROGRESS_INTERVAL_MS) { - this.lastProgressUpdate = now - this.sendProgress() - } - }, - onDone: () => { - if (this.currentTransfer && this.currentTransfer.size > 0) { - this.completedFiles.push({ - name: this.currentTransfer.name, - size: this.currentTransfer.size, - ...(type === 'download' ? { path: this.downloadPath } : {}) - }) - if (type === 'download') { - this.totalBytes += this.currentTransfer.size - } - } - this.sendToClient({ - event: 'file-complete', - name: this.currentTransfer?.name, - path: type === 'download' ? this.downloadPath : this.uploadPath - }) - } - } - } - - createTransfer () { - this.transfer = new TrzszTransfer((data) => { - if (typeof data === 'string') { - if ( - data.length < 200 && - (data.includes('Saved file') || data.includes('Saved directory')) - ) { - return - } - this.writeToTerminal(data) - return - } - - if (Buffer.isBuffer(data)) { - if ( - data.length < 200 && - (data.indexOf(TRZSZ_SAVED_FILE_BUFFER) >= 0 || - data.indexOf(TRZSZ_SAVED_DIR_BUFFER) >= 0) - ) { - return - } - this.writeToTerminal(data) - return - } - - this.writeToTerminal(data) - }, false) - - return this.transfer - } - - _waitForSavePath () { - if (this.savePath) return Promise.resolve(this.savePath) - return new Promise((resolve) => { - this._savePathResolve = resolve - }) - } - - startReceiver () { - try { - this._cancelling = false - this._setNoDelay(true) - this.createTransfer() - this.completedFiles = [] - this.totalBytes = 0 - this.transferStartTime = 0 - this.state = TRZSZ_STATE.RECEIVING - this.sendToClient({ - event: 'receive-start', - message: 'TRZSZ receive session started' - }) - this._runReceiverHandshake() - } catch (e) { - log.error('Failed to start trzsz receiver', e) - this.endSession() - } - } - - async _runReceiverHandshake () { - try { - await this.transfer.sendAction(true, false) - await this.transfer.recvConfig() - await this._waitForSavePath() - if (this.state !== TRZSZ_STATE.RECEIVING) return - await this._startFileReceiving() - } catch (err) { - log.error('Trzsz receiver handshake error:', err) - this.sendToClient({ event: 'session-error', error: err.message }) - this.endSession() - } - } - - async _startFileReceiving () { - try { - const downloadDir = this.savePath - if (!fs.existsSync(downloadDir)) { - fs.mkdirSync(downloadDir, { recursive: true }) - } - - const openSaveFile = async (saveParam, fileName, directory, overwrite) => { - const filePath = this.getUniqueFilePath(downloadDir, fileName) - const writer = new FileWriter(filePath, fileName) - this.fileWriters.push(writer) - if (this.currentTransfer) { - this.sendToClient({ - event: 'file-complete', - name: this.currentTransfer.name, - path: this.downloadPath - }) - } - this.currentTransfer = { name: fileName, size: 0 } - this.downloadPath = filePath - this.transferredBytes = 0 - this.startTime = Date.now() - this.sendToClient({ event: 'file-start', name: fileName, size: 0 }) - return writer - } - const progressCallback = this._createProgressCallback('download') - const savedFiles = await this.transfer.recvFiles( - downloadDir, - openSaveFile, - progressCallback - ) - - const savedFilePaths = savedFiles.map(name => path.join(downloadDir, sanitizeFilename(name))) - await Promise.all(this.fileWriters.map(w => w.closeFile())) - const totalElapsed = this.transferStartTime > 0 - ? (Date.now() - this.transferStartTime) / 1000 - : 0 - - this._pendingComplete = { - event: 'session-complete', - message: 'Download complete', - files: savedFilePaths, - savePath: downloadDir, - totalBytes: this.totalBytes, - totalElapsed, - avgSpeed: totalElapsed > 0 ? Math.round(this.totalBytes / totalElapsed) : 0, - completedFiles: this.completedFiles - } - this._completionTimeout = setTimeout(() => { - if (this._pendingComplete) { - log.warn('Trzsz download: timeout waiting for server response, auto-ending session') - this.sendToClient(this._pendingComplete) - this._pendingComplete = null - this.endSession() - } - }, COMPLETION_TIMEOUT_MS) - this.state = TRZSZ_STATE.IDLE - await this.transfer.clientExit('Success') - } catch (err) { - if (this._cancelling) { - log.info('Trzsz download cancelled by user') - } else { - log.error('Trzsz download error:', err) - this.sendToClient({ event: 'session-error', error: err.message }) - this.endSession() - } - } - } - - getUniqueFilePath (dir, fileName) { - const safeName = sanitizeFilename(fileName) - let filePath = path.join(dir, safeName) - if (!fs.existsSync(filePath)) return filePath - const ext = path.extname(safeName) - const baseName = path.basename(safeName, ext) - let counter = 1 - while (fs.existsSync(filePath)) { - filePath = path.join(dir, `${baseName}.${counter}${ext}`) - counter++ - } - return filePath - } - - sendProgress () { - const elapsed = (Date.now() - this.startTime) / 1000 - const speed = elapsed > 0 ? Math.round(this.transferredBytes / elapsed) : 0 - const percent = this.transferSize > 0 - ? Math.floor(this.transferredBytes * 100 / this.transferSize) - : 100 - this.sendToClient({ - event: 'progress', - name: this.currentTransfer?.name, - size: this.transferSize, - transferred: this.transferredBytes, - percent, - speed, - type: this.state === TRZSZ_STATE.RECEIVING ? 'download' : 'upload', - path: this.state === TRZSZ_STATE.RECEIVING ? this.downloadPath : this.uploadPath - }) - } - - setSavePath (savePath) { - this.savePath = savePath - if (this._savePathResolve) { - this._savePathResolve(savePath) - this._savePathResolve = null - } - } - - setSendFiles (files) { - this.pendingFiles = files - if (this._filesResolve) { - this._filesResolve(files) - this._filesResolve = null - } - } - - writeToTerminal (data) { - if (this.term && this.term.write) { - this.term.write(data) - } - } - - endSession () { - if (this._completionTimeout) { - clearTimeout(this._completionTimeout) - this._completionTimeout = null - } - if (this._cancelSuppressTimeout) { - clearTimeout(this._cancelSuppressTimeout) - this._cancelSuppressTimeout = null - } - if (this._filesResolve) { - this._filesResolve([]) - this._filesResolve = null - } - for (const writer of this.fileWriters) { - try { writer.closeFile() } catch (e) { log.error('Error closing file writer', e) } - } - for (const reader of this.fileReaders) { - try { reader.closeFile() } catch (e) { log.error('Error closing file reader', e) } - } - if (this.transfer) { - try { this.transfer.cleanup() } catch (e) { log.error('Error cleaning up transfer', e) } - } - this.sendToClient({ event: 'session-end' }) - this.state = TRZSZ_STATE.IDLE - this.transfer = null - this.currentTransfer = null - this.downloadPath = null - this.uploadPath = null - this.pendingFiles = [] - this.fileReaders = [] - this.fileWriters = [] - this.pendingData = [] - this.savePath = null - this.completedFiles = [] - this.totalBytes = 0 - this.transferStartTime = 0 - this._pendingComplete = null - this._setNoDelay(false) - } - - async cancel () { - const wasActive = this.state !== TRZSZ_STATE.IDLE - // Set cancelling flag BEFORE stopTransferring so that the - // catch blocks in startUploadProcess/_startFileReceiving - // know not to send session-error to the client - this._cancelling = true - if (this.transfer) { - try { await this.transfer.stopTransferring() } catch (e) { log.error('Error stopping transfer', e) } - } - this.endSession() - if (wasActive) { - // Suppress terminal output briefly to absorb any remaining - // protocol data from the dying remote trzsz process - const CANCEL_SUPPRESS_MS = 1000 - this._cancelSuppressUntil = Date.now() + CANCEL_SUPPRESS_MS - this._cancelSuppressTimeout = setTimeout(() => { - this._cancelSuppressUntil = 0 - this._cancelSuppressTimeout = null - // Send Enter to elicit a fresh shell prompt after suppression ends - this.writeToTerminal('\r') - }, CANCEL_SUPPRESS_MS) - // Send Ctrl+C to the remote terminal to kill the remote trzsz process - // so it doesn't hang waiting for data and eventually timeout - this.writeToTerminal('\x03') - } - // NOTE: do NOT reset _cancelling here — the async catch blocks - // in startUploadProcess/_startFileReceiving fire on the next tick - // and need to see the flag is still true. - } - - isActive () { - return this.state !== TRZSZ_STATE.IDLE || this._pendingComplete !== null || this._cancelSuppressUntil > 0 - } - - destroy () { - this.endSession() - this.term = null - this.ws = null - } -} -/** - * TrzszManager - manages sessions per terminal (unchanged API) - */ -class TrzszManager { - constructor () { - this.sessions = new Map() - } - - getSession (pid, term, ws) { - if (!this.sessions.has(pid)) { - this.sessions.set(pid, new TrzszSession(term, ws)) - } - return this.sessions.get(pid) - } - - handleData (pid, data, term, ws) { - return this.getSession(pid, term, ws).handleData(data) - } - - handleMessage (pid, msg, term, ws) { - const session = this.getSession(pid, term, ws) - switch (msg.event) { - case 'set-save-path': - session.setSavePath(msg.path) - break - case 'send-files': - session.setSendFiles(msg.files) - break - case 'cancel': - session.cancel() - break - } - } - - destroySession (pid) { - const session = this.sessions.get(pid) - if (session) { - session.destroy() - this.sessions.delete(pid) - } - } - - isActive (pid) { - const session = this.sessions.get(pid) - return session ? session.isActive() : false - } -} -const trzszManager = new TrzszManager() -export { trzszManager } diff --git a/src/app/server/webdav-sync.js b/src/app/server/webdav-sync.js deleted file mode 100644 index f50d595..0000000 --- a/src/app/server/webdav-sync.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * handle sync with WebDAV server - */ - -import log from '../common/log.js' -import rp from 'axios' -import https from 'https' -import { createProxyAgent } from '../lib/proxy-agent.js' - -rp.defaults.proxy = false - -/** - * Create an axios client for WebDAV operations - */ -function createClient (serverUrl, username, password, proxy, skipVerify = false) { - const proxyAgent = createProxyAgent(proxy) - let conf - if (proxyAgent) { - if (skipVerify) { - // Apply skipVerify through the proxy tunnel as well. - const agent = createProxyAgent(proxy, { rejectUnauthorized: false }) - conf = { httpAgent: agent, httpsAgent: agent } - } else { - conf = { httpAgent: proxyAgent, httpsAgent: proxyAgent } - } - } else if (skipVerify) { - const agent = new https.Agent({ rejectUnauthorized: false }) - conf = { httpAgent: agent, httpsAgent: agent } - } else { - conf = { proxy: false } - } - - const auth = Buffer.from(`${username}:${password}`).toString('base64') - - return rp.create({ - ...conf, - baseURL: serverUrl, - headers: { - Authorization: `Basic ${auth}`, - 'Content-Type': 'application/json; charset=utf-8' - }, - // do not throw on non-2xx so we can log status codes - validateStatus: () => true - }) -} - -/** - * Ensure directory exists on WebDAV server - */ -async function ensureDir (client, dirPath) { - log.info(`[WebDAV] ensureDir: ${dirPath}`) - const res = await client.request({ - method: 'MKCOL', - url: dirPath - }) - log.info(`[WebDAV] ensureDir: ${dirPath} -> ${res.status}`) - // 201 created, 405 already exists, 200 ok - if (res.status !== 201 && res.status !== 405 && res.status !== 200) { - throw new Error(`MKCOL ${dirPath} returned ${res.status}: ${typeof res.data === 'string' ? res.data : JSON.stringify(res.data)}`) - } -} - -/** - * Upload a file to WebDAV server - */ -async function uploadFile (client, filePath, content) { - log.info(`[WebDAV] uploadFile: ${filePath}`) - const body = typeof content === 'string' ? content : JSON.stringify(content) - const res = await client.request({ - method: 'PUT', - url: filePath, - data: body, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }) - log.info(`[WebDAV] uploadFile: ${filePath} -> ${res.status}`) - if (res.status >= 200 && res.status < 300) { - return { success: true } - } - const msg = `PUT ${filePath} returned ${res.status}: ${typeof res.data === 'string' ? res.data : JSON.stringify(res.data)}` - log.error(`[WebDAV] ${msg}`) - return { error: { message: msg } } -} - -/** - * Download a file from WebDAV server - */ -async function downloadFile (client, filePath) { - log.info(`[WebDAV] downloadFile: ${filePath}`) - const res = await client.request({ - method: 'GET', - url: filePath - }) - log.info(`[WebDAV] downloadFile: ${filePath} -> ${res.status}`) - if (res.status === 404) { - return null - } - if (res.status >= 200 && res.status < 300) { - return typeof res.data === 'string' ? res.data : JSON.stringify(res.data) - } - const msg = `GET ${filePath} returned ${res.status}: ${typeof res.data === 'string' ? res.data : JSON.stringify(res.data)}` - log.error(`[WebDAV] ${msg}`) - return { error: { message: msg } } -} - -/** - * Test connection to WebDAV server - */ -async function test (serverUrl, username, password, proxy, skipVerify) { - const client = createClient(serverUrl, username, password, proxy, skipVerify) - try { - log.info(`[WebDAV] test: probing ${serverUrl}`) - const res = await client.request({ - method: 'PROPFIND', - url: '/', - headers: { - Depth: '0' - } - }) - log.info(`[WebDAV] test: PROPFIND / -> ${res.status}`) - if (res.status === 207 || res.status === 200) { - return { success: true, status: res.status } - } - return { error: { message: `WebDAV server returned ${res.status}: ${typeof res.data === 'string' ? res.data : JSON.stringify(res.data)}` } } - } catch (err) { - log.error('[WebDAV] test error:', err.message) - log.error('[WebDAV] test error stack:', err.stack) - return { error: { message: err.message } } - } -} - -/** - * Upload electerm data to WebDAV server - */ -async function upload (serverUrl, username, password, data, proxy, skipVerify) { - const client = createClient(serverUrl, username, password, proxy, skipVerify) - const basePath = '/electerm' - - try { - log.info(`[WebDAV] upload: starting to ${serverUrl}${basePath}`) - log.info(`[WebDAV] upload: data keys = [${Object.keys(data).join(', ')}]`) - - // Ensure electerm directory exists - await ensureDir(client, basePath) - - // Upload each file - for (const [filename, content] of Object.entries(data)) { - const filePath = `${basePath}/${filename}` - const result = await uploadFile(client, filePath, content) - if (result.error) { - return { error: { message: `Failed to upload ${filename}: ${result.error.message}` } } - } - } - - log.info('[WebDAV] upload: complete') - return { success: true } - } catch (err) { - log.error('[WebDAV] upload error:', err.message) - log.error('[WebDAV] upload error stack:', err.stack) - return { error: { message: err.message } } - } -} - -/** - * Download electerm data from WebDAV server - */ -async function download (serverUrl, username, password, proxy, skipVerify) { - const client = createClient(serverUrl, username, password, proxy, skipVerify) - const basePath = '/electerm' - - try { - log.info(`[WebDAV] download: starting from ${serverUrl}${basePath}`) - - const result = { - files: {} - } - - const fileList = [ - 'settings.json', - 'bookmarks.json', - 'bookmarkGroups.json', - 'terminalThemes.json', - 'quickCommands.json', - 'profiles.json', - 'addressBookmarks.json', - 'workspaces.json', - 'userConfig.json', - 'electerm-status.json', - 'settings.order.json', - 'bookmarks.order.json', - 'bookmarkGroups.order.json', - 'terminalThemes.order.json', - 'quickCommands.order.json', - 'profiles.order.json', - 'addressBookmarks.order.json', - 'workspaces.order.json' - ] - - for (const filename of fileList) { - const filePath = `${basePath}/${filename}` - const content = await downloadFile(client, filePath) - if (content && typeof content === 'string') { - result.files[filename] = { - content - } - log.info(`[WebDAV] download: got ${filename} (${content.length} chars)`) - } - } - - log.info(`[WebDAV] download: complete, got ${Object.keys(result.files).length} files`) - return result - } catch (err) { - log.error('[WebDAV] download error:', err.message) - log.error('[WebDAV] download error stack:', err.stack) - return { error: { message: err.message } } - } -} - -/** - * Main WebDAV sync handler - */ -async function doWebdavSync (func, args, token, proxy) { - log.info(`[WebDAV] doWebdavSync: func=${func}`) - - // token format: serverUrl####username####password####skipVerify - const parts = token ? token.split('####') : [] - const serverUrl = parts[0] || '' - const username = parts[1] || '' - const password = parts[2] || '' - const skipVerify = parts[3] === 'true' - - log.info(`[WebDAV] serverUrl=${serverUrl}, username=${username}`) - - if (!serverUrl) { - const msg = 'WebDAV server URL is not configured' - log.error(`[WebDAV] ${msg}`) - return { error: { message: msg } } - } - - try { - switch (func) { - case 'test': - return await test(serverUrl, username, password, proxy, skipVerify) - case 'upload': - return await upload(serverUrl, username, password, args[0], proxy, skipVerify) - case 'download': - return await download(serverUrl, username, password, proxy, skipVerify) - default: { - const msg = `Unknown WebDAV function: ${func}` - log.error(`[WebDAV] ${msg}`) - return { error: { message: msg } } - } - } - } catch (err) { - log.error('[WebDAV] sync error:', err.message) - log.error('[WebDAV] sync error stack:', err.stack) - return { error: { message: err.message } } - } -} - -export { doWebdavSync } diff --git a/src/app/server/xmodem.js b/src/app/server/xmodem.js deleted file mode 100644 index 61c6807..0000000 --- a/src/app/server/xmodem.js +++ /dev/null @@ -1,940 +0,0 @@ -/** - * XMODEM protocol handler for serial port file transfers - * Supports XMODEM-CRC (128-byte) and XMODEM-1K (1024-byte) modes - */ - -import fs from 'fs' -import path from 'path' -import log from '../common/log.js' -import generate from '../common/uid.js' -import sanitizeFilename from '../common/sanitize-filename.js' - -// XMODEM control characters -const SOH = 0x01 // Start of 128-byte block -const STX = 0x02 // Start of 1024-byte block -const EOT = 0x04 // End of transmission -const ACK = 0x06 // Acknowledge -const NAK = 0x15 // Negative acknowledge -const CAN = 0x18 // Cancel -const CRC = 0x43 // 'C' - request CRC mode - -// Packet sizes -const PACKET_SIZE_128 = 128 -const PACKET_SIZE_1K = 1024 -// Header: SOH/STX(1) + blockNum(1) + ~blockNum(1) -const HEADER_SIZE = 3 -// Trailer: CRC-16(2) or checksum(1) -const CRC_TRAILER_SIZE = 2 -const CHECKSUM_TRAILER_SIZE = 1 - -// Protocol constants -const MAX_RETRIES = 10 -const RECEIVE_TIMEOUT_MS = 10000 // 10s timeout waiting for packet -const SEND_ACK_TIMEOUT_MS = 10000 // 10s timeout waiting for ACK -const PROGRESS_INTERVAL_MS = 500 - -// XMODEM session states -const XMODEM_STATE = { - IDLE: 'idle', - WAITING_REMOTE: 'waiting_remote', // Waiting for remote to start protocol - RECEIVING: 'receiving', - SENDING: 'sending', - WAITING_SAVE_PATH: 'waiting_save_path', - WAITING_FILES: 'waiting_files' -} - -/** - * CRC-16/XMODEM calculation - * @param {Buffer} data - * @returns {number} CRC-16 value - */ -function crc16Xmodem (data) { - let crc = 0 - for (let i = 0; i < data.length; i++) { - crc = crc ^ (data[i] << 8) - for (let j = 0; j < 8; j++) { - if (crc & 0x8000) { - crc = (crc << 1) ^ 0x1021 - } else { - crc = crc << 1 - } - } - crc = crc & 0xFFFF - } - return crc -} - -/** - * XmodemSession handles XMODEM file transfers for a terminal session - */ -class XmodemSession { - constructor (term, ws) { - this.term = term - this.ws = ws - this.state = XMODEM_STATE.IDLE - this.useCrc = true // Prefer CRC mode - this.use1K = false // 1K packets - - // Receive state - this.downloadStream = null - this.downloadPath = null - this.savePath = null - this.receiveFileName = null - this.expectedBlock = 1 - this.receiveBuffer = Buffer.alloc(0) - this.receiveTimeout = null - this.retries = 0 - - // Send state - this.uploadPath = null - this.uploadFd = null - this.sendBlock = 1 - this.sendSize = 0 - this.sentBytes = 0 - this.currentTransfer = null - this.transferSize = 0 - this.transferredBytes = 0 - this.startTime = 0 - this.sendTimeout = null - this.pendingFiles = [] - this.currentFileIndex = 0 - this.pendingSendData = [] - - // Progress - this.lastProgressUpdate = 0 - } - - /** - * Send message to client via websocket - */ - sendToClient (msg) { - if (this.ws && this.ws.s) { - this.ws.s({ - action: 'xmodem-event', - ...msg - }) - } - } - - /** - * Write data to the serial port / terminal. - * Uses writeRaw (if available) to bypass txLineEnding transformation, - * which would corrupt binary XMODEM protocol bytes (e.g. block# 0x0D = '\r'). - */ - writeToTerminal (data) { - if (!this.term) return - if (this.term.writeRaw) { - this.term.writeRaw(data) - } else if (this.term.write) { - this.term.write(data) - } - } - - /** - * Start XMODEM receive - waits for remote to send SOH/STX packets - */ - startReceive () { - this.state = XMODEM_STATE.WAITING_REMOTE - this.expectedBlock = 1 - this.receiveBuffer = Buffer.alloc(0) - this.retries = 0 - this.useCrc = true - this.use1K = false - - this.sendToClient({ - event: 'receive-start', - message: 'XMODEM receive started. Waiting for remote to send file...' - }) - - // Start timeout - if remote doesn't start sending within timeout, cancel - this.resetReceiveTimeout() - } - - /** - * Start XMODEM send - waits for remote to send NAK or 'C' - */ - startSend () { - this.state = XMODEM_STATE.WAITING_REMOTE - this.sendBlock = 1 - this.sentBytes = 0 - this.retries = 0 - - this.sendToClient({ - event: 'send-start', - message: 'XMODEM send started. Waiting for remote to request file...' - }) - - // Start timeout - this.resetReceiveTimeout() - } - - /** - * Handle incoming data from terminal - * @param {Buffer} data - * @returns {boolean} true if data was consumed by XMODEM - */ - handleData (data) { - if (!Buffer.isBuffer(data)) { - data = Buffer.from(data) - } - - // Waiting for save path - buffer data - if (this.state === XMODEM_STATE.WAITING_SAVE_PATH) { - this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]) - return true - } - - // Waiting for files to be selected - if (this.state === XMODEM_STATE.WAITING_FILES) { - this.pendingSendData.push(data) - return true - } - - // Actively receiving file data - if (this.state === XMODEM_STATE.RECEIVING) { - this.handleReceiveData(data) - return true - } - - // Actively sending - look for ACK/NAK/CAN - if (this.state === XMODEM_STATE.SENDING) { - this.handleSendResponse(data) - return true - } - - // Waiting for remote to start protocol - if (this.state === XMODEM_STATE.WAITING_REMOTE) { - if (this.pendingFiles.length > 0) { - // Send mode: look for NAK or 'C' from remote - return this.handleSendWaitData(data) - } else { - // Receive mode: look for SOH/STX from remote - return this.handleReceiveWaitData(data) - } - } - - return false - } - - /** - * Handle data while waiting for remote to start sending (receive mode) - */ - handleReceiveWaitData (data) { - for (let i = 0; i < data.length; i++) { - const byte = data[i] - if (byte === SOH || byte === STX) { - // Remote started sending a packet - this.clearReceiveTimeout() - this.state = XMODEM_STATE.RECEIVING - this.receiveBuffer = Buffer.alloc(0) - this.handleReceiveData(data.subarray(i)) - return true - } - } - // No SOH/STX found yet - still waiting - return true - } - - /** - * Handle data while waiting for remote to request file (send mode) - */ - handleSendWaitData (data) { - for (let i = 0; i < data.length; i++) { - const byte = data[i] - if (byte === NAK) { - // Remote requests checksum mode - this.clearReceiveTimeout() - this.useCrc = false - this.retries = 0 - this.sendNextPacket() - return true - } else if (byte === CRC) { - // Remote requests CRC mode - this.clearReceiveTimeout() - this.useCrc = true - this.retries = 0 - this.sendNextPacket() - return true - } - } - return true - } - - /** - * Handle data during active receive - */ - handleReceiveData (data) { - this.clearReceiveTimeout() - this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]) - - // Try to parse a complete packet - while (this.receiveBuffer.length > 0) { - const firstByte = this.receiveBuffer[0] - - if (firstByte === EOT) { - // End of transmission - this.receiveBuffer = this.receiveBuffer.subarray(1) - this.handleReceiveComplete() - return - } - - if (firstByte === CAN) { - // Remote cancelled - this.sendToClient({ - event: 'session-error', - error: 'Remote cancelled transfer' - }) - this.endSession() - return - } - - if (firstByte !== SOH && firstByte !== STX) { - // Skip non-protocol bytes - this.receiveBuffer = this.receiveBuffer.subarray(1) - continue - } - - const packetSize = firstByte === SOH ? PACKET_SIZE_128 : PACKET_SIZE_1K - const totalPacketSize = HEADER_SIZE + packetSize + (this.useCrc ? CRC_TRAILER_SIZE : CHECKSUM_TRAILER_SIZE) - - if (this.receiveBuffer.length < totalPacketSize) { - // Not enough data yet, wait for more - break - } - - const packet = this.receiveBuffer.subarray(0, totalPacketSize) - this.receiveBuffer = this.receiveBuffer.subarray(totalPacketSize) - - this.processReceivedPacket(firstByte, packet) - } - - // Reset timeout for next packet - this.resetReceiveTimeout() - } - - /** - * Process a received XMODEM packet - */ - processReceivedPacket (headerByte, packet) { - const blockNum = packet[1] - const blockNumInv = packet[2] - const dataStart = HEADER_SIZE - const dataEnd = dataStart + (headerByte === SOH ? PACKET_SIZE_128 : PACKET_SIZE_1K) - const data = packet.subarray(dataStart, dataEnd) - - // Validate block number complement - if ((blockNum ^ blockNumInv) !== 0xFF) { - log.warn('XMODEM: block number complement mismatch') - this.sendNak() - return - } - - // Validate CRC or checksum - if (this.useCrc) { - const receivedCrc = (packet[dataEnd] << 8) | packet[dataEnd + 1] - const calculatedCrc = crc16Xmodem(data) - if (receivedCrc !== calculatedCrc) { - log.warn('XMODEM: CRC mismatch') - this.sendNak() - return - } - } else { - let checksum = 0 - for (let i = 0; i < data.length; i++) { - checksum = (checksum + data[i]) & 0xFF - } - if (checksum !== packet[dataEnd]) { - log.warn('XMODEM: checksum mismatch') - this.sendNak() - return - } - } - - // Validate block sequence - if (blockNum !== (this.expectedBlock & 0xFF)) { - // Could be a retransmit of the previous block - if (blockNum === ((this.expectedBlock - 1) & 0xFF)) { - // Retransmit - just ACK it - this.sendAck() - return - } - log.warn(`XMODEM: expected block ${this.expectedBlock & 0xFF}, got ${blockNum}`) - this.sendCan() - this.endSession() - return - } - - // Valid packet - write data - if (!this.downloadStream) { - this.prepareReceiveFile() - } - - if (this.downloadStream) { - this.downloadStream.write(data) - this.transferredBytes += data.length - this.sendProgress() - } - - this.expectedBlock++ - this.retries = 0 - this.sendAck() - } - - /** - * Prepare to receive file - */ - prepareReceiveFile () { - if (!this.savePath) return - - // Use original filename if provided, otherwise generate one - const fileName = this.receiveFileName || `xmodem_${Date.now()}.bin` - let filePath = path.join(this.savePath, sanitizeFilename(fileName)) - - if (fs.existsSync(filePath)) { - filePath = filePath + '.' + generate() - } - - this.downloadPath = filePath - this.downloadStream = fs.createWriteStream(filePath, { - highWaterMark: 64 * 1024 - }) - this.transferredBytes = 0 - this.startTime = Date.now() - - this.sendToClient({ - event: 'file-start', - name: fileName, - size: 0 // XMODEM doesn't know size upfront - }) - } - - /** - * Handle receive complete (EOT received) - */ - handleReceiveComplete () { - // Send ACK for EOT - this.writeToTerminal(Buffer.from([ACK])) - - if (this.downloadStream) { - this.downloadStream.end() - this.downloadStream = null - } - - this.sendProgress() - - this.sendToClient({ - event: 'file-complete', - name: path.basename(this.downloadPath || ''), - path: this.downloadPath - }) - - this.sendToClient({ - event: 'session-end' - }) - - this.resetState() - } - - /** - * Send ACK to remote - */ - sendAck () { - this.writeToTerminal(Buffer.from([ACK])) - } - - /** - * Send NAK to remote - */ - sendNak () { - this.retries++ - if (this.retries > MAX_RETRIES) { - log.error('XMODEM: max retries exceeded') - this.sendCan() - this.endSession() - return - } - this.writeToTerminal(Buffer.from([NAK])) - this.resetReceiveTimeout() - } - - /** - * Send CAN (cancel) to remote - */ - sendCan () { - this.writeToTerminal(Buffer.from([CAN, CAN, CAN, CAN, CAN])) - } - - /** - * Reset receive timeout - */ - resetReceiveTimeout () { - this.clearReceiveTimeout() - this.receiveTimeout = setTimeout(() => { - if (this.state === XMODEM_STATE.RECEIVING) { - this.sendNak() - } else if (this.state === XMODEM_STATE.WAITING_REMOTE) { - this.retries++ - if (this.retries > MAX_RETRIES) { - this.sendToClient({ - event: 'session-error', - error: 'Timeout waiting for remote' - }) - this.endSession() - return - } - // In receive mode, send NAK to prompt remote to start - // In send mode, do nothing - remote must initiate - if (this.pendingFiles.length === 0) { - this.writeToTerminal(Buffer.from([NAK])) - } - this.resetReceiveTimeout() - } - }, RECEIVE_TIMEOUT_MS) - } - - /** - * Clear receive timeout - */ - clearReceiveTimeout () { - if (this.receiveTimeout) { - clearTimeout(this.receiveTimeout) - this.receiveTimeout = null - } - } - - /** - * Set save path for receiving files - */ - setSavePath (savePath, name) { - this.savePath = savePath - this.receiveFileName = name || null - // Process buffered data - if (this.receiveBuffer.length > 0) { - const buffered = this.receiveBuffer - this.receiveBuffer = Buffer.alloc(0) - this.state = XMODEM_STATE.RECEIVING - this.handleReceiveData(buffered) - } else { - this.state = XMODEM_STATE.WAITING_REMOTE - this.resetReceiveTimeout() - } - } - - /** - * Set files to send - */ - setSendFiles (files) { - this.pendingFiles = files - this.currentFileIndex = 0 - - // Process any buffered data (may contain NAK/C from remote) - if (this.pendingSendData.length > 0) { - for (const data of this.pendingSendData) { - this.handleSendWaitData(data) - } - this.pendingSendData = [] - } - - // If we already detected NAK/C and are ready to send, start - if (files.length > 0 && this.state === XMODEM_STATE.WAITING_REMOTE) { - // Remote hasn't sent NAK/C yet, keep waiting - this.resetReceiveTimeout() - } - } - - /** - * Send next packet to remote - */ - sendNextPacket () { - if (this.currentFileIndex >= this.pendingFiles.length) { - // All files sent - this.sendEot() - return - } - - const file = this.pendingFiles[this.currentFileIndex] - - // Open file if not already open - if (!this.uploadFd) { - try { - this.uploadFd = fs.openSync(file.path, 'r') - this.sendSize = file.size - this.sentBytes = 0 - this.sendBlock = 1 - - this.currentTransfer = { - name: file.name, - size: file.size - } - this.transferSize = file.size - this.transferredBytes = 0 - this.startTime = Date.now() - - this.sendToClient({ - event: 'file-start', - name: file.name, - size: file.size - }) - } catch (e) { - log.error('XMODEM: failed to open file', e) - this.sendCan() - this.endSession() - return - } - } - - // Read next chunk - const packetSize = this.use1K ? PACKET_SIZE_1K : PACKET_SIZE_128 - const remaining = this.sendSize - this.sentBytes - - if (remaining <= 0) { - // File done, send EOT - fs.closeSync(this.uploadFd) - this.uploadFd = null - this.sendEot() - return - } - - const readSize = Math.min(packetSize, remaining) - const buf = Buffer.alloc(packetSize) // Pad with 0x1A (SUB) if needed - buf.fill(0x1A) // XMODEM pads with SUB (0x1A) - - try { - fs.readSync(this.uploadFd, buf, 0, readSize, this.sentBytes) - } catch (e) { - log.error('XMODEM: failed to read file', e) - this.sendCan() - this.endSession() - return - } - - // Build packet - const headerByte = this.use1K ? STX : SOH - const blockNum = this.sendBlock & 0xFF - const packet = Buffer.alloc(HEADER_SIZE + packetSize + (this.useCrc ? CRC_TRAILER_SIZE : CHECKSUM_TRAILER_SIZE)) - - packet[0] = headerByte - packet[1] = blockNum - packet[2] = blockNum ^ 0xFF - buf.copy(packet, HEADER_SIZE, 0, packetSize) - - if (this.useCrc) { - const crc = crc16Xmodem(buf.subarray(0, packetSize)) - packet[HEADER_SIZE + packetSize] = (crc >> 8) & 0xFF - packet[HEADER_SIZE + packetSize + 1] = crc & 0xFF - } else { - let checksum = 0 - for (let i = 0; i < packetSize; i++) { - checksum = (checksum + buf[i]) & 0xFF - } - packet[HEADER_SIZE + packetSize] = checksum - } - - this.writeToTerminal(packet) - this.state = XMODEM_STATE.SENDING - this.sentBytes += readSize - this.transferredBytes = this.sentBytes - - // Progress update - const now = Date.now() - if (!this.lastProgressUpdate || now - this.lastProgressUpdate > PROGRESS_INTERVAL_MS) { - this.lastProgressUpdate = now - this.sendProgress() - } - - // Timeout waiting for ACK - this.resetSendTimeout() - } - - /** - * Handle response during send (ACK/NAK/CAN) - */ - handleSendResponse (data) { - this.clearSendTimeout() - - for (let i = 0; i < data.length; i++) { - const byte = data[i] - - if (byte === ACK) { - // Block acknowledged - this.sendBlock++ - this.retries = 0 - this.sendNextPacket() - return - } else if (byte === NAK) { - // Retransmit current block - this.retries++ - if (this.retries > MAX_RETRIES) { - log.error('XMODEM: max retries exceeded during send') - this.sendCan() - this.endSession() - return - } - // Re-read and resend the same block (keep sendBlock unchanged – same block# must be retransmitted) - this.sentBytes -= (this.use1K ? PACKET_SIZE_1K : PACKET_SIZE_128) - if (this.sentBytes < 0) this.sentBytes = 0 - this.sendNextPacket() - return - } else if (byte === CAN) { - // Remote cancelled - this.sendToClient({ - event: 'session-error', - error: 'Remote cancelled transfer' - }) - this.endSession() - return - } - } - } - - /** - * Send EOT (end of transmission) - */ - sendEot () { - // Guard: if already past SENDING (e.g. called again via ACK→sendNextPacket - // after we already sent EOT), skip to avoid infinite EOT loop. - if (this.state !== XMODEM_STATE.SENDING && - this.state !== XMODEM_STATE.WAITING_REMOTE) { - return - } - - this.writeToTerminal(Buffer.from([EOT])) - - // Wait for ACK of EOT - this.resetSendTimeout() - - // Send final progress - if (this.currentTransfer) { - this.transferredBytes = this.transferSize - this.sendProgress() - } - - this.sendToClient({ - event: 'file-complete', - name: this.currentTransfer?.name, - path: this.uploadPath - }) - - this.sendToClient({ - event: 'session-end' - }) - - // Reset state so isActive() returns false and normal terminal I/O resumes. - // This mirrors handleReceiveComplete() which also calls resetState() after - // sending session-end. - this.resetState() - } - - /** - * Reset send timeout - */ - resetSendTimeout () { - this.clearSendTimeout() - this.sendTimeout = setTimeout(() => { - if (this.state === XMODEM_STATE.SENDING) { - this.retries++ - if (this.retries > MAX_RETRIES) { - this.sendToClient({ - event: 'session-error', - error: 'Timeout waiting for ACK' - }) - this.endSession() - return - } - // Resend EOT if we already sent it, otherwise resend packet - if (this.sentBytes >= this.sendSize) { - this.writeToTerminal(Buffer.from([EOT])) - } else { - // Resend current block (keep sendBlock unchanged – same block# must be retransmitted) - this.sentBytes -= (this.use1K ? PACKET_SIZE_1K : PACKET_SIZE_128) - if (this.sentBytes < 0) this.sentBytes = 0 - this.sendNextPacket() - } - this.resetSendTimeout() - } - }, SEND_ACK_TIMEOUT_MS) - } - - /** - * Clear send timeout - */ - clearSendTimeout () { - if (this.sendTimeout) { - clearTimeout(this.sendTimeout) - this.sendTimeout = null - } - } - - /** - * Send progress update to client - */ - sendProgress () { - const elapsed = (Date.now() - this.startTime) / 1000 - const speed = elapsed > 0 ? Math.round(this.transferredBytes / elapsed) : 0 - const percent = this.transferSize > 0 - ? Math.floor(this.transferredBytes * 100 / this.transferSize) - : 0 - - this.sendToClient({ - event: 'progress', - name: this.currentTransfer?.name, - size: this.transferSize, - transferred: this.transferredBytes, - percent, - speed, - type: this.state === XMODEM_STATE.RECEIVING ? 'download' : 'upload', - path: this.state === XMODEM_STATE.RECEIVING ? this.downloadPath : this.uploadPath - }) - } - - /** - * Cancel transfer - */ - cancel () { - this.sendCan() - this.endSession() - } - - /** - * End session and reset state - */ - endSession () { - this.clearReceiveTimeout() - this.clearSendTimeout() - - if (this.downloadStream) { - try { this.downloadStream.end() } catch (e) { log.error('Error closing download stream', e) } - this.downloadStream = null - } - - if (this.uploadFd) { - try { fs.closeSync(this.uploadFd) } catch (e) { log.error('Error closing upload file', e) } - this.uploadFd = null - } - - if (this.currentTransfer) { - this.transferredBytes = this.transferSize - this.sendProgress() - } - - this.sendToClient({ event: 'session-end' }) - this.resetState() - } - - /** - * Reset session state - */ - resetState () { - this.clearReceiveTimeout() - this.clearSendTimeout() - this.state = XMODEM_STATE.IDLE - this.downloadStream = null - this.downloadPath = null - this.savePath = null - this.receiveFileName = null - this.expectedBlock = 1 - this.receiveBuffer = Buffer.alloc(0) - this.retries = 0 - this.uploadPath = null - this.uploadFd = null - this.sendBlock = 1 - this.sendSize = 0 - this.sentBytes = 0 - this.currentTransfer = null - this.transferSize = 0 - this.transferredBytes = 0 - this.startTime = 0 - this.pendingFiles = [] - this.currentFileIndex = 0 - this.pendingSendData = [] - this.lastProgressUpdate = 0 - } - - /** - * Check if session is active - */ - isActive () { - return this.state !== XMODEM_STATE.IDLE - } - - /** - * Clean up resources - */ - destroy () { - this.endSession() - this.term = null - this.ws = null - } -} - -/** - * XmodemManager manages XMODEM sessions for multiple terminals - */ -class XmodemManager { - constructor () { - this.sessions = new Map() - } - - getSession (pid, term, ws) { - if (!this.sessions.has(pid)) { - this.sessions.set(pid, new XmodemSession(term, ws)) - } - return this.sessions.get(pid) - } - - /** - * Handle data for a terminal - * @returns {boolean} true if data was consumed - */ - handleData (pid, data, term, ws) { - const session = this.getSession(pid, term, ws) - return session.handleData(data) - } - - /** - * Handle client message - */ - handleMessage (pid, msg, term, ws) { - const session = this.getSession(pid, term, ws) - - switch (msg.event) { - case 'set-save-path': - session.setSavePath(msg.path, msg.name) - break - case 'send-files': - session.setSendFiles(msg.files) - break - case 'cancel': - session.cancel() - break - case 'start-receive': - session.startReceive() - break - case 'start-send': - session.startSend() - break - } - } - - destroySession (pid) { - const session = this.sessions.get(pid) - if (session) { - session.destroy() - this.sessions.delete(pid) - } - } - - isActive (pid) { - const session = this.sessions.get(pid) - return session ? session.isActive() : false - } -} - -const xmodemManager = new XmodemManager() - -export { - XmodemSession, - XmodemManager, - xmodemManager, - XMODEM_STATE -} diff --git a/src/app/server/zmodem.js b/src/app/server/zmodem.js deleted file mode 100644 index f2d659a..0000000 --- a/src/app/server/zmodem.js +++ /dev/null @@ -1,1259 +0,0 @@ -/** - * Zmodem protocol handler for server-side terminal sessions - * Uses zmodem2 (pure JS) for protocol implementation - * - * Design notes: - * - Detection: zmodem headers (** ZDLE B ...) are detected on a small - * carry-over buffer so a header split across pty read chunks is still - * found. `detect()` returns the data unconsumed when no session is - * active, so normal terminal output is never swallowed. - * - ZSKIP (remote refuses a file, e.g. rz aborted on name clash): the - * hex frame is scanned on the same carry buffer. On skip we abort the - * whole batch with the canonical cancel sequence (rz does not offer a - * reliable "next file" path when its UI already exited) and let the - * trailing garbage drain to the terminal - that is the shell/rz error - * output the user needs to see. - * - Watchdog: every state transition (awaiting save path / file dialog, - * awaiting protocol reply, mid-transfer) arms a timer. A stalled - * session always ends itself instead of hanging forever with the - * terminal frozen. - * - All cleanup goes through `_cleanup()`. `endSession()` is the single - * public reset path so `isActive()` always returns false after any - * failure, which is what restores normal terminal display. - */ - -import fs from 'fs' -import path from 'path' -import log from '../common/log.js' -import generate from '../common/uid.js' -import sanitizeFilename from '../common/sanitize-filename.js' - -// Import zmodem2 (pure JS, no WASM) -import { Sender, Receiver, SenderEvent, ReceiverEvent } from 'zmodem2' - -// Zmodem state constants -const ZMODEM_STATE = { - IDLE: 'idle', - RECEIVING: 'receiving', - SENDING: 'sending', - WAITING_SAVE_PATH: 'waiting_save_path', - WAITING_FILES: 'waiting_files' -} - -// Zmodem header signature: ** + ZDLE(0x18) + B(0x42) -const ZMODEM_HEADER = Buffer.from([0x2a, 0x2a, 0x18, 0x42]) - -// ZRQINIT = "00" (remote wants to send -> we receive) -const ZRQINIT_HEX = Buffer.from([0x30, 0x30]) -// ZRINIT = "01" (remote ready to receive -> we send) -const ZRINIT_HEX = Buffer.from([0x30, 0x31]) -// ZSKIP = "05" (remote refuses current file) -const ZSKIP_HEX = Buffer.from([0x30, 0x35]) - -// Cancel sequence per ZMODEM spec: 8x CAN (0x18) then "B". Some peers -// only react to the 5-CAN form, 8 covers both. -const CANCEL_SEQUENCE = Buffer.from([0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x42]) - -// Watchdog timeouts (ms) -const WATCHDOG = { - // waiting for user to pick save folder / files - USER_ACTION: 10 * 60 * 1000, - // waiting for protocol reply (ZRINIT after ZRQINIT etc.) - HANDSHAKE: 30 * 1000, - // mid-transfer silence - TRANSFER: 60 * 1000 -} - -// Max bytes buffered while sniffing for a split header or waiting for -// user action. Anything beyond this is treated as a dead/aborted peer. -const MAX_BUFFERED_BYTES = 4 * 1024 * 1024 - -// A peer aborted mid-transfer (user hit Ctrl-C on the remote rz/sz) -// announces it with a run of CAN (0x18) bytes. 4+ in a row can never -// appear in payload data on the wire: every literal 0x18 inside file -// data is ZDLE-escaped, so a raw run is always intentional. -const MIN_CAN_RUN = 4 - -// After a session ends (completion, cancel, or remote abort) the dying -// peer keeps emitting protocol debris for a moment — pty buffer flushes -// can hold several KB of in-flight frames. During this window output is -// swallowed until the shell prompt reappears (or the window expires, so -// display always recovers). -const NOISE_SUPPRESS_MS = 3000 - -/** - * ZmodemSession class handles zmodem file transfers for a terminal session - */ -class ZmodemSession { - constructor (term, ws) { - this.term = term - this.ws = ws - this.state = ZMODEM_STATE.IDLE - this.receiver = null - this.sender = null - this.currentTransfer = null - this.downloadStream = null // Write stream for file download - this.downloadPath = null - this.uploadFd = null // File descriptor for upload read - this.uploadPath = null - this.transferSize = 0 - this.transferredBytes = 0 - this.startTime = 0 - this.lastProgressUpdate = 0 - this.savePath = null - this.pendingFiles = [] - this.currentFileIndex = 0 - this.fileReadPosition = 0 - this.currentMtime = 0 - - // Carry buffer holds wire data while waiting for user action. In idle - // state a separately displayed tail is retained for split-header scans. - this.carry = null - this.idleScanTail = null - this.scanTail = null - this.canTail = null - this.carrySince = 0 - - this.watchdogTimer = null - this.destroyed = false - this._drainTimer = null - - // Set while the dying peer's trailing garbage is still expected on - // the wire; see NOISE_SUPPRESS_MS. - this.suppressNoiseUntil = 0 - this.residueTail = null - } - - // ── watchdog ──────────────────────────────────────────────── - - /** - * (Re)arm the watchdog timer - * @param {number} ms - Timeout in ms - */ - armWatchdog (ms) { - this.disarmWatchdog() - if (this.destroyed || !ms) return - this.watchdogTimer = setTimeout(() => { - if (this.state === ZMODEM_STATE.IDLE) return - log.warn(`zmodem watchdog timeout in state ${this.state}, ending session`) - this.sendToClient({ - event: 'session-timeout', - message: 'ZMODEM session timed out' - }) - this.abort(true) - }, ms) - // Do not keep the event loop alive just for the watchdog - if (this.watchdogTimer.unref) this.watchdogTimer.unref() - } - - disarmWatchdog () { - if (this.watchdogTimer) { - clearTimeout(this.watchdogTimer) - this.watchdogTimer = null - } - } - - // ── client / terminal io ──────────────────────────────────── - - /** - * Send message to client via websocket - * @param {Object} msg - Message to send - */ - sendToClient (msg) { - if (this.ws && this.ws.s && !this.destroyed) { - this.ws.s({ - action: 'zmodem-event', - ...msg - }) - } - } - - /** - * Write data to terminal - * @param {Buffer} data - Data to write - */ - writeToTerminal (data) { - if (this.term && this.term.write) { - this.term.write(data) - } - } - - // ── detection ─────────────────────────────────────────────── - - /** - * Scan a buffer for a zmodem start/skip header starting at or after `from` - * @param {Buffer} data - * @param {number} from - * @returns {Object|null} { kind: 'receive'|'send'|'skip', offset: number } - */ - scanBuffer (data, from = 0) { - const end = data.length - ZMODEM_HEADER.length - 1 - for (let i = from; i <= end; i++) { - if ( - data[i] === ZMODEM_HEADER[0] && - data[i + 1] === ZMODEM_HEADER[1] && - data[i + 2] === ZMODEM_HEADER[2] && - data[i + 3] === ZMODEM_HEADER[3] - ) { - const h1 = data[i + 4] - const h2 = data[i + 5] - if (h1 === ZRQINIT_HEX[0] && h2 === ZRQINIT_HEX[1]) { - return { kind: 'receive', offset: i } - } - if (h1 === ZRINIT_HEX[0] && h2 === ZRINIT_HEX[1]) { - return { kind: 'send', offset: i } - } - if (h1 === ZSKIP_HEX[0] && h2 === ZSKIP_HEX[1]) { - return { kind: 'skip', offset: i } - } - } - } - return null - } - - /** - * Check whether the tail of `data` could be the beginning of a header - * that continues in the next chunk (e.g. "*\x18B0" waiting for its - * final type nibble). Returns the partial length, or 0. - * @param {Buffer} data - * @returns {number} - */ - partialHeaderLength (data) { - const max = Math.min(data.length, ZMODEM_HEADER.length + 1) - for (let len = max; len > 0; len--) { - let ok = true - for (let j = 0; j < len; j++) { - if (data[data.length - len + j] !== ZMODEM_HEADER[j]) { - ok = false - break - } - } - if (ok) return len - } - return 0 - } - - /** - * Find a raw run of CAN (0x18) bytes, the peer's abort signal. - * Scans across chunk boundaries via its own tail buffer (independent - * of the ZSKIP scanTail). Escaped 0x18 in file data always arrives as - * ZDLE-escaped pairs, so a raw MIN_CAN_RUN run is unambiguous. - * @param {Buffer} data - * @returns {boolean} - */ - hasCanRun (data) { - const hay = this.canTail ? Buffer.concat([this.canTail, data]) : data - this.canTail = Buffer.from(hay.subarray(Math.max(0, hay.length - (MIN_CAN_RUN - 1)))) - let run = 0 - for (const b of hay) { - if (b === 0x18) { - run++ - if (run >= MIN_CAN_RUN) return true - } else { - run = 0 - } - } - return false - } - - // ── data entry point ──────────────────────────────────────── - - /** - * Handle incoming data from terminal - * @param {Buffer} data - Incoming data - * @returns {boolean} - True if data was consumed by zmodem - */ - /** - * Observe user keystrokes on their way to the pty (called by the - * session-server before writing terminal input). A Ctrl-C (ETX) - * while a transfer is running means the remote rz/sz is about to be - * killed by SIGINT: end the session right away so (a) we stop - * feeding protocol frames into the shell that takes over the pty - * (they would be echoed back as garbage text) and (b) terminal - * output resumes immediately instead of after the watchdog timeout. - * The keystroke itself is never swallowed - normal shell behavior is - * untouched. - * @param {string|Buffer} data - User input about to reach the pty - */ - handleUserInput (data) { - if (this.destroyed || this.state === ZMODEM_STATE.IDLE) return - if (typeof data === 'string' ? !data.includes('\x03') : !data.includes(3)) return - log.debug('zmodem: user pressed Ctrl-C during transfer, ending session') - this.sendToClient({ - event: 'transfer-error', - message: 'Transfer interrupted (Ctrl-C)' - }) - // No cancel sequence: the remote program is dying / dead already; - // writing ours would just be echoed by the shell as garbage. - this.abort(false) - } - - handleData (data) { - if (this.destroyed) return false - if (!Buffer.isBuffer(data)) data = Buffer.from(data) - - switch (this.state) { - case ZMODEM_STATE.IDLE: - return this.handleIdleData(data) - - case ZMODEM_STATE.WAITING_SAVE_PATH: - case ZMODEM_STATE.WAITING_FILES: - this.bufferPending(data) - return true - - case ZMODEM_STATE.RECEIVING: - // A raw CAN run means the remote rz was killed (Ctrl-C). The - // state machine would ignore those bytes and stall until the - // watchdog, so detect and end immediately - silently: writing - // our own cancel sequence back would only echo more garbage - // from the shell that now owns the pty. - if (this.hasCanRun(data)) { - log.debug('zmodem: remote sent CAN run, session aborted by peer') - this.sendToClient({ - event: 'transfer-error', - message: 'Transfer aborted by remote side' - }) - this.abort(false) - return true - } - this.armWatchdog(WATCHDOG.TRANSFER) - this.handleReceiverData(data) - return true - - case ZMODEM_STATE.SENDING: - if (this.hasCanRun(data)) { - log.debug('zmodem: remote sent CAN run, session aborted by peer') - this.sendToClient({ - event: 'transfer-error', - message: 'Transfer aborted by remote side' - }) - this.abort(false) - return true - } - this.armWatchdog(WATCHDOG.TRANSFER) - this.handleSenderData(data) - return true - - default: - return false - } - } - - /** - * State: idle. Look for the start of a session. - * - * Contract with session-server: returning false means "not zmodem, - * send the chunk to the client yourself"; returning true means "mine, - * already forwarded anything displayable via ws". While sniffing a - * split header we own the output so nothing is double-sent. - * @returns {boolean} - */ - handleIdleData (data) { - // Post-session noise suppression. After a transfer dies mid-flight - // (user Ctrl-C etc.) the kernel pty buffers flush up to several KB - // of in-flight protocol data, and the shell echoes back frames it - // swallowed as input — screens worth of garbage. Printable-ness is - // NOT a usable filter here (file payloads and hex frames are pure - // printable text), so during the window we swallow EVERYTHING and - // only resume display once the shell prompt reappears: a short - // printable line ending in a prompt char ($ # > %). The window has - // a hard cap so display always recovers even if no prompt is ever - // detected (plain sh, unusual PS1). - if (Date.now() < this.suppressNoiseUntil) { - // Match against remembered tail + new data so a prompt split - // across chunks is still recognized - const hay = this.residueTail - ? this.residueTail + data.toString('utf8') - : data.toString('utf8') - // A prompt: a fresh line (or chunk start) of short printable - // text ending in a prompt char ($ # > %) right at the end. - const m = hay.match(/(?:^|[\r\n])([^\r\n]{1,80})[\x20\t]*$/) - const promptish = m !== null && /[\x24#>%»]\s?$/.test(m[1]) - if (promptish) { - // prompt reappeared: show just the prompt line, close the window - this.passThroughPrefix(Buffer.from(m[0], 'utf8')) - this.suppressNoiseUntil = 0 - this.residueTail = null - return true - } - if (!this.looksLikeNoise(data)) { - // keep a printable tail for the cross-chunk match above - const keep = Math.min(hay.length, 160) - this.residueTail = hay.slice(hay.length - keep) - } else { - this.residueTail = null - } - // Debris still flowing past the window cap: keep swallowing - // (extend) — a large pty-buffer flush can outlast one window. - if (Date.now() + 50 >= this.suppressNoiseUntil) { - this.suppressNoiseUntil = Date.now() + NOISE_SUPPRESS_MS - } - return true - } - this.residueTail = null - - // Search the previous idle chunk tail + data so a header split across - // chunks is found. Unlike an unconfirmed carry, that tail has already - // been displayed. Keeping it separately avoids withholding ordinary - // terminal echo such as repeated `*` characters while still allowing a - // ZMODEM header to be recognized across a chunk boundary. - const previousTail = this.idleScanTail - const hay = previousTail ? Buffer.concat([previousTail, data]) : data - this.idleScanTail = null - const alreadySentLength = previousTail ? previousTail.length : 0 - - const hit = this.scanBuffer(hay, 0) - if (hit) { - if (hit.kind === 'skip') { - // ZSKIP with no session in flight is stray noise - drop it - return true - } - // Output before the header (e.g. "rz waiting to receive.\r\n") stays - // visible. Bytes from the previous tail were already sent and must not - // be duplicated. - if (hit.offset > alreadySentLength) { - this.passThroughPrefix(hay.subarray(alreadySentLength, hit.offset)) - } - this.startSessionFromHit(hit.kind, hay.subarray(hit.offset)) - return true - } - - // No full header. Remember a possible header prefix for the next chunk, - // but let session-server forward this chunk immediately. - const partial = this.partialHeaderLength(hay) - this.idleScanTail = partial > 0 - ? Buffer.from(hay.subarray(hay.length - partial)) - : null - return false - } - - /** - * Emit pre-header terminal output back to the client. Called via the - * same ws path the session-server would have used. - * @param {Buffer} prefix - */ - passThroughPrefix (prefix) { - if (!prefix || !prefix.length || !this.ws || !this.ws.send) return - // Only forward readable output; drop protocol noise that would - // corrupt the display. - if (this.looksLikeNoise(prefix)) return - try { - this.ws.send(prefix) - } catch (e) { - // ws closed - nothing to do - } - } - - /** - * Heuristic: buffers that are mostly control bytes / non-printable - * are zmodem line noise, not something to display. - * @param {Buffer} buf - * @returns {boolean} - */ - looksLikeNoise (buf) { - if (!buf.length) return true - let printable = 0 - for (const b of buf) { - // CR LF TAB ESC BEL BS and visible ASCII count as displayable - if (b === 0x0d || b === 0x0a || b === 0x09 || b === 0x1b || b === 0x07 || b === 0x08 || (b >= 0x20 && b !== 0x7f)) printable++ - } - return printable / buf.length < 0.5 - } - - /** - * Begin a receiver or sender session based on detected frame kind - * @param {string} kind - 'receive' | 'send' | 'skip' - * @param {Buffer} rest - Data from the header onwards - */ - startSessionFromHit (kind, rest) { - if (kind === 'receive') { - this.startReceiver(rest) - } else if (kind === 'send') { - this.startSender(rest) - } - } - - /** - * Buffer data while waiting for user action, with sanity limits - * @param {Buffer} data - */ - bufferPending (data) { - this.carry = this.carry ? Buffer.concat([this.carry, data]) : Buffer.from(data) - if (!this.carrySince) this.carrySince = Date.now() - if (this.carry.length > MAX_BUFFERED_BYTES) { - log.warn('zmodem: peer flooded the session while waiting for user action, aborting') - this.abort(true) - } - } - - // ── receive (download) ────────────────────────────────────── - - /** - * Start a receive session (remote is sending file(s)) - * @param {Buffer} initialData - Initial zmodem data - */ - startReceiver (initialData) { - try { - this.receiver = new Receiver() - this.transferredBytes = 0 - this.currentMtime = 0 - this.carry = initialData && initialData.length - ? Buffer.from(initialData) - : null - this.carrySince = Date.now() - this.state = ZMODEM_STATE.WAITING_SAVE_PATH - - this.sendToClient({ - event: 'receive-start', - message: 'ZMODEM receive session started' - }) - this.armWatchdog(WATCHDOG.USER_ACTION) - } catch (e) { - log.error('Failed to start zmodem receiver', e) - this.abort(true) - } - } - - /** - * Set save path for receiving files, then replay buffered wire data - * @param {string} savePath - Directory path to save files - */ - setSavePath (savePath) { - if (this.state !== ZMODEM_STATE.WAITING_SAVE_PATH) return - this.savePath = savePath - this.state = ZMODEM_STATE.RECEIVING - this.armWatchdog(WATCHDOG.TRANSFER) - - const pending = this.carry || Buffer.alloc(0) - this.carry = null - if (pending.length) { - this.handleReceiverData(pending) - } - } - - /** - * Feed wire data to the receiver state machine and pump outputs - * @param {Buffer} data - */ - handleReceiverData (data) { - if (!this.receiver) return - const u8 = Buffer.isBuffer(data) ? new Uint8Array(data) : new Uint8Array(Buffer.from(data)) - let offset = 0 - let iterations = 0 - - while (offset < u8.length && iterations++ < 1000 && this.receiver) { - try { - const consumed = this.receiver.feedIncoming(u8.subarray(offset)) - offset += consumed - const drained = this.pumpReceiver() - if (consumed === 0 && !drained) break - } catch (e) { - log.error('Zmodem receiver error:', e) - this.sendToClient({ - event: 'transfer-error', - message: 'ZMODEM protocol error during receive' - }) - this.abort(true) - return - } - } - } - - /** - * Drain receiver outputs: wire replies, events, file data - * @returns {boolean} - True if work was done - */ - pumpReceiver () { - if (!this.receiver) return false - let didWork = false - - try { - // Order matters: drain file data FIRST. finishSubpacket (triggered - // by drainFile/advanceFile) queues ZACK replies; draining outgoing - // last flushes them in the same pump. With the opposite order a - // trailing ZACK stays queued when input runs out, and the peer - // stalls forever waiting for its ack. - const chunk = this.receiver.drainFile() - if (chunk && chunk.length > 0) { - this.handleFileData(Buffer.from(chunk)) - this.receiver.advanceFile(chunk.length) - didWork = true - } - - let event - while ((event = this.receiver.pollEvent()) !== null) { - didWork = true - if (event === ReceiverEvent.FileStart) { - this.handleFileStart( - this.receiver.getFileName(), - this.receiver.getFileSize(), - this.receiver.getFileMtime() - ) - } else if (event === ReceiverEvent.FileComplete) { - this.handleFileComplete() - } else if (event === ReceiverEvent.SessionComplete) { - // Drain the queued ZFIN ack BEFORE resetting: the remote - // waits for it to leave state 6 and print its exit message. - const finalReply = this.receiver.drainOutgoing() - if (finalReply && finalReply.length > 0) { - this.writeToTerminal(Buffer.from(finalReply)) - } - this.endSession(true) - return true - } - } - - const outgoing = this.receiver.drainOutgoing() - if (outgoing && outgoing.length > 0) { - this.writeToTerminal(Buffer.from(outgoing)) - didWork = true - } - } catch (e) { - log.error('Zmodem receiver pump error:', e) - this.abort(true) - return false - } - return didWork - } - - /** - * Handle file start event - */ - handleFileStart (name, size, mtime) { - this.currentTransfer = { name, size } - this.transferSize = size - this.transferredBytes = 0 - this.currentMtime = mtime || 0 - this.lastProgressUpdate = 0 - this.prepareReceiveFile(name, size) - this.sendToClient({ event: 'file-start', name, size }) - } - - /** - * Create the output file write stream - * @param {string} name - * @param {number} size - */ - prepareReceiveFile (name, size) { - try { - let filePath = path.join(this.savePath, sanitizeFilename(name)) - - // Avoid clobbering an existing file - if (fs.existsSync(filePath)) { - filePath = `${filePath}.${generate()}` - } - - this.downloadPath = filePath - const stream = fs.createWriteStream(filePath, { - highWaterMark: 64 * 1024 - }) - // A failed write (disk full, permission) must end the session - // instead of leaking a broken stream. - stream.on('error', (e) => { - log.error('zmodem download stream error', e) - this.sendToClient({ - event: 'transfer-error', - message: `Failed to write ${filePath}: ${e.message}` - }) - this.downloadStream = null - this.abort(true) - }) - this.downloadStream = stream - - this.sendToClient({ - event: 'file-prepared', - name, - path: filePath, - size - }) - } catch (e) { - log.error('Failed to prepare receive file', e) - this.abort(true) - } - } - - /** - * Handle file data chunk - * @param {Buffer} data - */ - handleFileData (data) { - if (!this.downloadStream || !this.currentTransfer) return - - if (this.transferredBytes === 0) { - this.startTime = Date.now() - } - - this.downloadStream.write(data) - this.transferredBytes += data.length - - const now = Date.now() - if (now - this.lastProgressUpdate > 500) { - this.lastProgressUpdate = now - this.sendProgress() - } - } - - /** - * Handle file complete event - */ - handleFileComplete () { - const filePath = this.downloadPath - const fileMtime = this.currentMtime - const currentTransfer = this.currentTransfer - - // Notify the client immediately: the protocol has all bytes, and the - // ZFIN handshake (session-end) often wins the race against the - // stream's async finish event, which would otherwise report the - // transfer complete only after the session already closed. - this.sendToClient({ - event: 'file-complete', - name: currentTransfer?.name, - path: filePath - }) - this.currentTransfer = null - this.downloadPath = null - this.currentMtime = 0 - - const finalize = () => { - if (filePath && fileMtime > 0) { - try { - const mtimeDate = new Date(fileMtime) - fs.utimesSync(filePath, mtimeDate, mtimeDate) - } catch (e) { - log.error('Failed to set file modification time', e) - } - } - } - - if (this.downloadStream) { - const stream = this.downloadStream - this.downloadStream = null - stream.on('finish', finalize) - stream.on('error', () => {}) // error path already handled above - stream.end() - } else { - finalize() - } - } - - // ── send (upload) ─────────────────────────────────────────── - - /** - * Start a send session (remote is ready to receive file(s)) - * @param {Buffer} initialData - Initial zmodem data (contains ZRINIT) - */ - startSender (initialData) { - try { - this.state = ZMODEM_STATE.WAITING_FILES - // Non-initiator: remote sent ZRINIT first - this.sender = new Sender(false) - this.carry = initialData && initialData.length - ? Buffer.from(initialData) - : null - this.carrySince = Date.now() - - this.sendToClient({ - event: 'send-start', - message: 'ZMODEM send session started, please select files' - }) - this.armWatchdog(WATCHDOG.USER_ACTION) - } catch (e) { - log.error('Failed to start zmodem sender', e) - this.abort(true) - } - } - - /** - * Feed wire data to the sender state machine and pump outputs. - * Also watches for ZSKIP (remote refuses the current file). - * @param {Buffer} data - */ - handleSenderData (data) { - if (!this.sender) return - const u8 = Buffer.isBuffer(data) ? new Uint8Array(data) : new Uint8Array(Buffer.from(data)) - - // ZSKIP scan: the zmodem2 Sender ignores unknown frames, so without - // this a "rz: file exists" abort would hang forever. Scan across - // chunk boundaries by prepending the tail of the previous chunk. - const hay = this.scanTail ? Buffer.concat([this.scanTail, data]) : data - this.scanTail = Buffer.from(hay.subarray(Math.max(0, hay.length - (ZMODEM_HEADER.length + 1)))) - const skip = this.scanBuffer(hay, 0) - if (skip && skip.kind === 'skip') { - log.debug('zmodem: ZSKIP received, remote refused file') - this.handleFileSkipped() - return - } - - let offset = 0 - let iterations = 0 - while (offset < u8.length && iterations++ < 1000 && this.sender) { - try { - const consumed = this.sender.feedIncoming(u8.subarray(offset)) - offset += consumed - const drained = this.pumpSender() - if (consumed === 0 && !drained) break - } catch (e) { - log.error('Zmodem sender error:', e) - this.sendToClient({ - event: 'transfer-error', - message: 'ZMODEM protocol error during send' - }) - this.abort(true) - return - } - } - } - - /** - * Handle ZSKIP: remote refused the file. rz has usually exited by - * now, so abort the batch cleanly and let trailing output drain. - */ - handleFileSkipped () { - this.sendToClient({ - event: 'file-skipped', - name: this.currentTransfer?.name, - message: 'Skipped by remote side (file exists or refused)' - }) - - // Notify the remote we are done, then tear down. Subsequent pty - // output (shell prompt / rz error text) goes back to the terminal - // because isActive() is false again. - this.writeToTerminal(CANCEL_SEQUENCE) - this.abort(false) - } - - /** - * Drain sender outputs: wire data, events, file read requests - * @returns {boolean} - */ - pumpSender () { - if (!this.sender) return false - let didWork = false - - try { - const outgoing = this.sender.drainOutgoing() - if (outgoing && outgoing.length > 0) { - this.writeToTerminal(Buffer.from(outgoing)) - didWork = true - } - - let event - while ((event = this.sender.pollEvent()) !== null) { - didWork = true - if (event === SenderEvent.FileComplete) { - this.handleSendFileComplete() - } else if (event === SenderEvent.SessionComplete) { - this.endSession(true) - return true - } - } - - const request = this.sender.pollFile() - if (request !== null) { - this.sendFileData(request.offset, request.len) - didWork = true - } - } catch (e) { - log.error('Zmodem sender pump error', e) - this.abort(true) - return false - } - - return didWork - } - - /** - * Read file data at offset and feed it to the sender - * @param {number} offset - File offset - * @param {number} length - Data length to read - */ - sendFileData (offset, length) { - if (!this.currentTransfer || !this.sender || !this.uploadPath) return - - try { - const CHUNK_SIZE = 64 * 1024 - const readLen = Math.min(length, CHUNK_SIZE) - const data = Buffer.allocUnsafe(readLen) - - if (this.uploadFd === null || this.uploadFd === undefined) { - this.uploadFd = fs.openSync(this.uploadPath, 'r') - this.fileReadPosition = 0 - } - - const bytesRead = readLen > 0 ? fs.readSync(this.uploadFd, data, 0, readLen, offset) : 0 - this.fileReadPosition = offset + bytesRead - const actualData = data.subarray(0, bytesRead) - - if (bytesRead > 0) { - if (this.transferredBytes === 0) { - this.startTime = Date.now() - } - this.sender.feedFile(new Uint8Array(actualData)) - this.transferredBytes = offset + bytesRead - - const now = Date.now() - if (now - this.lastProgressUpdate > 500) { - this.lastProgressUpdate = now - this.sendProgress() - } - - const outgoing = this.sender.drainOutgoing() - if (outgoing && outgoing.length > 0) { - this.writeToTerminal(Buffer.from(outgoing)) - } - } - - if (bytesRead === 0 || offset + bytesRead >= this.currentTransfer.size) { - // Whole file fed. The state machine completes on ZEOF/ZRINIT; - // finishSession() is only for "no more files" (finishSender). - if (this.uploadFd !== null && this.uploadFd !== undefined) { - fs.closeSync(this.uploadFd) - this.uploadFd = null - } - } - } catch (e) { - log.error('Failed to read file data for sending', e) - this.sendToClient({ - event: 'transfer-error', - message: `Failed to read ${this.uploadPath}: ${e.message}` - }) - this.abort(true) - } - } - - /** - * Handle send file complete event - */ - handleSendFileComplete () { - if (this.currentTransfer) { - this.transferredBytes = this.transferSize - this.sendProgress() - } - - this.sendToClient({ - event: 'file-complete', - name: this.currentTransfer?.name, - path: this.uploadPath - }) - - this.currentFileIndex++ - if (this.pendingFiles.length > this.currentFileIndex) { - this.sendFile(this.pendingFiles[this.currentFileIndex]) - } else { - this.finishSender() - } - } - - /** - * Begin sending one file - * @param {Object} file - File info { path, name, size } - */ - sendFile (file) { - if (!this.sender) return - - try { - this.currentTransfer = { name: file.name, size: file.size } - this.transferSize = file.size - this.transferredBytes = 0 - this.uploadPath = file.path - this.fileReadPosition = 0 - this.lastProgressUpdate = 0 - - if (this.uploadFd !== null && this.uploadFd !== undefined) { - fs.closeSync(this.uploadFd) - this.uploadFd = null - } - - // mtime in ms so the remote side preserves modification time - this.sender.startFile(file.name, file.size, file.modifyTime || 0) - - const outgoing = this.sender.drainOutgoing() - if (outgoing && outgoing.length > 0) { - this.writeToTerminal(Buffer.from(outgoing)) - } - - this.sendToClient({ - event: 'file-start', - name: file.name, - size: file.size - }) - } catch (e) { - log.error('Failed to send file', e) - this.abort(true) - } - } - - /** - * Finish sender session after the last file - */ - finishSender () { - if (!this.sender) return - - try { - this.sender.finishSession() - const outgoing = this.sender.drainOutgoing() - if (outgoing && outgoing.length > 0) { - this.writeToTerminal(Buffer.from(outgoing)) - } - this.armWatchdog(WATCHDOG.HANDSHAKE) - } catch (e) { - log.error('Failed to finish zmodem sender session', e) - this.abort(true) - } - } - - /** - * Set files to send and kick off the first transfer - * @param {Array} files - Array of file info objects - */ - setSendFiles (files) { - if (this.state !== ZMODEM_STATE.WAITING_FILES) return - this.pendingFiles = Array.isArray(files) ? files : [] - this.currentFileIndex = 0 - this.state = ZMODEM_STATE.SENDING - this.armWatchdog(WATCHDOG.TRANSFER) - - // Replay the buffered wire data (initial ZRINIT etc.) so the sender - // state machine can transition before we start the first file. - const pending = this.carry || Buffer.alloc(0) - this.carry = null - if (pending.length) { - this.handleSenderData(pending) - } - - if (this.pendingFiles.length > 0) { - this.sendFile(this.pendingFiles[0]) - } else { - this.finishSender() - } - } - - // ── progress ──────────────────────────────────────────────── - - /** - * Send progress update to client - */ - sendProgress () { - const elapsed = (Date.now() - this.startTime) / 1000 - const speed = elapsed > 0 ? Math.round(this.transferredBytes / elapsed) : 0 - const percent = this.transferSize > 0 - ? Math.min(100, Math.floor(this.transferredBytes * 100 / this.transferSize)) - : 100 - - this.sendToClient({ - event: 'progress', - name: this.currentTransfer?.name, - size: this.transferSize, - transferred: this.transferredBytes, - percent, - speed, - type: this.state === ZMODEM_STATE.RECEIVING ? 'download' : 'upload', - path: this.state === ZMODEM_STATE.RECEIVING ? this.downloadPath : this.uploadPath - }) - } - - // ── teardown ──────────────────────────────────────────────── - - /** - * Abort an ongoing transfer: tell the remote, clean up, notify client. - * @param {boolean} sendCancel - Write the cancel sequence to the pty - */ - abort (sendCancel) { - if (sendCancel) { - this.writeToTerminal(CANCEL_SEQUENCE) - } - this.endSession() - } - - /** - * End zmodem session and release every resource. Safe to call twice. - * @param {boolean} clean - True when the protocol closed properly - * (ZFIN handshake): no debris is expected, so the noise-suppression - * window is NOT armed and shell output flows immediately. Abnormal - * ends arm the window to swallow the dying peer's garbage. - */ - endSession (clean = false) { - if (this.downloadStream) { - const stream = this.downloadStream - this.downloadStream = null - stream.destroy() - try { stream.end() } catch (e) { /* already destroyed */ } - } - - if (this.uploadFd !== null && this.uploadFd !== undefined) { - try { - fs.closeSync(this.uploadFd) - } catch (e) { - log.error('Error closing upload file', e) - } - this.uploadFd = null - } - - this.disarmWatchdog() - - // Only an aborted transfer leaves debris on the wire. A clean ZFIN - // close arms nothing, so post-transfer shell output shows instantly. - if (!clean) { - this.suppressNoiseUntil = Date.now() + NOISE_SUPPRESS_MS - } - - this.sendToClient({ event: 'session-end' }) - - this.state = ZMODEM_STATE.IDLE - this.receiver = null - this.sender = null - this.currentTransfer = null - this.currentMtime = 0 - this.downloadPath = null - this.uploadPath = null - this.pendingFiles = [] - this.currentFileIndex = 0 - this.savePath = null - this.fileReadPosition = 0 - this.transferredBytes = 0 - this.transferSize = 0 - this.carry = null - this.idleScanTail = null - this.scanTail = null - this.canTail = null - this.carrySince = 0 - this.lastProgressUpdate = 0 - this.residueTail = null - // keep suppressNoiseUntil: it was just armed above - } - - /** - * User-initiated cancel - */ - cancel () { - this.abort(true) - } - - /** - * Check if session is active - * @returns {boolean} - */ - isActive () { - return this.state !== ZMODEM_STATE.IDLE && !this.destroyed - } - - /** - * Final teardown when the terminal goes away - */ - destroy () { - if (this.destroyed) return - this.destroyed = true - this.endSession() - this.term = null - this.ws = null - } -} - -/** - * ZmodemManager manages zmodem sessions for multiple terminals - */ -class ZmodemManager { - constructor () { - this.sessions = new Map() - } - - /** - * Create or get zmodem session for a terminal - * @param {string} pid - Terminal PID - * @param {Object} term - Terminal instance - * @param {Object} ws - WebSocket connection - * @returns {ZmodemSession} - */ - getSession (pid, term, ws) { - if (!this.sessions.has(pid)) { - const session = new ZmodemSession(term, ws) - this.sessions.set(pid, session) - } - return this.sessions.get(pid) - } - - /** - * Handle data for a terminal - * @param {string} pid - Terminal PID - * @param {Buffer} data - Incoming data - * @param {Object} term - Terminal instance - * @param {Object} ws - WebSocket connection - * @returns {boolean} - True if data was consumed by zmodem - */ - handleData (pid, data, term, ws) { - const session = this.getSession(pid, term, ws) - return session.handleData(data) - } - - /** - * Handle client message - * @param {string} pid - Terminal PID - * @param {Object} msg - Message from client - * @param {Object} term - Terminal instance - * @param {Object} ws - WebSocket connection - */ - handleMessage (pid, msg, term, ws) { - const session = this.getSession(pid, term, ws) - - switch (msg.event) { - case 'set-save-path': - session.setSavePath(msg.path) - break - case 'send-files': - session.setSendFiles(msg.files) - break - case 'cancel': - session.cancel() - break - case 'prepare-receive': - // kept for backward compatibility; receive prep is automatic now - break - } - } - - /** - * Observe user keystrokes for a terminal before they reach the pty. - * Lets an active session react to Ctrl-C immediately. - * @param {string} pid - Terminal PID - * @param {string|Buffer} data - User input - */ - handleUserInput (pid, data) { - const session = this.sessions.get(pid) - if (session) session.handleUserInput(data) - } - - /** - * Destroy session for a terminal - * @param {string} pid - Terminal PID - */ - destroySession (pid) { - const session = this.sessions.get(pid) - if (session) { - session.destroy() - this.sessions.delete(pid) - } - } - - /** - * Check if terminal has active zmodem session - * @param {string} pid - Terminal PID - * @returns {boolean} - */ - isActive (pid) { - const session = this.sessions.get(pid) - return session ? session.isActive() : false - } -} - -// Export singleton manager -const zmodemManager = new ZmodemManager() - -export { - ZmodemSession, - ZmodemManager, - zmodemManager, - ZMODEM_STATE, - ZMODEM_HEADER, - WATCHDOG, - MAX_BUFFERED_BYTES -} diff --git a/src/app/upgrade/db-defaults.js b/src/app/upgrade/db-defaults.js deleted file mode 100644 index a18aef9..0000000 --- a/src/app/upgrade/db-defaults.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * database default should init - */ - -function parsor (themeTxt) { - return themeTxt.split('\n').reduce((prev, line) => { - let [key = '', value = ''] = line.split('=') - key = key.trim() - value = value.trim() - if (!key || !value) { - return prev - } - prev[key] = value - return prev - }, {}) -} - -const defaultTheme = parsor(` - main = #141314 - main-dark = #000 - main-light = #2E3338 - text = #ddd - text-light = #fff - text-dark = #888 - text-disabled = #777 - primary = #08c - info = #FFD166 - success = #06D6A0 - error = #EF476F - warn = #E55934 -`) -const defaultThemeLight = parsor(` - main=#ededed - main-dark=#cccccc - main-light=#fefefe - text=#555 - text-light=#777 - text-dark=#444 - text-disabled=#888 - primary=#08c - info=#FFD166 - success=#06D6A0 - error=#EF476F - warn=#E55934 -`) -const defaultThemeLightTerminal = parsor(` -foreground=#333333 -background=#ededed -cursor=#b5bd68 -cursorAccent=#1d1f21 -selectionBackground=rgba(0, 0, 0, 0.3) -black=#575757 -red=#FF2C6D -green=#19f9d8 -yellow=#FFB86C -blue=#45A9F9 -magenta=#FF75B5 -cyan=#B084EB -white=#CDCDCD -brightBlack=#757575 -brightRed=#FF2C6D -brightGreen=#19f9d8 -brightYellow=#FFCC95 -brightBlue=#6FC1FF -brightMagenta=#FF9AC1 -brightCyan=#BCAAFE -brightWhite=#E6E6E6 -`) - -const defaultThemeTerminal = { - foreground: '#bbbbbb', - background: '#141314', - cursor: '#b5bd68', - cursorAccent: '#1d1f21', - selectionBackground: 'rgba(200, 200, 200, 0.6)', - black: '#575757', - red: '#FF2C6D', - green: '#19f9d8', - yellow: '#FFB86C', - blue: '#45A9F9', - magenta: '#FF75B5', - cyan: '#B084EB', - white: '#CDCDCD', - brightBlack: '#757575', - brightRed: '#FF2C6D', - brightGreen: '#19f9d8', - brightYellow: '#FFCC95', - brightBlue: '#6FC1FF', - brightMagenta: '#FF9AC1', - brightCyan: '#BCAAFE', - brightWhite: '#E6E6E6' -} - -export default [ - { - db: 'terminalThemes', - data: [ - { - _id: 'default', - name: 'default', - themeConfig: defaultThemeTerminal, - uiThemeConfig: defaultTheme - }, - { - _id: 'defaultLight', - name: 'default light', - themeConfig: defaultThemeLightTerminal, - uiThemeConfig: defaultThemeLight - } - ] - }, - { - db: 'bookmarkGroups', - data: [ - { - _id: 'default', - title: 'default', - bookmarkIds: [], - bookmarkGroupIds: [], - color: '#0088cc' - } - ] - } -] diff --git a/src/app/upgrade/index.js b/src/app/upgrade/index.js deleted file mode 100644 index df1d98b..0000000 --- a/src/app/upgrade/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * common data upgrade process - * It will check current version in db and check version in package.json, - * run every upgrade script one by one - */ - -import { packInfo } from '../common/runtime-constants.js' -import { resolve, dirname } from 'path' -import fs from 'fs' -import log from '../common/log.js' -import compare from '../common/version-compare.js' -import { dbAction } from '../lib/db.js' -import _ from 'lodash' -import initData from './init-nedb.js' -import { updateDBVersion } from './version-upgrade.js' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const { version: packVersion } = packInfo -const emptyVersion = '0.0.0' -const versionQuery = { - _id: 'version' -} - -async function getDBVersion () { - const version = await dbAction('data', 'findOne', versionQuery) - .then(doc => { - return doc ? doc.value : emptyVersion - }) - .catch(e => { - log.error(e) - return emptyVersion - }) - return version -} - -/** - * get upgrade versions should be run as version upgrade - */ -async function getUpgradeVersionList () { - const version = await getDBVersion() - const list = fs.readdirSync(__dirname) - return list.filter(f => { - const vv = f.replace('.js', '').replace('v', '') - return /^v\d/.test(f) && compare(vv, version) > 0 && compare(vv, packVersion) <= 0 - }).sort((a, b) => { - return compare(a, b) - }) -} - -async function versionShouldUpgrade () { - const dbVersion = await getDBVersion() - log.info('database version:', dbVersion) - return compare(dbVersion, packVersion) < 0 -} - -export async function checkDbUpgrade () { - const shouldUpgradeVersion = await versionShouldUpgrade() - if (!shouldUpgradeVersion) { - return false - } - const dbVersion = await getDBVersion() - log.info('dbVersion', dbVersion) - if (dbVersion === emptyVersion) { - await initData() - await updateDBVersion(packVersion) - return false - } - const list = await getUpgradeVersionList() - if (_.isEmpty(list)) { - await updateDBVersion(packVersion) - return false - } - return { - dbVersion, - packVersion - } -} - -export async function doUpgrade () { - const list = await getUpgradeVersionList() - log.info('Upgrading...') - for (const v of list) { - const p = resolve(__dirname, v) - const run = import(p).then(d => d.default) - await run() - } - log.info('Upgrade end') -} diff --git a/src/app/upgrade/init-nedb.js b/src/app/upgrade/init-nedb.js deleted file mode 100644 index d67cf63..0000000 --- a/src/app/upgrade/init-nedb.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * for new user, they do not have old json db - * just need init db - */ - -import { dbAction } from '../lib/db.js' -import log from '../common/log.js' -import defaults from './db-defaults.js' - -export default async function initData () { - log.info('start: init db') - for (const conf of defaults) { - const { - db, data - } = conf - await dbAction(db, 'insert', data).catch(log.error) - } - log.info('end: init db') -} diff --git a/src/app/upgrade/version-upgrade.js b/src/app/upgrade/version-upgrade.js deleted file mode 100644 index 3af905f..0000000 --- a/src/app/upgrade/version-upgrade.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * upgrade db version - */ - -/** - * common data upgrade process - * It will check current version in db and check version in package.json, - * run every upgrade script one by one - */ - -import log from '../common/log.js' -import { dbAction } from '../lib/db.js' - -export async function updateDBVersion (toVersion) { - const versionQuery = { - _id: 'version' - } - log.info('upgrade db version to', toVersion) - await dbAction('data', 'update', versionQuery, { - ...versionQuery, - value: toVersion - }, { - upsert: true - }) - .catch(e => { - log.error(e) - log.error('upgrade db version error', toVersion) - }) - await dbAction('dbUpgradeLog', 'insert', { - time: Date.now(), - toVersion - }) - .catch(e => { - log.error(e) - log.error('insert dbUpgradeLog error', toVersion) - }) -} diff --git a/src/app/views/index.pug b/src/app/views/index.pug deleted file mode 100644 index 957104f..0000000 --- a/src/app/views/index.pug +++ /dev/null @@ -1,69 +0,0 @@ - -doctype html -html - head - meta(charset='UTF-8') - meta(http-equiv='x-ua-compatible' content='IE=edge') - meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no') - title #{siteName} - style. - body { - background: #000; - } - #content-loading { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: #141314; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - .electerm-logo-bg { - background: transparent 50% 50% no-repeat url("./images/electerm-watermark.png"); - } - .morph-shape { - background: linear-gradient(45deg, #08c 0%, #09c 100%); - animation: morph 8s ease-in-out infinite; - border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; - transition: all 1s ease-in-out; - z-index: 5; - } - - if (!isDev) - link(rel='stylesheet', href='css/style-' + version + '.css') - style(id='theme-css'). - style(id='custom-css'). - body - - if (isDev) - style(id='theme-css'). - style(id='custom-css'). - #container - #content-loading - .morph-shape.iblock.pd3 - img.iblock.logo-filter(src='images/electerm.png', alt='', height=80) - script. - window.et = !{JSON.stringify(_global)} - - if (tokenElecterm) - script. - window.localStorage.setItem('tokenElecterm', window.et.tokenElecterm) - - var url = '/src/client/entry-web/basic.js' - - if (isDev) - //- script(src='/external/react.development.js?' + version) - //- script(src='/external/react-dom.development.js?' + version) - script(type='module'). - import RefreshRuntime from '/@react-refresh' - RefreshRuntime.injectIntoGlobalHook(window) - window.$RefreshReg$ = () => {} - window.$RefreshSig$ = () => (type) => type - window.__vite_plugin_react_preamble_installed__ = true - script(src='/@vite/client', type='module') - script(src=url, type='module') - - else - //- script(src='/external/react.production.min.js?' + version) - //- script(src='/external/react-dom.production.min.js?' + version) - - var url = src=cdn + '/js/basic-' + version + '.js' - script(src=url, type='module') - diff --git a/src/app/widgets/load-widget.js b/src/app/widgets/load-widget.js deleted file mode 100644 index 8e22952..0000000 --- a/src/app/widgets/load-widget.js +++ /dev/null @@ -1,201 +0,0 @@ -// load-widget.js - -import fs from 'fs' -import path from 'path' -import { fileURLToPath } from 'url' -// import log from '../common/log.js' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const widgetIdPattern = /^[a-z0-9-]+$/ - -function resolveWidgetPath (widgetId, widgetDirectory = __dirname) { - if (typeof widgetId !== 'string' || !widgetIdPattern.test(widgetId)) { - throw new Error(`Invalid widget ID: ${widgetId}`) - } - - const widgetPath = path.resolve(widgetDirectory, `widget-${widgetId}.js`) - const relativePath = path.relative(widgetDirectory, widgetPath) - - if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { - throw new Error(`Invalid widget ID: ${widgetId}`) - } - - return widgetPath -} - -// Store running widget instances -const runningInstances = new Map() - -async function listWidgetsFromFolder (widgetDirectory = __dirname) { - const widgetFiles = fs.readdirSync(widgetDirectory).filter(file => file.startsWith('widget-') && file.endsWith('.js')) - const res = [] - for (const file of widgetFiles) { - try { - const widgetPath = path.join(widgetDirectory, file) - const widgetModule = await import(`file://${widgetPath}`) - res.push({ - id: file.slice(7, -3), - info: widgetModule.widgetInfo - }) - } catch (error) { - console.error(`Error loading widget from file ${file}:`, error) - continue - } - } - return res -} - -async function listWidgets () { - const widgets1 = await listWidgetsFromFolder() - return widgets1 - // if (process.versions.electron === undefined) { - // return widgets1 - // } - // const { - // appPath - // } = require('../common/app-props') - // const userWidgetsDir = path.resolve( - // appPath, 'widgets' - // ) - // // Ensure user widgets directory exists when app starts - // try { - // if (!fs.existsSync(userWidgetsDir)) { - // fs.mkdirSync(userWidgetsDir, { recursive: true }) - // } - // } catch (err) { - // log.error(`Failed to create user widgets directory ${userWidgetsDir}:`, err) - // } - // const widgets2 = listWidgetsFromFolder( - // userWidgetsDir - // ) - // return [ - // ...widgets1, - // ...widgets2 - // ] -} - -function hasRunningInstance (widgetId) { - for (const [, instance] of runningInstances) { - if (instance.widgetId === widgetId) { - return true - } - } - return false -} - -async function runWidget (widgetId, config) { - const widgetPath = resolveWidgetPath(widgetId) - const widget = await import(`file://${widgetPath}`) - - const { type, singleInstance } = widget.widgetInfo - if (type !== 'instance') { - return widget.widgetRun(config) - } - - // Check if singleInstance widget already has a running instance - if (singleInstance && hasRunningInstance(widgetId)) { - return Promise.reject(new Error(`Widget ${widgetId} already has a running instance. Only one instance is allowed.`)) - } - - const instance = widget.widgetRun(config) - instance.widgetId = widgetId - runningInstances.set(instance.instanceId, instance) - - return instance.start() - .then((result) => { - return { - instanceId: instance.instanceId, - widgetId, - singleInstance: !!singleInstance, - ...result - } - }) - .catch((err) => { - runningInstances.delete(instance.instanceId) - return instance.stop().catch(() => {}).then(() => { throw err }) - }) -} - -function stopWidget (instanceId) { - const instance = runningInstances.get(instanceId) - if (!instance) { - console.error(`No running instance found for instanceId: ${instanceId}`) - return - } - - return instance.stop() - .then(() => { - runningInstances.delete(instanceId) - return { instanceId, status: 'stopped' } - }) -} - -async function runWidgetFunc (instanceId, funcName, ...args) { - const instance = runningInstances.get(instanceId) - if (!instance) { - throw new Error(`No running instance found for instanceId: ${instanceId}`) - } - - if (typeof instance[funcName] !== 'function') { - throw new Error(`Function ${funcName} not found in widget instance`) - } - - try { - const result = await instance[funcName](...args) - return result - } catch (error) { - console.error(`Error executing ${funcName} on widget instance ${instanceId}:`, error) - throw error - } -} - -async function cleanup () { - if (runningInstances.size === 0) { - return - } - - const stopPromises = [] - - for (const [instanceId, instance] of runningInstances) { - console.log(`Stopping widget instance: ${instanceId}`) - try { - const stopPromise = instance.stop() - .then(() => { - console.log(`Successfully stopped widget instance: ${instanceId}`) - }) - .catch(err => { - console.error(`Error stopping widget instance ${instanceId}:`, err) - }) - stopPromises.push(stopPromise) - } catch (err) { - console.error(`Error initiating stop for widget instance ${instanceId}:`, err) - } - } - - try { - await Promise.allSettled(stopPromises) - runningInstances.clear() - console.log('All widget instances have been stopped') - } catch (err) { - console.error('Error during cleanup:', err) - } -} - -// Register cleanup handlers only for process exit signals -function registerCleanupHandlers () { - process.on('SIGTERM', async () => { - console.log('Received SIGTERM, cleaning up widgets...') - await cleanup() - }) -} - -// Initialize cleanup handlers -registerCleanupHandlers() - -export { - listWidgets, - runWidget, - stopWidget, - runWidgetFunc -} diff --git a/src/app/widgets/widget-batch-op.js b/src/app/widgets/widget-batch-op.js deleted file mode 100644 index b96fca9..0000000 --- a/src/app/widgets/widget-batch-op.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Batch Operation Widget - * Allows users to define multi-step workflows in JSON format - * Runs entirely in the frontend, uses MCP tools for execution - */ - -import uid from '../common/uid.js' - -const widgetInfo = { - name: 'Batch Operation', - description: 'Define and execute multi-step SSH/SFTP workflows with progress tracking.', - version: '1.0.0', - type: 'frontend', - builtin: true, - singleInstance: false, - configs: [] -} - -function getDefaultConfig () { - return widgetInfo.configs.reduce((acc, config) => { - acc[config.name] = config.default - return acc - }, {}) -} - -async function widgetRun (config) { - const instanceId = uid() - return { - instanceId, - widgetId: 'batch-op', - success: true, - msg: 'Batch operation workflow started', - serverInfo: null, - config - } -} - -export { - widgetInfo, - getDefaultConfig, - widgetRun -} diff --git a/src/app/widgets/widget-local-file-server.js b/src/app/widgets/widget-local-file-server.js deleted file mode 100644 index c6cc75e..0000000 --- a/src/app/widgets/widget-local-file-server.js +++ /dev/null @@ -1,194 +0,0 @@ -import os from 'os' -// import path from 'path' -import express from 'express' -import uid from '../common/uid.js' - -const widgetInfo = { - name: 'Static File Server', - description: 'A simple local file server to serve static files from your computer.', - version: '1.0.0', - author: 'ZHAO Xudong', - type: 'instance', - builtin: true, - configs: [ - { - name: 'host', - type: 'string', - default: '127.0.0.1', - description: 'The IP address to bind the server to' - }, - { - name: 'port', - type: 'number', - default: 3456, - description: 'The port number to listen on' - }, - { - name: 'directory', - type: 'string', - default: os.homedir(), - description: 'The directory to serve files from (default: user\'s home directory)' - }, - { - name: 'maxAge', - type: 'number', - default: 365 * 24 * 60 * 60 * 1000, - description: 'Browser cache max-age in milliseconds' - }, - // { - // name: 'immutable', - // type: 'boolean', - // default: false, - // description: 'Enable or disable the immutable directive in the Cache-Control header' - // }, - { - name: 'cacheControl', - type: 'boolean', - default: true, - description: 'Enable or disable setting Cache-Control response header' - }, - { - name: 'lastModified', - type: 'boolean', - default: true, - description: 'Enable or disable the Last-Modified header' - }, - { - name: 'etag', - type: 'boolean', - default: true, - description: 'Enable or disable etag generation' - }, - // { - // name: 'extensions', - // type: 'array', - // default: [], - // description: 'Array of file extensions to try when resolving a file' - // }, - // { - // name: 'fallthrough', - // type: 'boolean', - // default: true, - // description: 'Let client errors fall-through as unhandled requests' - // }, - { - name: 'index', - type: 'string', - default: 'index.html', - description: 'Name of the index file to serve' - }, - { - name: 'redirect', - type: 'boolean', - default: true, - description: 'Enable or disable redirects when pathname is a directory' - }, - // { - // name: 'setHeaders', - // type: 'function', - // default: null, - // description: 'Function for setting custom headers (e.g., (res, path, stat) => { res.set("X-Custom-Header", "value"); })' - // }, - { - name: 'dotfiles', - type: 'string', - default: 'allow', - choices: ['allow', 'deny', 'ignore'], - description: 'Option for serving dotfiles' - }, - { - name: 'acceptRanges', - type: 'boolean', - default: true, - description: 'Enable or disable accepting ranged requests' - }, - { - name: 'autoRun', - type: 'boolean', - default: false, - description: 'Automatically run this widget when the app launches' - } - ] -} - -function getDefaultConfig () { - return widgetInfo.configs.reduce((acc, config) => { - acc[config.name] = config.default - return acc - }, {}) -} - -function widgetRun (instanceConfig) { - const config = { ...getDefaultConfig(), ...instanceConfig } - const instanceId = uid() - let server = null - const app = express() - - const start = () => { - return new Promise((resolve, reject) => { - if (server) { - reject(new Error('Server is already running')) - return - } - const { - directory, - port, - host, - ...rest - } = config - app.use(express.static(directory, rest)) - - server = app.listen(port, host, (err) => { - if (err) { - console.error(`Failed to start ${widgetInfo.name}:`, err) - reject(err) - } else { - const serverInfo = { - url: `http://${host}:${port}`, - path: directory - } - const msg = `${widgetInfo.name} is running at ${serverInfo.url}` - console.log(msg) - console.log(`Serving files from: ${serverInfo.path}`) - resolve({ serverInfo, msg, success: true }) - } - }) - - server.on('error', (err) => { - console.error(`${widgetInfo.name} encountered an error:`, err) - reject(err) - }) - }) - } - - const stop = () => { - return new Promise((resolve, reject) => { - if (server) { - server.close((err) => { - if (err) { - console.error('Error stopping the server:', err) - reject(err) - } else { - console.log(`${widgetInfo.name} has been stopped`) - server = null - resolve() - } - }) - } else { - console.log(`${widgetInfo.name} is not running`) - resolve() - } - }) - } - - return { - instanceId, - start, - stop - } -} - -export { - widgetInfo, - widgetRun -} diff --git a/src/app/widgets/widget-local-ftp-server.js b/src/app/widgets/widget-local-ftp-server.js deleted file mode 100644 index c573fae..0000000 --- a/src/app/widgets/widget-local-ftp-server.js +++ /dev/null @@ -1,143 +0,0 @@ -import os from 'os' -import uid from '../common/uid.js' -import FtpSrv from '@electerm/ftp-srv' - -const widgetInfo = { - name: 'Local FTP Server', - description: 'A local FTP server to share files over FTP protocol.', - version: '1.0.0', - author: 'ZHAO Xudong', - type: 'instance', - builtin: true, - configs: [ - { - name: 'host', - type: 'string', - default: '0.0.0.0', - description: 'The IP address to bind the FTP server to' - }, - { - name: 'port', - type: 'number', - default: 2121, - description: 'The port number to listen on' - }, - { - name: 'directory', - type: 'string', - default: os.homedir(), - description: 'The directory to serve files from (default: user\'s home directory)' - }, - { - name: 'anonymous', - type: 'boolean', - default: false, - description: 'Allow anonymous FTP access' - }, - { - name: 'username', - type: 'string', - default: 'ftpuser', - description: 'Username for FTP authentication (used when anonymous is false)' - }, - { - name: 'password', - type: 'string', - default: 'ftppass', - description: 'Password for FTP authentication (used when anonymous is false)' - }, - { - name: 'autoRun', - type: 'boolean', - default: false, - description: 'Automatically start this FTP server when the app launches' - } - ] -} - -function getDefaultConfig () { - return widgetInfo.configs.reduce((acc, config) => { - acc[config.name] = config.default - return acc - }, {}) -} - -function widgetRun (instanceConfig) { - const config = { ...getDefaultConfig(), ...instanceConfig } - const instanceId = uid() - let server = null - - const start = async () => { - if (server) { - throw new Error('Server is already running') - } - - server = new FtpSrv({ - url: `ftp://${config.host}:${config.port}`, - anonymous: config.anonymous, - root: config.directory - }) - - if (!config.anonymous) { - server.on('login', ({ username, password }, resolve, reject) => { - if (username === config.username && password === config.password) { - return resolve({ root: config.directory }) - } - return reject(new Error('Invalid username or password')) - }) - } - - server.on('client-error', ({ connection, context, error }) => { - console.log('FTP client error:', error) - }) - - return new Promise((resolve, reject) => { - server.listen() - .then(() => { - const url = config.anonymous - ? `ftp://${config.host}:${config.port}` - : `ftp://${config.username}:${config.password}@${config.host}:${config.port}` - const serverInfo = { - url, - path: config.directory - } - const msg = `${widgetInfo.name} is running at ${serverInfo.url}` - console.log(msg) - console.log(`Serving files from: ${serverInfo.path}`) - resolve({ serverInfo, msg, success: true }) - }) - .catch(reject) - }) - } - - const stop = () => { - return new Promise((resolve, reject) => { - if (server) { - server.close() - .then(() => { - console.log(`${widgetInfo.name} has been stopped`) - server = null - resolve() - }) - .catch((err) => { - console.error('Error stopping the FTP server:', err) - reject(err) - }) - } else { - console.log(`${widgetInfo.name} is not running`) - resolve() - } - }) - } - - return { - instanceId, - start, - stop - } -} - -export { - widgetInfo, - widgetRun -} diff --git a/src/app/widgets/widget-mcp-server.js b/src/app/widgets/widget-mcp-server.js deleted file mode 100644 index e9a359f..0000000 --- a/src/app/widgets/widget-mcp-server.js +++ /dev/null @@ -1,1225 +0,0 @@ -/** - * MCP Server Widget - * Exposes electerm store APIs via Model Context Protocol - * Runs in main process and uses IPC to communicate with frontend - * Uses a simple local MCP implementation - */ - -import { McpServer } from '../mcp/server/mcp.js' -import { StreamableHTTPServerTransport } from '../mcp/server/streamableHttp.js' -import { TaskManager } from '../mcp/server/tasks.js' -import { z } from '../lib/zod.js' -import express from 'express' -import uid from '../common/uid.js' -import globalState from '../server/global-state.js' -import { - sshBookmarkSchema, - telnetBookmarkSchema, - serialBookmarkSchema -} from '../common/bookmark-zod-schemas.js' - -// Dangerous tab props that allow arbitrary command execution. -// Must be stripped from any MCP tool args before forwarding to the renderer. -// Mirrors src/client/store/tab.js dangerousTabProps. -const dangerousTabProps = [ - 'execLinux', - 'execMac', - 'execWindows', - 'execWindowsArgs', - 'execMacArgs', - 'execLinuxArgs', - 'setEnv', - 'runScripts', - 'interactiveValues' -] - -function stripDangerousTabProps (obj) { - return Object.fromEntries( - Object.entries(obj).filter(([key]) => !dangerousTabProps.includes(key)) - ) -} - -const widgetInfo = { - name: 'MCP Server', - description: 'Expose electerm APIs via Model Context Protocol (MCP) for AI assistants and external tools.', - version: '1.0.0', - author: 'ZHAO Xudong', - type: 'instance', - builtin: true, - singleInstance: true, - configs: [ - { - name: 'host', - type: 'string', - default: '127.0.0.1', - description: 'The IP address to bind the MCP server to' - }, - { - name: 'port', - type: 'number', - default: 30837, - description: 'The port number to listen on' - }, - { - name: 'apiKey', - type: 'string', - default: '', - showGenerator: true, - description: 'Optional API key for authenticating MCP requests. If set, clients must send this in the Authorization header as: Bearer . Leave empty to skip authentication.' - }, - { - name: 'enableBookmarks', - type: 'boolean', - default: true, - description: 'Enable bookmark APIs (list, get, add, edit, delete)' - }, - { - name: 'bookmarkKeyword', - type: 'string', - default: '', - description: 'Filter keyword for bookmark list API. Only bookmarks with titles containing this keyword (case-insensitive) will be returned. Leave empty to return all bookmarks.' - }, - { - name: 'enableBookmarkGroups', - type: 'boolean', - default: true, - description: 'Enable bookmark group APIs' - }, - { - name: 'enableSftp', - type: 'boolean', - default: true, - description: 'Enable SFTP APIs (list, stat, read, delete, upload, download, trzsz)' - }, - { - name: 'enableSettings', - type: 'boolean', - default: false, - description: 'Enable settings APIs' - }, - { - name: 'autoRun', - type: 'boolean', - default: false, - description: 'Automatically start this MCP server when the app launches' - }, - { - name: 'commandBlacklist', - type: 'textarea', - default: '', - description: 'Newline-separated list of regex patterns. Commands matching any pattern are rejected. Built-in dangerous patterns are always active.' - }, - { - name: 'commandWhitelist', - type: 'textarea', - default: '', - description: 'Newline-separated list of regex patterns. When non-empty, only commands matching at least one pattern are allowed (whitelist mode).' - }, - { - name: 'execTimeoutMs', - type: 'number', - default: 120000, - description: 'Default timeout (ms) for execute_electerm_command. Commands exceeding it return partial output with timedOut=true.' - }, - { - name: 'execMaxOutputBytes', - type: 'number', - default: 204800, - description: 'Max characters of stdout/stderr returned by execute_electerm_command. Longer output is tail-truncated with truncated=true.' - }, - { - name: 'enableTasks', - type: 'boolean', - default: true, - description: 'Enable the MCP Tasks extension (io.modelcontextprotocol/tasks, SEP-2663). Lets supporting clients run long commands as pollable tasks via execute_electerm_command with wait=false.' - }, - { - name: 'taskTtlMs', - type: 'number', - default: 3600000, - description: 'How long (ms) a finished MCP task is retained for tasks/get before being swept. Also triggers remote temp-file cleanup.' - } - ] -} - -function getDefaultConfig () { - return widgetInfo.configs.reduce((acc, config) => { - acc[config.name] = config.default - return acc - }, {}) -} - -class ElectermMCPServer { - constructor (config) { - this.config = config - // API key is optional; when empty, authentication is skipped. - this.instanceId = uid() - this.httpServer = null - this.mcpServer = null - this.ipcHandler = null - this.pendingRequests = new Map() - this.transports = {} - this.taskManager = null - } - - static get BUILTIN_BLACKLIST () { - return [ - /rm\s+-[^\s]*[rR][^\s]*\s+\//, - /rm\s+-[^\s]*[rR][^\s]*\s+~/, - /rm\s+--recursive/, - /:\s*\(\s*\)\s*\{.*\|.*:.*&.*\}\s*;.*:/, - /\bdd\b.*\bof\s*=\s*\/dev\//, - /\bmkfs\b/, - />\s*\/dev\/[sh]d[a-z]/, - /\bsudo\s+rm\b/, - /curl\s+.*\|\s*sh/, - /wget\s+.*\|\s*sh/, - /curl\s+.*\|\s*bash/, - /wget\s+.*\|\s*bash/ - ] - } - - validateCommand (command) { - for (const pattern of ElectermMCPServer.BUILTIN_BLACKLIST) { - if (pattern.test(command)) { - return { allowed: false, reason: `Command blocked by built-in safety rule: ${pattern}` } - } - } - const userBlacklist = (this.config.commandBlacklist || '').split('\n').map(s => s.trim()).filter(Boolean) - for (const raw of userBlacklist) { - try { - if (new RegExp(raw).test(command)) { - return { allowed: false, reason: `Command blocked by blacklist pattern: ${raw}` } - } - } catch (_) {} - } - const userWhitelist = (this.config.commandWhitelist || '').split('\n').map(s => s.trim()).filter(Boolean) - if (userWhitelist.length > 0) { - const allowed = userWhitelist.some(raw => { try { return new RegExp(raw).test(command) } catch (_) { return false } }) - if (!allowed) { return { allowed: false, reason: 'Command not in whitelist' } } - } - return { allowed: true } - } - - // Send request to renderer process via IPC - sendToRenderer (action, data, timeoutMs = 30000) { - return new Promise((resolve, reject) => { - const requestId = uid() - const commonWs = globalState.getCommonWs() - - if (!commonWs) { - reject(new Error('No commonWs connection available')) - return - } - - // Set up response handler - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Request timeout')) - }, timeoutMs) - - this.pendingRequests.set(requestId, { resolve, reject, timeout }) - - // Send to renderer - commonWs.s({ - type: 'mcp-request', - requestId, - action, - data - }) - }) - } - - // ==================== MCP Tasks lifecycle (SEP-2663) ==================== - // Tasks wrap the renderer's background-command engine (nohup + pid/exit/ - // log files). These methods map renderer background states onto the MCP - // task state machine. - - // onGet hook: refresh a working task from the renderer before tasks/get - // returns it. Terminal renderer states move the task to a terminal status. - async refreshTask (task) { - const { bgTaskId, startedAt } = task.meta || {} - if (!bgTaskId) { - return - } - try { - const status = await this.sendToRenderer('tool-call', { - toolName: 'get_background_task_status', - args: { taskId: bgTaskId } - }) - if (status.status === 'completed') { - const log = await this.sendToRenderer('tool-call', { - toolName: 'get_background_task_log', - args: { taskId: bgTaskId, lines: 200 } - }) - const maxOut = this.config.execMaxOutputBytes || 204800 - let stdout = log.output || '' - let truncated = false - if (stdout.length > maxOut) { - stdout = stdout.slice(-maxOut) - truncated = true - } - this.taskManager.complete(task.taskId, { - stdout, - stderr: '', - stderrMerged: true, - exitCode: typeof status.exitCode === 'number' ? status.exitCode : null, - durationMs: (status.endTime || Date.now()) - (startedAt || Date.now()), - truncated, - mode: 'background', - tabId: task.meta.tabId - }) - } else if (status.status === 'cancelled') { - this.taskManager.cancelLocal(task.taskId) - } else if (status.status === 'unknown') { - this.taskManager.fail(task.taskId, status.message || 'Background task state unknown') - } else { - // still running — surface elapsed time for polling clients - const elapsed = Math.round((Date.now() - (startedAt || Date.now())) / 1000) - task.statusMessage = `Running (${elapsed}s elapsed)` - } - } catch (e) { - this.taskManager.fail(task.taskId, e.message) - } - } - - // onCancel hook: kill the underlying background process. - async cancelTaskRemote (task) { - const { bgTaskId } = task.meta || {} - if (!bgTaskId) { - return - } - try { - await this.sendToRenderer('tool-call', { - toolName: 'cancel_background_task', - args: { taskId: bgTaskId } - }) - } catch (_) { - // best-effort kill — the task is marked cancelled regardless - } - } - - // onSweep hook: remove remote temp files (log/pid/exit) for swept tasks. - async sweepTaskRemote (task) { - const { bgTaskId } = task.meta || {} - if (!bgTaskId) { - return - } - try { - await this.sendToRenderer('tool-call', { - toolName: 'cleanup_background_task', - args: { taskId: bgTaskId } - }, 10000) - } catch (_) { - // best-effort cleanup - } - } - - // Register all tools on the MCP server - registerTools () { - const server = this.mcpServer - const self = this - - // ==================== Tab/Terminal APIs (always enabled) ==================== - - server.registerTool( - 'list_electerm_tabs', - { - description: 'List all open electerm terminal tabs', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'list_tabs', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'get_electerm_active_tab', - { - description: 'Get the currently active electerm tab', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'get_active_tab', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'switch_electerm_tab', - { - description: 'Switch to a specific electerm tab', - inputSchema: { - tabId: z.string().describe('Tab ID to switch to') - } - }, - async ({ tabId }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'switch_tab', args: { tabId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'close_electerm_tab', - { - description: 'Close a specific electerm tab', - inputSchema: { - tabId: z.string().describe('Tab ID to close') - } - }, - async ({ tabId }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'close_tab', args: { tabId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'reload_electerm_tab', - { - description: 'Reload/reconnect an electerm tab', - inputSchema: { - tabId: z.string().optional().describe('Tab ID to reload (default: active tab)') - } - }, - async (args) => { - const tabId = args?.tabId - const result = await self.sendToRenderer('tool-call', { toolName: 'reload_tab', args: { tabId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'duplicate_electerm_tab', - { - description: 'Duplicate an electerm tab', - inputSchema: { - tabId: z.string().describe('Tab ID to duplicate') - } - }, - async ({ tabId }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'duplicate_tab', args: { tabId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'send_electerm_terminal_command', - { - description: 'Send a command to the active electerm terminal. For non-interactive commands, prefer execute_electerm_command — it returns structured stdout/stderr/exitCode in one call instead of requiring send + wait + read.', - inputSchema: { - command: z.string().describe('Command to send'), - tabId: z.string().optional().describe('Optional: specific tab ID'), - inputOnly: z.boolean().optional().describe('Input only mode (no enter key)') - } - }, - async ({ command, tabId, inputOnly }) => { - const check = self.validateCommand(command) - if (!check.allowed) { - return { content: [{ type: 'text', text: JSON.stringify({ error: check.reason }, null, 2) }], isError: true } - } - const result = await self.sendToRenderer('tool-call', { - toolName: 'send_terminal_command', - args: { command, tabId, inputOnly } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'get_electerm_terminal_selection', - { - description: 'Get the current text selection in electerm terminal', - inputSchema: { - tabId: z.string().optional().describe('Optional: specific tab ID') - } - }, - async (args) => { - const tabId = args?.tabId - const result = await self.sendToRenderer('tool-call', { toolName: 'get_terminal_selection', args: { tabId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'get_electerm_terminal_output', - { - description: 'Get recent electerm terminal output/buffer content', - inputSchema: { - tabId: z.string().optional().describe('Optional: specific tab ID'), - lines: z.number().optional().describe('Number of lines to return (default: 50)') - } - }, - async (args) => { - const tabId = args?.tabId - const lines = args?.lines - const result = await self.sendToRenderer('tool-call', { toolName: 'get_terminal_output', args: { tabId, lines } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'wait_for_electerm_terminal_idle', - { - description: 'Wait until the active terminal stops producing output, then return its content. ' + - 'Use this after send_electerm_terminal_command to know when the command has finished. ' + - 'The terminal is considered idle when no data has arrived for ~4 seconds. ' + - 'Returns output and elapsed time; timedOut=true if the command was still running at the timeout.', - inputSchema: { - tabId: z.string().optional().describe('Tab ID to watch (default: active tab)'), - timeout: z.number().optional().describe('Max milliseconds to wait for idle (default: 30000, max: 120000)'), - lines: z.number().optional().describe('Lines of terminal output to return when idle (default: 50)'), - minWait: z.number().optional().describe('Initial delay before polling starts, ms (default: 1000)') - } - }, - async (args) => { - // IPC timeout must exceed the tool timeout by a safe margin - const toolTimeout = Math.min(args?.timeout || 30000, 120000) - const ipcTimeout = toolTimeout + 10000 - const result = await self.sendToRenderer( - 'tool-call', - { toolName: 'wait_for_terminal_idle', args }, - ipcTimeout - ) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'get_electerm_terminal_status', - { - description: 'Get the current status of a terminal tab. Returns whether it is actively receiving data (running), idle (no data for 4+ seconds), or has a password prompt. Also returns the last 20 lines of terminal output. This is a lightweight, non-blocking check ideal for monitoring long-running commands.', - inputSchema: { - tabId: z.string().optional().describe('Tab ID to check (default: active tab)') - } - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'get_terminal_status', args - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'cancel_electerm_terminal_command', - { - description: 'Cancel the currently running command in a terminal by sending Ctrl+C. Use this to interrupt a long-running or stuck command.', - inputSchema: { - tabId: z.string().optional().describe('Tab ID to cancel command in (default: active tab)') - } - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'cancel_terminal_command', args - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'execute_electerm_command', - { - description: 'Execute a non-interactive shell command and return a structured result: { stdout, stderr, exitCode, durationMs, timedOut, truncated, mode, tabId }. ' + - 'On SSH tabs this uses a dedicated exec channel (mode="exec") with real stdout/stderr/exit code capture — no terminal buffer parsing needed. ' + - 'On other tabs it falls back to sentinel-based PTY capture (mode="pty", stderr merged into stdout). ' + - 'Prefer this over send_electerm_terminal_command + wait_for_electerm_terminal_idle for regular commands like git status, docker ps, npm test. ' + - 'For interactive programs (vim, top, ssh) use the terminal send/read tools instead. ' + - 'For long-running commands pass wait=false: runs in the background and returns an MCP task handle (poll with tasks/get, stop with tasks/cancel). Requires the MCP Tasks extension.', - inputSchema: { - command: z.string().describe('The shell command to execute'), - tabId: z.string().optional().describe('Tab ID to run on (default: active tab)'), - timeoutMs: z.number().optional().describe('Max execution time in ms (default: 120000, max: 600000). On timeout returns partial output with timedOut=true.'), - wait: z.boolean().optional().describe('Wait for completion and return the structured result (default: true). Set false for long-running commands to get a task handle instead.'), - mode: z.enum(['exec', 'pty']).optional().describe('Execution mode: "exec" (default) uses the SSH exec channel with PTY fallback; "pty" forces execution in the visible terminal (for commands needing a TTY: colors, sudo prompts, TTY-aware tools). Ignored when wait=false.') - } - }, - async (args, ctx) => { - const check = self.validateCommand(args?.command || '') - if (!check.allowed) { - return { content: [{ type: 'text', text: JSON.stringify({ error: check.reason }, null, 2) }], isError: true } - } - - // Async path: run in background, return a task handle instead of the result - if (args?.wait === false) { - // Requires the MCP Tasks extension — there is no non-task way to - // poll or cancel an async run (legacy background tools were removed). - if (!ctx?.clientSupportsTasks || !self.taskManager) { - return { - content: [{ - type: 'text', - text: JSON.stringify({ - error: 'wait=false requires the MCP Tasks extension (io.modelcontextprotocol/tasks). ' + - 'Declare the extension in client capabilities, or call with wait=true (default) to run synchronously.' - }, null, 2) - }], - isError: true - } - } - const bg = await self.sendToRenderer('tool-call', { - toolName: 'run_background_command', - args: { command: args.command, tabId: args.tabId } - }) - const task = self.taskManager.create({ - toolName: 'execute_electerm_command', - meta: { - bgTaskId: bg.taskId, - tabId: bg.tabId, - command: args.command, - startedAt: Date.now() - } - }) - return { - resultType: 'task', - task: self.taskManager.toWire(task) - } - } - - // Sync path: wait for completion, return structured result - const timeoutMs = Math.min(Math.max(args?.timeoutMs || self.config.execTimeoutMs || 120000, 1000), 600000) - const result = await self.sendToRenderer( - 'tool-call', - { - toolName: 'execute_command', - args: { - command: args.command, - tabId: args.tabId, - timeoutMs, - maxOutputBytes: self.config.execMaxOutputBytes || 204800, - mode: args.mode - } - }, - timeoutMs + 15000 - ) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - // ==================== Direct Tab Open APIs (always enabled) ==================== - - server.registerTool( - 'open_electerm_tab_ssh', - { - description: 'Open a new SSH terminal tab directly with connection parameters (no bookmark created)', - inputSchema: sshBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'open_tab', - args: { ...stripDangerousTabProps(args), type: 'ssh' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'open_electerm_tab_telnet', - { - description: 'Open a new Telnet terminal tab directly with connection parameters (no bookmark created)', - inputSchema: telnetBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'open_tab', - args: { ...stripDangerousTabProps(args), type: 'telnet' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'open_electerm_tab_serial', - { - description: 'Open a new Serial terminal tab directly with connection parameters (no bookmark created)', - inputSchema: serialBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'open_tab', - args: { ...stripDangerousTabProps(args), type: 'serial' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - // ==================== Bookmark APIs ==================== - if (this.config.enableBookmarks) { - server.registerTool( - 'list_electerm_bookmarks', - { - description: 'List all electerm SSH/terminal bookmarks', - inputSchema: {} - }, - async (args) => { - let result = await self.sendToRenderer('tool-call', { toolName: 'list_bookmarks', args: {} }) - const keyword = self.config.bookmarkKeyword - if (keyword && Array.isArray(result)) { - const lower = keyword.toLowerCase() - result = result.filter(b => (b.title || '').toLowerCase().includes(lower)) - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'get_electerm_bookmark', - { - description: 'Get a specific electerm bookmark by ID', - inputSchema: { - id: z.string().describe('Bookmark ID') - } - }, - async ({ id }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'get_bookmark', args: { id } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'add_electerm_bookmark_ssh', - { - description: 'Add a new SSH bookmark to electerm', - inputSchema: sshBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'add_bookmark', - args: { ...args, type: 'ssh' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'add_electerm_bookmark_telnet', - { - description: 'Add a new Telnet bookmark to electerm', - inputSchema: telnetBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'add_bookmark', - args: { ...args, type: 'telnet' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'add_electerm_bookmark_serial', - { - description: 'Add a new Serial bookmark to electerm', - inputSchema: serialBookmarkSchema - }, - async (args) => { - const result = await self.sendToRenderer('tool-call', { - toolName: 'add_bookmark', - args: { ...args, type: 'serial' } - }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'edit_electerm_bookmark', - { - description: 'Edit an existing electerm bookmark', - inputSchema: { - id: z.string().describe('Bookmark ID to edit'), - updates: z.record(z.any()).describe('Fields to update') - } - }, - async ({ id, updates }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'edit_bookmark', args: { id, updates } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'delete_electerm_bookmark', - { - description: 'Delete an electerm bookmark', - inputSchema: { - id: z.string().describe('Bookmark ID to delete') - } - }, - async ({ id }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'delete_bookmark', args: { id } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'open_electerm_bookmark', - { - description: 'Open an electerm bookmark in a new tab', - inputSchema: { - id: z.string().describe('Bookmark ID to open') - } - }, - async ({ id }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'open_bookmark', args: { id } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - } - - // ==================== Bookmark Group APIs ==================== - if (this.config.enableBookmarkGroups) { - server.registerTool( - 'list_electerm_bookmark_groups', - { - description: 'List all electerm bookmark groups/folders', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'list_bookmark_groups', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'add_electerm_bookmark_group', - { - description: 'Add a new electerm bookmark group', - inputSchema: { - title: z.string().describe('Group title'), - parentId: z.string().optional().describe('Optional parent group ID') - } - }, - async ({ title, parentId }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'add_bookmark_group', args: { title, parentId } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - } - // ==================== SFTP APIs ==================== - if (this.config.enableSftp) { - server.registerTool( - 'electerm_sftp_list', - { - description: 'List files and folders in a remote directory on the SSH-connected tab', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remotePath: z.string().describe('Remote directory path to list') - } - }, - async ({ tabId, remotePath }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_list', args: { tabId, remotePath } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_stat', - { - description: 'Get file or directory stat/info on the remote SSH server', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remotePath: z.string().describe('Remote file or directory path') - } - }, - async ({ tabId, remotePath }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_stat', args: { tabId, remotePath } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_read_file', - { - description: 'Read the content of a remote file on the SSH server', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remotePath: z.string().describe('Remote file path to read') - } - }, - async ({ tabId, remotePath }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_read_file', args: { tabId, remotePath } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_del_file_or_folder', - { - description: 'Delete a file or folder on the remote SSH server', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remotePath: z.string().describe('Remote file or directory path to delete') - } - }, - async ({ tabId, remotePath }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_del', args: { tabId, remotePath } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_upload', - { - description: 'Upload a local file or folder to the remote SSH server using the SFTP transfer panel', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - localPath: z.string().describe('Local file or folder path to upload'), - remotePath: z.string().describe('Remote destination path'), - conflictPolicy: z.enum(['mergeOrOverwriteAll', 'renameAll']).optional().describe('Conflict policy: mergeOrOverwriteAll or renameAll (default: mergeOrOverwriteAll)') - } - }, - async ({ tabId, localPath, remotePath, conflictPolicy }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_upload', args: { tabId, localPath, remotePath, conflictPolicy } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_download', - { - description: 'Download a remote file or folder from the SSH server to a local path using the SFTP transfer panel', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remotePath: z.string().describe('Remote file or directory path to download'), - localPath: z.string().describe('Local destination path'), - conflictPolicy: z.enum(['overwrite', 'rename']).optional().describe('Conflict policy: overwrite or rename (default: overwrite)') - } - }, - async ({ tabId, remotePath, localPath, conflictPolicy }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_download', args: { tabId, remotePath, localPath, conflictPolicy } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_zmodem_upload', - { - description: 'Upload local files to the remote SSH server using trzsz (trz) or rzsz (rz). The SSH tab must have the chosen protocol installed.', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - files: z.array(z.string()).describe('List of local file paths to upload'), - protocol: z.enum(['trzsz', 'rzsz']).optional().describe('Transfer protocol: trzsz (trz) or rzsz (rz) (default: rzsz)') - } - }, - async ({ tabId, files, protocol }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'zmodem_upload', args: { tabId, files, protocol } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_zmodem_download', - { - description: 'Download remote files from the SSH server using trzsz (tsz) or rzsz (sz). The SSH tab must have the chosen protocol installed.', - inputSchema: { - tabId: z.string().optional().describe('SSH tab ID (default: active tab)'), - remoteFiles: z.array(z.string()).describe('List of remote file paths to download'), - saveFolder: z.string().describe('Local folder path to save downloaded files'), - protocol: z.enum(['trzsz', 'rzsz']).optional().describe('Transfer protocol: trzsz (tsz) or rzsz (sz) (default: rzsz)') - } - }, - async ({ tabId, remoteFiles, saveFolder, protocol }) => { - const result = await self.sendToRenderer('tool-call', { toolName: 'zmodem_download', args: { tabId, remoteFiles, saveFolder, protocol } }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_transfer_list', - { - description: 'Get the list of all currently active/pending SFTP file transfers', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_transfer_list', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - - server.registerTool( - 'electerm_sftp_transfer_history', - { - description: 'Get the history of completed/failed SFTP file transfers', - inputSchema: z.object({}) - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'sftp_transfer_history', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - } - // ==================== Settings APIs ==================== - if (this.config.enableSettings) { - server.registerTool( - 'get_electerm_settings', - { - description: 'Get current electerm application settings', - inputSchema: undefined - }, - async () => { - const result = await self.sendToRenderer('tool-call', { toolName: 'get_settings', args: {} }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - } - ) - } - } - - // Start the MCP server - async start () { - const { host, port } = this.config - - // Set up IPC response handler - this.ipcHandler = (message) => { - const msg = JSON.parse(message) - const { requestId, result, error, type } = msg - if (type !== 'mcp-response-back') { - return - } - const pending = this.pendingRequests.get(requestId) - if (pending) { - clearTimeout(pending.timeout) - this.pendingRequests.delete(requestId) - if (error) { - pending.reject(new Error(error)) - } else { - pending.resolve(result) - } - } - } - const commonWs = globalState.getCommonWs() - commonWs.on('message', this.ipcHandler) - - // Create MCP task manager (SEP-2663) when the tasks extension is enabled. - // `enableTasks` defaults to true, so treat anything other than an - // explicit `false` (undefined/null from a stale pre-5.0.6 config) as on. - if (this.config.enableTasks !== false) { - this.taskManager = new TaskManager({ - ttl: this.config.taskTtlMs > 0 ? this.config.taskTtlMs : 3600000 - }) - this.taskManager.onGet = (task) => this.refreshTask(task) - this.taskManager.onCancel = (task) => this.cancelTaskRemote(task) - this.taskManager.onSweep = (task) => this.sweepTaskRemote(task) - } - console.log( - `[mcp-widget] tasks extension: ${this.taskManager ? 'enabled' : 'disabled'} ` + - `(config.enableTasks = ${JSON.stringify(this.config.enableTasks)})` - ) - - // Create MCP server - this.mcpServer = new McpServer({ - name: 'electerm-mcp-server', - version: widgetInfo.version, - taskManager: this.taskManager - }) - - // Register all tools - this.registerTools() - - // Create Express app - const app = express() - app.use(express.json()) - - // Handle CORS, defaulting to same-origin only. - app.use((req, res, next) => { - const allowedOrigin = this.config.allowedOrigin || '' - if (allowedOrigin) { - res.setHeader('Access-Control-Allow-Origin', allowedOrigin) - } - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Authorization') - if (req.method === 'OPTIONS') { - res.status(204).end() - return - } - next() - }) - - // Authenticate requests with API key when configured. - if (this.config.apiKey) { - app.use((req, res, next) => { - const authHeader = req.headers.authorization || '' - const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '' - if (!token || token !== this.config.apiKey) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32600, - message: 'Unauthorized: invalid or missing API key' - }, - id: null - }) - return - } - next() - }) - } - - const self = this - - // Handle MCP requests - app.post('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] - - try { - let transport = sessionId ? self.transports[sessionId] : null - - if (!transport) { - // Create new transport for new session - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => uid(), - onsessioninitialized: (sid) => { - self.transports[sid] = transport - } - }) - - transport.onclose = () => { - const sid = Object.keys(self.transports).find(k => self.transports[k] === transport) - if (sid) { - delete self.transports[sid] - } - } - - await self.mcpServer.connect(transport) - } - - await transport.handleRequest(req, res, req.body) - } catch (error) { - console.error('Error handling MCP request:', error) - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error' - }, - id: null - }) - } - } - }) - - // Handle GET requests for SSE streams - app.get('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] - if (!sessionId || !self.transports[sessionId]) { - res.status(400).send('Invalid or missing session ID') - return - } - - const transport = self.transports[sessionId] - await transport.handleRequest(req, res) - }) - - // Handle DELETE requests for session termination - app.delete('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] - if (!sessionId || !self.transports[sessionId]) { - res.status(400).send('Invalid or missing session ID') - return - } - - const transport = self.transports[sessionId] - await transport.handleRequest(req, res) - }) - - return new Promise((resolve, reject) => { - this.httpServer = app.listen(port, host, (err) => { - if (err) { - console.error('MCP Server error:', err) - reject(err) - return - } - - const serverInfo = { - url: `http://${host}:${port}/mcp`, - protocol: 'mcp', - version: self.mcpServer.supportedProtocolVersions[0], - apiKey: self.config.apiKey - } - const authNote = self.config.apiKey ? '(API key required)' : '(no auth required)' - const msg = `MCP Server is running at ${serverInfo.url} ${authNote}` - resolve({ - serverInfo, - msg, - success: true - }) - }) - - this.httpServer.on('error', (err) => { - console.error('MCP Server error:', err) - reject(err) - }) - }) - } - - // Stop the MCP server - async stop () { - // Remove IPC handler - if (this.ipcHandler) { - const commonWs = globalState.getCommonWs() - commonWs.removeListener('message', this.ipcHandler) - this.ipcHandler = null - } - - // Destroy task manager (stops the TTL sweep timer) - if (this.taskManager) { - this.taskManager.destroy() - this.taskManager = null - } - - // Clear pending requests - for (const [, pending] of this.pendingRequests) { - clearTimeout(pending.timeout) - pending.reject(new Error('Server stopping')) - } - this.pendingRequests.clear() - - // Close all transports - for (const sessionId of Object.keys(this.transports)) { - try { - await this.transports[sessionId].close() - } catch (e) { - console.error(`Error closing transport ${sessionId}:`, e) - } - } - this.transports = {} - - // Close MCP server - if (this.mcpServer) { - await this.mcpServer.close() - this.mcpServer = null - } - - // Close HTTP server - return new Promise((resolve, reject) => { - if (this.httpServer) { - this.httpServer.close((err) => { - if (err) { - console.error('Error stopping MCP server:', err) - reject(err) - } else { - this.httpServer = null - resolve() - } - }) - } else { - resolve() - } - }) - } -} - -function widgetRun (instanceConfig) { - const config = { ...getDefaultConfig(), ...instanceConfig } - const mcpServer = new ElectermMCPServer(config) - - return { - instanceId: mcpServer.instanceId, - start: () => mcpServer.start(), - stop: () => mcpServer.stop() - } -} - -export { - widgetInfo, - widgetRun, - ElectermMCPServer -} diff --git a/src/app/widgets/widget-rename.js b/src/app/widgets/widget-rename.js deleted file mode 100644 index e740fa5..0000000 --- a/src/app/widgets/widget-rename.js +++ /dev/null @@ -1,182 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const fsPromises = fs.promises -const pathSeparatorPattern = /[\\/]/ -function resolveRenamePath (dir, newName) { - if (typeof newName !== 'string' || !newName.trim() || newName === '.' || newName === '..') { - throw new Error('Template produced an invalid file name') - } - - if (pathSeparatorPattern.test(newName)) { - throw new Error('Template must not include path separators') - } - - const newPath = path.resolve(dir, newName) - const relativePath = path.relative(dir, newPath) - - if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { - throw new Error('Template must keep files within the source directory') - } - - return newPath -} - -// Define defaults in one place -const DEFAULTS = { - directory: '', - template: '{name}-{n}.{ext}', - includeSubfolders: false, - fileTypes: '*', - startNumber: 1, - preserveCase: true -} - -const widgetInfo = { - name: 'File Renamer', - description: 'Batch rename files in a folder using customizable templates', - version: '1.0.0', - author: 'ZHAO Xudong', - type: 'once', - builtin: true, - configs: [ - { - name: 'directory', - type: 'string', - default: DEFAULTS.directory, - description: 'The directory containing files to rename' - }, - { - name: 'template', - type: 'string', - default: DEFAULTS.template, - description: 'Template for new file names. Available tags:\n{n} - Sequential number (e.g., 1, 2, 3)\n{n:padding} - Padded number (e.g., {n:3} => 001, 002)\n{name} - Original filename without extension\n{ext} - File extension\n{date} - File creation date (YYYY-MM-DD)\n{time} - File creation time (HH-mm-ss)\n{random} - Random string' - }, - { - name: 'includeSubfolders', - type: 'boolean', - default: DEFAULTS.includeSubfolders, - description: 'Process files in subfolders' - }, - { - name: 'fileTypes', - type: 'string', - default: DEFAULTS.fileTypes, - description: 'Comma-separated list of file extensions (e.g., jpg,png,gif) or * for all' - }, - { - name: 'startNumber', - type: 'number', - default: DEFAULTS.startNumber, - description: 'Starting number for sequential naming' - }, - { - name: 'preserveCase', - type: 'boolean', - default: DEFAULTS.preserveCase, - description: 'Preserve case of original filenames' - } - ] -} - -async function getFiles (dir, fileTypes, includeSubfolders) { - const files = await fsPromises.readdir(dir, { withFileTypes: true }) - let results = [] - for (const file of files) { - const fullPath = path.join(dir, file.name) - if (file.isDirectory() && includeSubfolders) { - results = results.concat(await getFiles(fullPath, fileTypes, includeSubfolders)) - } else if (file.isFile()) { - const ext = path.extname(file.name).toLowerCase().slice(1) - if (fileTypes === '*' || fileTypes.split(',').map(t => t.trim().toLowerCase()).includes(ext)) { - results.push(fullPath) - } - } - } - return results -} - -async function processTemplate (template, filePath, index, startNumber, preserveCase) { - const stats = await fsPromises.stat(filePath) - const parsedPath = path.parse(filePath) - const date = new Date(stats.birthtime) - const replacements = { - n: (padding) => { - const num = startNumber + index - return padding ? String(num).padStart(parseInt(padding), '0') : String(num) - }, - name: () => preserveCase ? parsedPath.name : parsedPath.name.toLowerCase(), - ext: () => parsedPath.ext.slice(1), - date: () => date.toISOString().split('T')[0], - time: () => date.toTimeString().split(' ')[0].replace(/:/g, '-'), - random: () => Math.random().toString(36).substring(2, 8), - parent: () => parsedPath.dir.split(path.sep).pop() - } - - let result = template - for (const [tag, func] of Object.entries(replacements)) { - // Handle tags with parameters like {n:3} - result = result.replace(new RegExp(`{${tag}(?::([^}]+))?}`, 'g'), (match, param) => func(param)) - } - return result -} - -async function widgetRun (params = {}) { - const config = { - ...DEFAULTS, - ...params - } - - const { - directory, - template, - includeSubfolders, - fileTypes, - startNumber, - preserveCase - } = config - - if (!directory) { - return { - success: false, - error: 'Directory must be specified' - } - } - - try { - const files = await getFiles(directory, fileTypes, includeSubfolders) - const results = [] - - for (let i = 0; i < files.length; i++) { - const filePath = files[i] - const dir = path.dirname(filePath) - const newName = await processTemplate(template, filePath, i, startNumber, preserveCase) - const newPath = resolveRenamePath(dir, newName) - await fsPromises.rename(filePath, newPath) - - results.push({ - oldPath: filePath, - newPath, - success: true - }) - } - - return { - success: true, - totalRenamed: files.length, - msg: `Renamed ${files.length} files successfully`, - details: results - } - } catch (error) { - return { - success: false, - error: error.message, - details: error - } - } -} - -export { - widgetInfo, - widgetRun -} diff --git a/src/client/entry-web/basic.js b/src/client/entry-web/basic.js deleted file mode 100644 index 95b9576..0000000 --- a/src/client/entry-web/basic.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * init app data then write main script to html body - */ -import '../electerm-react/css/basic.styl' -import '../web-components/style-overide.styl' -import '../electerm-react/css/mobile.styl' -import '../web-components/web-api.js' -import '../web-components/web-pre.js' -import { get as _get } from 'lodash-es' - -const { isDev, version, cdn } = window.et - -window.et.buildWsUrl = ( - host, - port, - tokenElecterm, - id, - type = 'terminals', - extra = '' -) => { - const ss = isDev ? window.et.server : window.location.href - const s = ss - ? ss.replace(/https?:\/\//, '').replace(/\/$/, '') - : `${host}:${port}` - const pre = ss.startsWith('https') ? 'wss' : 'ws' - return `${pre}://${s}/${type}/${id}?token=${tokenElecterm}${extra}` -} - -async function loadWorker () { - return new Promise((resolve) => { - const url = !isDev ? cdn + `/js/worker-${version}.js` : cdn + '/js/worker.js' - window.worker = new window.Worker(url) - function onInit (e) { - if (!e || !e.data) { - return false - } - const { - action - } = e.data - if (action === 'worker-init') { - window.worker.removeEventListener('message', onInit) - resolve(1) - } - } - window.worker.addEventListener('message', onInit) - }) -} - -async function load () { - window.capitalizeFirstLetter = (string) => { - return string.charAt(0).toUpperCase() + string.slice(1) - } - function loadScript () { - const rcs = document.createElement('script') - const url = !isDev ? cdn + `/js/electerm-${version}.js` : cdn + '/js/electerm.js' - rcs.src = url - rcs.type = 'module' - rcs.onload = () => { - const loadingEl = document.getElementById('content-loading') - if (loadingEl) { - document.body.removeChild(loadingEl) - } - } - document.body.appendChild(rcs) - } - window.getLang = (lang = window.store?.config.language || 'en_us') => { - return _get(window.langMap, `[${lang}].lang`) - } - window.translate = txt => { - const lang = window.getLang() - const str = _get(lang, `[${txt}]`) || txt - return window.capitalizeFirstLetter(str) - } - await loadWorker() - if (!window.et.isDev) { - window.worker.postMessage({ - action: 'init-url', - url: window.location.href - }) - } - loadScript() -} - -// window.addEventListener('load', load) -load() diff --git a/src/client/entry-web/electerm.jsx b/src/client/entry-web/electerm.jsx deleted file mode 100644 index 2e6fdd8..0000000 --- a/src/client/entry-web/electerm.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createRoot } from 'react-dom/client' -import '../../../node_modules/antd/dist/reset.css' -// import '../electerm-react/common/trzsz' -import '@fontsource/maple-mono/index.css' -import Main from '../web-components/web-main' - -const rootElement = document.getElementById('container') -const root = createRoot(rootElement) - -root.render(
) diff --git a/src/client/entry-web/worker.js b/src/client/entry-web/worker.js deleted file mode 100644 index e4feada..0000000 --- a/src/client/entry-web/worker.js +++ /dev/null @@ -1,200 +0,0 @@ -/** - * web worker - */ - -self.insts = {} - -function createWs ( - type, - id, - sftpId = '', - config -) { - // init gloabl ws - const { host, port, tokenElecterm, server = '' } = config - const ss = self.currentUrl || server - const s = ss - ? ss.replace(/https?:\/\//, '').replace(/\/$/, '') - : `${host}:${port}` - const pre = ss.startsWith('https') ? 'wss' : 'ws' - const wsUrl = `${pre}://${s}/${type}/${id}?sftpId=${sftpId}&token=${tokenElecterm}` - const ws = new WebSocket(wsUrl) - ws.s = msg => { - ws.send(JSON.stringify(msg)) - } - ws.id = id - // Buffer incoming messages until at least one addEventListener is - // registered. Without this, messages that arrive before the client - // has called addEventListener (e.g. the "session-interactive" prompt - // sent during SSH host-key verification on first connect) are silently - // lost, causing the SSH connection to hang indefinitely. - ws._messageBuffer = [] - ws._bufferActive = true - ws._bufferHandler = (evt) => { - if (ws._bufferActive) { - ws._messageBuffer.push(evt.data) - } - } - ws.addEventListener('message', ws._bufferHandler) - ws.once = (callack, id) => { - const func = (evt) => { - const arg = JSON.parse(evt.data) - if (id === arg.id) { - callack(arg) - ws.removeEventListener('message', func) - } - } - ws.addEventListener('message', func) - } - ws.onclose = () => { - if (ws.dup) { - return - } - send({ - id: ws.id, - action: 'close' - }) - delete self.insts[ws.id] - } - return new Promise((resolve) => { - ws.onopen = () => { - if (self.insts[ws.id]) { - ws.dup = true - ws.close() - resolve(null) - } else { - resolve(ws) - } - } - }) -} - -function send (data) { - self.postMessage(data) -} - -async function onMsg (e) { - const { - id, - wsId, - args, - action, - type, - persist, - url - } = e.data - if (action === 'init-url') { - self.currentUrl = url - return false - } - if (action === 'create') { - const inst = self.insts[id] - if (inst instanceof WebSocket) { - return send({ - action, - id, - persist - }, '*') - } else if (inst) { - return false - } else { - const ws = await createWs(...args) - if (ws) { - self.insts[id] = ws - } - } - send({ - action, - persist, - id - }, '*') - } else if (action === 'once') { - const ws = self.insts[wsId] - if (ws) { - const cb = (data) => { - send({ - id, - wsId, - data - }) - } - ws.once(cb, id) - } - } else if (action === 'close') { - const ws = self.insts[wsId] - if (ws) { - ws.close() - } - } else if (action === 's') { - const ws = self.insts[wsId] - if (ws) { - ws.s(...args) - } - } else if (action === 'addEventListener') { - const ws = self.insts[wsId] - if (ws) { - // Support multiple listeners using a Map keyed by listener ID - if (!ws.listeners) { - ws.listeners = new Map() - } - // Check if this listener ID already exists (prevent duplicates for same ID) - if (ws.listeners.has(id)) { - ws.removeEventListener(type, ws.listeners.get(id).cb) - } - const cb = (e) => { - send({ - wsId, - id, - data: { - data: e.data - } - }) - } - ws.listeners.set(id, { type, cb }) - ws.addEventListener(type, cb) - // Flush any buffered messages to the newly registered listener so - // that messages received before addEventListener was called are - // not lost (fixes first-use SSH connection hang). - if (type === 'message' && ws._messageBuffer && ws._messageBuffer.length > 0) { - for (const bufData of ws._messageBuffer) { - send({ - wsId, - id, - data: { - data: bufData - } - }) - } - } - // Stop buffering once at least one listener is active – future - // messages will be delivered directly via the addEventListener - // callback. Keep the buffer array around briefly so that any - // subsequent addEventListener calls (e.g. MCP handler) can also - // receive the backlog. - if (type === 'message' && ws._bufferActive) { - ws._bufferActive = false - if (ws._bufferHandler) { - ws.removeEventListener('message', ws._bufferHandler) - ws._bufferHandler = null - } - setTimeout(() => { - ws._messageBuffer = null - }, 5000) - } - } - } else if (action === 'removeEventListener') { - const ws = self.insts[wsId] - if (ws && ws.listeners && ws.listeners.has(id)) { - const listener = ws.listeners.get(id) - ws.removeEventListener(listener.type, listener.cb) - ws.listeners.delete(id) - } - } -} - -self.addEventListener('message', onMsg) -setTimeout(() => { - send({ - action: 'worker-init' - }) -}, 10) diff --git a/src/client/file-select-dialog/file-item.jsx b/src/client/file-select-dialog/file-item.jsx deleted file mode 100644 index bd1dcf3..0000000 --- a/src/client/file-select-dialog/file-item.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import FileIcon from '../electerm-react/components/sftp/file-icon' -import classNames from 'classnames' -export default function FileItem (props) { - const { - file, - selected, - onClick, - onDbClick - } = props - const handleClick = (e) => { - onClick(file, e) - } - const handleDbClick = () => { - onDbClick(file) - } - const cls = classNames( - 'dialog-file-item elli', - { - selected - } - ) - return ( -
- - {file.name} -
- ) -} diff --git a/src/client/file-select-dialog/file-select-dialog.jsx b/src/client/file-select-dialog/file-select-dialog.jsx deleted file mode 100644 index 9d7ed47..0000000 --- a/src/client/file-select-dialog/file-select-dialog.jsx +++ /dev/null @@ -1,542 +0,0 @@ -/** - * file/folder select dialog component - */ - -import { Component } from 'react' -import { - Spin, - Pagination, - Button, - Input, - ConfigProvider -} from 'antd' -import { SaveOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons' -import Modal from '../electerm-react/components/common/modal' -import { notification } from '../electerm-react/components/common/notification' -import FileItem from './file-item' -import AddressBar from '../electerm-react/components/sftp/address-bar' -import isValidPath from '../electerm-react/common/is-valid-path' -import { - typeMap -} from '../electerm-react/common/constants' -import { resolve } from '../web-components/path' -import './file-select-dialog.styl' - -const s = window.translate - -export default class FileSelectDialog extends Component { - constructor (props) { - super(props) - const p = window.localStorage.getItem(this.lsKey) || window.et.home - this.state = { - opts: null, - isSaveDialog: false, - saveFileName: '', - loading: false, - page: 1, - localShowHiddenFile: false, - localPathHistory: [], - fileSelected: null, - selectedFiles: [], - lastClickedIndex: null, - pageSize: 100, - localInputFocus: false, - list: [], - localPathTemp: p, - localPath: p - } - } - - componentDidMount () { - window.addEventListener('message', this.handleMsg) - } - - componentWillUnmount () { - window.removeEventListener('message', this.handleMsg) - } - - lsKey = 'dialog-start-path' - - fileInputRef = null - - handleBrowserUpload = () => { - if (this.fileInputRef) { - this.fileInputRef.click() - } - } - - handleBrowserFileChange = (e) => { - const file = e.target.files[0] - if (!file) return - const reader = new FileReader() - reader.onload = (evt) => { - const fileContent = evt.target.result - const fileName = file.name - this.setState({ opts: null }) - window.postMessage({ - type: 'handleDialog', - data: { fileContent, fileName } - }, '*') - } - reader.readAsText(file) - e.target.value = '' - } - - handleBrowserDownload = () => { - const { opts } = this.state - const { filename, content } = opts - const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - this.handleClose() - } - - handleMsg = (e) => { - if (e?.data?.type === 'openDialog') { - this.setState({ opts: e.data.data, isSaveDialog: false, saveFileName: '' }, this.localList) - } else if (e?.data?.type === 'saveDialog') { - const opts = e.data.data || {} - const defaultName = opts.defaultPath || '' - this.setState({ opts, isSaveDialog: true, saveFileName: defaultName }, this.localList) - } - } - - handlePageChange = (page, pageSize) => { - this.setState({ page, pageSize, lastClickedIndex: null }) - } - - handlePageSizeChange = (k, pageSize) => { - this.setState({ pageSize }) - } - - handleLocalPathChange = (e) => { - this.setState({ localPath: e.target.value }) - } - - handleClose = () => { - const { isSaveDialog } = this.state - if (isSaveDialog) { - window.postMessage({ - type: 'closeSaveDialog' - }, '*') - } else { - window.postMessage({ - type: 'closeDialog' - }, '*') - } - this.setState({ opts: null }) - } - - isMultiSelectMode = () => { - const { opts, isSaveDialog } = this.state - const properties = opts?.properties || [] - return !isSaveDialog && - properties.includes('openFile') && - properties.includes('multiSelections') - } - - handleSubmit = () => { - const { selectedFiles, fileSelected, localPath, isSaveDialog, saveFileName } = this.state - if (isSaveDialog) { - const name = saveFileName.trim() - if (!name) { - return notification.warning({ message: 'Please enter a file name' }) - } - const filePath = resolve(localPath, name) - this.setState({ opts: null }) - window.postMessage({ - type: 'handleSaveDialog', - data: { canceled: false, filePath } - }, '*') - return - } - if (selectedFiles.length) { - const paths = selectedFiles.map(f => resolve(localPath, f.name)) - this.setState({ opts: null }) - window.postMessage({ - type: 'handleDialog', - data: paths - }, '*') - return - } - const p = fileSelected - ? resolve(localPath, fileSelected.name) - : localPath - this.setState({ - opts: null - }) - window.postMessage({ - type: 'handleDialog', - data: [p] - }, '*') - } - - localList = async () => { - this.setState({ - loading: true, - fileSelected: null, - selectedFiles: [], - lastClickedIndex: null - }) - const { - localPath, - opts, - isSaveDialog - } = this.state - const properties = opts?.properties || [] - const func = !isSaveDialog && properties.includes('openDirectory') - ? window.fs.readdirOnly - : window.fs.readdirAndFiles - const list = await func(localPath) - .catch((err) => { - console.log(err) - return [] - }) - this.updateLs(localPath) - this.setState({ list, loading: false, page: 1 }) - } - - onChange = e => { - this.setState({ - localPathTemp: e.target.value - }) - } - - onInputBlur = (type) => { - this.inputFocus = false - this.timer4 = setTimeout(() => { - this.setState({ - [type + 'InputFocus']: false - }) - }, 200) - } - - onInputFocus = (type) => { - this.setState({ - [type + 'InputFocus']: true - }) - this.inputFocus = true - } - - onGoto = (type, e) => { - e && e.preventDefault() - const n = `${type}Path` - const nt = n + 'Temp' - const np = this.state[nt] - if (!isValidPath(np)) { - return notification.warning({ - message: 'path not valid' - }) - } - this.updateLs(np) - this.setState({ - [n]: np - }, this[`${type}List`]) - } - - updateLs = (np = this.state.localPath) => { - window.localStorage.setItem(this.lsKey, np) - } - - toggleShowHiddenFile = type => { - const prop = `${type}ShowHiddenFile` - const b = this.state[prop] - this.setState({ - [prop]: !b - }) - } - - onClickHistory = (type, path) => { - const n = `${type}Path` - this.setState({ - [n]: path, - [`${n}Temp`]: path - }, this[`${type}List`]) - } - - goParent = (type) => { - const n = `${type}Path` - const p = this.state[n] - const np = resolve(p, '..') - if (np !== p) { - this.updateLs(np) - this.setState({ - [n]: np, - [n + 'Temp']: np - }, this[`${type}List`]) - } - } - - handleClickFile = (item, index, event) => { - const { isSaveDialog } = this.state - if (isSaveDialog) { - if (!item.isDirectory) { - this.setState({ - fileSelected: item, - saveFileName: item.name, - selectedFiles: [item] - }) - } else { - this.setState({ - fileSelected: item, - selectedFiles: [item] - }) - } - return - } - if (!this.isMultiSelectMode()) { - this.setState({ - fileSelected: item, - selectedFiles: [item], - lastClickedIndex: index - }) - return - } - // multi-select file mode - const { selectedFiles, lastClickedIndex, list } = this.state - const shift = event?.shiftKey - const meta = event?.metaKey || event?.ctrlKey - if (shift && lastClickedIndex !== null) { - const start = Math.min(lastClickedIndex, index) - const end = Math.max(lastClickedIndex, index) - const range = list.slice(start, end + 1) - this.setState({ - selectedFiles: range, - fileSelected: item - }) - event?.preventDefault?.() - } else if (meta) { - const exists = selectedFiles.some(f => f.name === item.name) - const next = exists - ? selectedFiles.filter(f => f.name !== item.name) - : [...selectedFiles, item] - this.setState({ - selectedFiles: next, - fileSelected: next.length ? item : null, - lastClickedIndex: index - }) - event?.preventDefault?.() - } else { - this.setState({ - selectedFiles: [item], - fileSelected: item, - lastClickedIndex: index - }) - } - } - - handleDbClickFile = (item) => { - if (!item.isDirectory) { - return false - } - const { localPath } = this.state - const np = resolve(localPath, item.name) - this.setState({ - localPath: np, - localPathTemp: np - }, this.localList) - } - - renderSaveInput () { - const { - isSaveDialog, - saveFileName - } = this.state - if (!isSaveDialog) { - return null - } - return ( -
- } - onChange={e => this.setState({ saveFileName: e.target.value })} - /> -
- ) - } - - renderHeader () { - const { - localPath, - localPathTemp, - loading, - localPathHistory, - localInputFocus, - localShowHiddenFile - } = this.state - const props = { - type: typeMap.local, - onChange: this.onChange, - onInputBlur: this.onInputBlur, - onInputFocus: this.onInputFocus, - onGoto: this.onGoto, - localInputFocus, - localPath, - localShowHiddenFile, - toggleShowHiddenFile: this.toggleShowHiddenFile, - localPathTemp, - onClickHistory: this.onClickHistory, - goParent: this.goParent, - localPathHistory, - loadingSftp: loading - } - return ( -
- -
- ) - } - - renderFooter () { - const e = window.translate - const { - isSaveDialog, - selectedFiles - } = this.state - const opts = this.state.opts - const properties = opts?.properties || [] - const disabled = !isSaveDialog && - properties.includes('openFile') && - selectedFiles.length === 0 - const noBrowserTransfer = opts?.noBrowserTransfer - const showBrowserUpload = !noBrowserTransfer && !isSaveDialog && properties.includes('openFile') - const showBrowserDownload = !noBrowserTransfer && !isSaveDialog && opts?.content - return ( -
-
- {this.renderPager()} - {showBrowserUpload && ( - - )} - {showBrowserDownload && ( - - )} -
-
- - -
-
- ) - } - - renderList () { - const { list, selectedFiles, page, pageSize } = this.state - const all = list.slice((page - 1) * pageSize, page * pageSize) - const offset = (page - 1) * pageSize - const selectedNames = new Set(selectedFiles.map(f => f.name)) - return ( -
- { - all.map((item, i) => { - const index = offset + i - return ( - this.handleClickFile(file, index, ev)} - /> - ) - }) - } -
- ) - } - - renderPager () { - const { - page, - pageSize, - list - } = this.state - const len = list.length - if (len <= pageSize) { - return null - } - return ( - - ) - } - - renderContent = () => { - const { - opts, - loading, - isSaveDialog - } = this.state - const props = { - maskClosable: false, - open: true, - width: 'min(800px, 90vw)', - title: opts.title || (isSaveDialog ? 'Save As' : 'Open'), - footer: this.renderFooter(), - onCancel: this.handleClose, - wrapClassName: 'file-select-modal' - } - return ( - - { this.fileInputRef = r }} - className='hide' - onChange={this.handleBrowserFileChange} - /> - - - {this.renderSaveInput()} - {this.renderHeader()} - {this.renderList()} - - - - ) - } - - render () { - const { - opts - } = this.state - if (!opts) { - return null - } - return this.renderContent() - } -} diff --git a/src/client/file-select-dialog/file-select-dialog.styl b/src/client/file-select-dialog/file-select-dialog.styl deleted file mode 100644 index 0318515..0000000 --- a/src/client/file-select-dialog/file-select-dialog.styl +++ /dev/null @@ -1,35 +0,0 @@ - -.dialog-file-item - user-select none - padding 6px 10px - &:hover - background-color var(--primary) - color var(--primary-contrast) - &.selected - background-color var(--primary) - color var(--primary-contrast) -.file-dialog-list-wrap - height calc(min(600px, 90vh) - 200px) - overflow-y auto -.file-dialog-header - .sftp-title - .anticon-eye-invisible - .anticon-home - .anticon-plus - display none -.file-select-dialog-footer - display flex - justify-content space-between - align-items center - flex-wrap wrap - gap 8px -.file-select-dialog-footer-actions - display flex - align-items center - flex-wrap wrap - gap 8px -.file-select-dialog-footer-submit - display flex - align-items center - gap 8px - margin-left auto \ No newline at end of file diff --git a/src/client/simple-auth/logout.jsx b/src/client/simple-auth/logout.jsx deleted file mode 100644 index ef2b16b..0000000 --- a/src/client/simple-auth/logout.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { auto } from 'manate/react' -import { - LogoutOutlined -} from '@ant-design/icons' -import './logout.styl' - -export default auto(function Logout (props) { - const handleLogout = () => { - window.localStorage.removeItem('tokenElecterm') - props.store.logined = false - } - - if (window.et.tokenElecterm) { - return null - } - - return ( -
- -
- ) -}) diff --git a/src/client/simple-auth/logout.styl b/src/client/simple-auth/logout.styl deleted file mode 100644 index eaa9b0d..0000000 --- a/src/client/simple-auth/logout.styl +++ /dev/null @@ -1,8 +0,0 @@ -.logout-icon - position fixed - left 0 - bottom 0 - z-index 100 - width 43px - height 48px - text-align center \ No newline at end of file diff --git a/src/client/simple-auth/web-login.jsx b/src/client/simple-auth/web-login.jsx deleted file mode 100644 index 7cd0a58..0000000 --- a/src/client/simple-auth/web-login.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import { auto } from 'manate/react' -import { useState, useEffect, useRef } from 'react' -import LogoElem from '../electerm-react/components/common/logo-elem.jsx' -import { - Input, - Spin -} from 'antd' -import message from '../electerm-react/components/common/message' -import { - ArrowRightOutlined, - Loading3QuartersOutlined -} from '@ant-design/icons' -import Main from '../electerm-react/components/main/main.jsx' - -const f = window.translate - -export default auto(function Login ({ store }) { - const [pass, setPass] = useState('') - const submitting = useRef(false) - - useEffect(() => { - store.getConstants() - }, []) - - const handlePassChange = e => { - setPass(e.target.value) - } - - const handleSubmit = async () => { - if (!pass) { - return message.warning('password required') - } else if (submitting.current) { - return - } - submitting.current = true - await store.login(pass) - submitting.current = false - } - - const renderUnchecked = () => { - return ( - -
- -
- -
- -
- ) - } - - const renderAfter = () => { - return ( - - ) - } - - const renderLogin = () => { - const { - logining, - fetchingUser - } = store - - return ( - -
- -
- -
- -
- -
- -
- ) - } - - if (!store.authChecked) { - return renderUnchecked() - } else if (!store.logined) { - return renderLogin() - } - - return ( -
- ) -}) diff --git a/src/client/statics/favicon.ico b/src/client/statics/favicon.ico deleted file mode 100644 index f808f3a29e79df183a7e18a1c8e0a7547c2f94f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYHS0yw${w*&EeApWALqGd;n z8-Qy6q92KfByjvJCJ@50GWvmboNexyP>0JGFS=Beuz#42@}Uh zgA4%4BXnW1@aZR{7u_^qeu)9%dLTAIm!k&%KhP)t`xzMiFf%ZGU}Ru805r4#ih&dy UG=SuQ_yJJe4|bp)kUk&=0H7L+MgRZ+ diff --git a/src/client/web-components/path.js b/src/client/web-components/path.js deleted file mode 100644 index b25a876..0000000 --- a/src/client/web-components/path.js +++ /dev/null @@ -1,57 +0,0 @@ -export function join (...parts) { - const { isWin } = window.et - const separator = isWin ? '\\' : '/' - const joined = parts.join(separator) - const regex = new RegExp(`${separator}{2,}`, 'g') - return joined.replace(regex, separator) -} - -export function resolve (...paths) { - const { isWin } = window.et - const separator = isWin ? '\\' : '/' - const resolved = [] - - let root = '' - if (paths[0].startsWith(separator)) { - root = separator - paths[0] = paths[0].slice(1) - } else if (paths[0].match(/^[a-zA-Z]+:/)) { - root = paths.shift() + separator - } - const len = paths.length - if (paths[len - 1].endsWith(separator)) { - paths[len - 1] = paths[len - 1].slice(0, -1) - } - - for (const path of paths) { - if (typeof path !== 'string') { - throw new TypeError(`Invalid argument type: ${typeof path}`) - } - - const parts = path.split(separator).filter(d => d) - - for (const part of parts) { - if (part === '') { - resolved.length = 0 - break - } else if (part === '.') { - continue - } else if (part === '..') { - resolved.pop() - } else { - resolved.push(part) - } - } - } - - return `${root}${resolved.join(separator)}` -} - -export function basename (path, ext) { - const { isWin } = window.et - const separator = isWin ? '\\' : '/' - const parts = path.split(separator).filter(d => d) - const lastPart = parts[parts.length - 1] - const basename = ext ? lastPart.slice(0, -ext.length) : lastPart - return basename -} diff --git a/src/client/web-components/store-login.js b/src/client/web-components/store-login.js deleted file mode 100644 index 3d97d5d..0000000 --- a/src/client/web-components/store-login.js +++ /dev/null @@ -1,51 +0,0 @@ -import Fetch from '../electerm-react/common/fetch.jsx' -import { initWsCommon } from '../electerm-react/common/fetch-from-server.js' - -export default Store => { - Store.prototype.getConstants = async function () { - const { store } = window - store.fetchingUser = true - const res = await Fetch.get('/api/get-constants', null, { - handleErr: console.log - }) - if (res) { - Object.assign(window.pre, res) - window.reqs.fs.constants = window.pre.fsConstants - store.updateConfig(res.config) - await initWsCommon() - Object.assign(store, { - logined: true, - authChecked: true, - fetchingUser: false, - logining: false - }) - return true - } else { - console.log('getConstants err') - store.authChecked = true - Object.assign(store, { - authChecked: true, - logined: false, - fetchingUser: false - }) - return false - } - } - Store.prototype.login = async function (password) { - const { store } = window - store.logining = true - const res = await Fetch.post('/api/login', { - password - }) - if (res) { - store.updateConfig({ - tokenElecterm: res - }) - window.localStorage.setItem('tokenElecterm', res) - store.getConstants() - } - Object.assign(store, { - logining: false - }) - } -} diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl deleted file mode 100644 index f2d432e..0000000 --- a/src/client/web-components/style-overide.styl +++ /dev/null @@ -1,36 +0,0 @@ -@-moz-document url-prefix() - .tabs-inner - overflow-x hidden !important - -// Fix custom modal not rendering content on Android WebView. -// -// Root cause: the original .custom-modal-wrap is position:fixed WITH -// overflow:auto. On Android WebView, overflow on a position:fixed element -// does not render its children — the mask shows but the content card is -// invisible. Changing overflow to hidden (previous attempt) did not help -// because even overflow:hidden on position:fixed can trigger the same bug. -// -// Fix: switch .custom-modal-wrap and .custom-modal-mask from position:fixed -// to position:absolute. The original overflow:auto / position:relative / -// min-height:100% from modal.styl all work correctly on position:absolute -// elements in Android WebView. Since the modal covers the full viewport -// (top/left/right/bottom:0) and the app body does not scroll, absolute -// behaves identically to fixed here. -// -// Use `body` prefix for specificity: this override is imported early (in -// basic.js, before the app bundle), while modal.styl is imported by the Modal -// component (loaded later). With equal specificity the later rule wins, so we -// need the extra `body` ancestor to raise specificity above modal.styl. -// `body` covers both modals inside #container AND Modal.info()/Modal.confirm() -// instances that are appended directly to document.body. -body .custom-modal-wrap - position absolute - -body .custom-modal-mask - position absolute - -body .custom-modal-container - position relative - -body .custom-modal-content - position relative diff --git a/src/client/web-components/web-api.js b/src/client/web-components/web-api.js deleted file mode 100644 index b9ec332..0000000 --- a/src/client/web-components/web-api.js +++ /dev/null @@ -1,135 +0,0 @@ -// window preload -// import { message } from 'antd' - -window.api = { - fetch: (url, options = {}) => { - const headers = { - token: window.store?.config.tokenElecterm, - ...options.headers - } - return window.fetch(url, { ...options, headers }) - .then(res => { - if (res.status > 304) { - return res.json() - .catch(() => ({})) - .then(data => { - throw new Error(data.error || `Request failed (${res.status})`) - }) - } - return res - }) - }, - getZoomFactor: () => 1, - setZoomFactor: (nl) => { - // message.info('Set ZoomFactor not supported') - }, - openDialog: (opts) => { - return new Promise((resolve, reject) => { - window.et.handleDialogEvent = (e) => { - if (e?.data?.type === 'handleDialog') { - window.removeEventListener('message', window.et.handleDialogEvent) - delete window.et.handleDialogEvent - resolve(e.data.data) - } else if (e?.data?.type === 'closeDialog') { - window.removeEventListener('message', window.et.handleDialogEvent) - delete window.et.handleDialogEvent - resolve(false) - } - } - window.addEventListener('message', window.et.handleDialogEvent) - window.postMessage({ - type: 'openDialog', - data: opts - }, '*') - }) - }, - saveDialog: (opts) => { - return new Promise((resolve, reject) => { - window.et.handleSaveDialogEvent = (e) => { - if (e?.data?.type === 'handleSaveDialog') { - window.removeEventListener('message', window.et.handleSaveDialogEvent) - delete window.et.handleSaveDialogEvent - resolve(e.data.data) - } else if (e?.data?.type === 'closeSaveDialog') { - window.removeEventListener('message', window.et.handleSaveDialogEvent) - delete window.et.handleSaveDialogEvent - resolve({ canceled: true, filePath: '' }) - } - } - window.addEventListener('message', window.et.handleSaveDialogEvent) - window.postMessage({ - type: 'saveDialog', - data: opts - }, '*') - }) - }, - ipcOnEvent: (event, cb) => { - - }, - ipcOffEvent: (event, cb) => { - - }, - runGlobalAsync: async (func, ...args) => { - if (func === 'initCommandLine') { - try { - const { init } = window.et.query - return init ? JSON.parse(window.et.query.init) : null - } catch (err) { - console.log('initCommandLine error:', err) - } - } else if (func === 'setTitle') { - document.title = args[0] - return - } else if (func === 'openNewInstance') { - return window.open(args[0], '_blank') - } else if (func === 'closeApp') { - return window.close() - } else if (func === 'restart') { - return window.location.reload() - } else if (func === 'init') { - const d = await window.wsFetch({ - action: 'runSync', - args, - func - }) - d.config.tokenElecterm = window.localStorage.getItem('tokenElecterm') || '' - return d - } - return window.wsFetch({ - action: 'runSync', - args, - func - }) - }, - sendMcpResponse: data => { - window.et.commonWs.s({ - type: 'mcp-response-back', - ...data - }) - }, - runSync: (func, ...args) => { - if (func === 'isMaximized') { - return false - } else if (func === 'isSecondInstance') { - return false - } else if (func === 'windowMove') { - return false - } else if (func === 'getLoadTime' || func === 'setLoadTime') { - return 0 - } else if (func === 'getInitTime') { - if (window.et.initTime !== undefined) { - return window.et.initTime - } else { - window.et.initTime = Date.now() - return window.et.initTime - } - } else if (func === 'nodePtyCheck') { - return window.et.hasNodePty - } - return window.wsFetch({ - action: 'runSync', - args, - func - }) - } -} diff --git a/src/client/web-components/web-main.jsx b/src/client/web-components/web-main.jsx deleted file mode 100644 index df4370c..0000000 --- a/src/client/web-components/web-main.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import ErrorBoundary from '../electerm-react/components/main/error-wrapper' -import Login from '../simple-auth/web-login' -import store from './web-store' -import FileSelectDialog from '../file-select-dialog/file-select-dialog' -import Logout from '../simple-auth/logout' -export default function MainEntry () { - return ( - - - - - - ) -} diff --git a/src/client/web-components/web-pre.js b/src/client/web-components/web-pre.js deleted file mode 100644 index 683c7f2..0000000 --- a/src/client/web-components/web-pre.js +++ /dev/null @@ -1,257 +0,0 @@ -import * as path from './path.js' -import message from '../electerm-react/components/common/message' - -const { - ipcOnEvent, - ipcOffEvent, - runGlobalAsync, - getZoomFactor, - setZoomFactor, - runSync -} = window.api - -// Encoding function -function encodeUint8Array (uint8Array) { - let str = '' - const len = uint8Array.byteLength - - for (let i = 0; i < len; i++) { - str += String.fromCharCode(uint8Array[i]) - } - - return btoa(str) -} - -// Decoding function -function decodeBase64String (base64String) { - const str = atob(base64String) - const len = str.length - - const uint8Array = new Uint8Array(len) - - for (let i = 0; i < len; i++) { - uint8Array[i] = str.charCodeAt(i) - } - - return uint8Array -} - -window.log = window.console - -// Fallback clipboard copy using execCommand, for Android WebView where -// navigator.clipboard.writeText() may fail silently (non-secure http -// scheme, missing user-gesture context from antd Dropdown menu clicks, -// or Promise rejection that the try/catch does not catch). -// execCommand('copy') uses the WebView's internal clipboard mechanism -// which is connected to the Android system ClipboardManager. -function execCommandCopy (str) { - const textarea = document.createElement('textarea') - textarea.value = str - textarea.setAttribute('readonly', '') - textarea.style.position = 'fixed' - textarea.style.left = '-9999px' - textarea.style.top = '0' - textarea.style.opacity = '0' - document.body.appendChild(textarea) - textarea.focus() - textarea.select() - // For iOS Safari compatibility - textarea.setSelectionRange(0, str.length) - let ok = false - try { - ok = document.execCommand('copy') - } catch (e) { - // ignore - } - document.body.removeChild(textarea) - return ok -} - -window.pre = { - resolve: (...args) => { - return path.resolve(...args.map(d => d || '')) - }, - transferKeys: [ - 'pause', - 'resume', - 'destroy' - ], - // Safe defaults for API-dependent data to prevent render crashes - // before /api/get-constants response arrives (fixes Android info-modal - // showing only background with no content) - osInfoData: [], - osInfo: () => { return window.pre.osInfoData || [] }, - extIconPath: window.et.extIconPath, - readClipboard: () => { - return window.et.clipboard || '' - }, - - writeClipboard: str => { - window.et.clipboard = str - if (!navigator.clipboard) { - // navigator.clipboard not available — use execCommand fallback - // (works in Android WebView via the system ClipboardManager) - if (!execCommandCopy(str)) { - message.error('Clipboard API not available') - } - return - } - try { - const promise = navigator.clipboard.writeText(str) - // Handle Promise rejection — the try/catch above only catches - // synchronous errors, not async rejections. On Android WebView, - // writeText() may reject because the page is served over http:// - // (not a secure context) or the user-gesture requirement is not - // satisfied from a Dropdown menu click. - if (promise && typeof promise.catch === 'function') { - promise.catch(() => { - execCommandCopy(str) - }) - } - return promise - } catch (err) { - // Synchronous error — try execCommand fallback - if (!execCommandCopy(str)) { - message.error('Failed to copy text: ' + err) - } - } - }, - readClipboardSync: function readClipboard () { - if (!navigator.clipboard) { - // Fallback: return in-memory clipboard value (may be stale if the - // user copied via the WebView's native text selection, but there - // is no synchronous clipboard read API available in this case). - return window.et.clipboard || '' - } - try { - return navigator.clipboard.readText() - } catch (err) { - // Fallback: return in-memory clipboard value - return window.et.clipboard || '' - } - }, - - // writeClipboard: function writeClipboard (str) { - // if (!navigator.clipboard) { - // message.error('Clipboard API not available') - // return - // } - // try { - // return navigator.clipboard.writeText(str) - // } catch (err) { - // message.error('Failed to copy text: ' + err) - // } - // }, - showItemInFolder: (href) => runSync('showItemInFolder', href), - ipcOnEvent, - ipcOffEvent, - getZoomFactor, - setZoomFactor, - openExternal: (url) => { - window.open(url, '_blank') - }, - runSync, - runGlobalAsync, - versions: {} -} - -// Ensure window.et.packInfo has all fields required by info-modal.jsx -// On Android/Capacitor the packInfo is minimal and missing author/bugs/releases/etc. -const _packInfoDefaults = { - author: { - name: 'ZHAO Xudong', - email: 'zxdong@gmail.com', - url: 'https://github.com/zxdong262' - }, - homepage: 'https://electerm.org', - bugs: { - url: 'https://github.com/electerm/electerm/issues' - }, - releases: 'https://github.com/electerm/electerm/releases', - sponsorLink: 'https://electerm.org/sponsor-electerm/', - knownIssuesLink: 'https://github.com/electerm/electerm/wiki/Known-issues', - langugeRepo: 'https://github.com/electerm/electerm-languages' -} -if (window.et.packInfo) { - window.et.packInfo = { - ...window.et.packInfo, - ..._packInfoDefaults - } -} - -const fs = { - stat: (path, cb) => { - window.fs.statCustom(path) - .catch(err => cb(err)) - .then(obj => { - obj.isDirectory = () => obj.isD - obj.isFile = () => obj.isF - cb(undefined, obj) - }) - }, - access: (...args) => { - const cb = args.pop() - window.fs.access(...args) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - open: (...args) => { - const cb = args.pop() - window.fs.openCustom(...args) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - read: (p1, arr, ...args) => { - const cb = args.pop() - window.fs.readCustom( - p1, - encodeUint8Array(arr), - ...args - ) - .then((data) => { - const { n, newArr } = data - const newArr1 = decodeBase64String(newArr) - cb(undefined, n, newArr1) - }) - .catch(err => cb(err)) - }, - close: (fd, cb) => { - window.fs.closeCustom(fd) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - readdir: (p, cb) => { - window.fs.readdir(p) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - mkdir: (...args) => { - const cb = args.pop() - window.fs.mkdir(...args) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - write: (p1, buf, cb) => { - window.fs.writeCustom(p1, encodeUint8Array(buf)) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - }, - realpath: (p, cb) => { - window.fs.realpath(p) - .then((data) => cb(undefined, data)) - .catch((err) => cb(err)) - } -} - -window.reqs = { - path, - fs -} - -function require (name) { - return window.reqs[name] -} - -require.resolve = name => name - -window.require = require diff --git a/src/client/web-components/web-store.js b/src/client/web-components/web-store.js deleted file mode 100644 index 65ab728..0000000 --- a/src/client/web-components/web-store.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * central state store powered by manate - https://github.com/tylerlong/manate - */ - -import { manage } from 'manate' -import initState from '../electerm-react/store/init-state' -import { StateStore } from '../electerm-react/store/store' -import loginExtend from './store-login' - -class Store extends StateStore { - constructor () { - super() - Object.assign( - this, - initState, - { - logined: false, - authChecked: false, - fetchingUser: false, - logining: false, - height: window.innerHeight, - _config: { - tokenElecterm: window.localStorage.getItem('tokenElecterm') || '' - } - } - ) - } -} - -Store.prototype.initMcpHandler = function () { - // Listen for MCP requests from main process - window.et.commonWs.addEventListener('message', (e) => { - if (e && - e.data && - typeof e.data === 'string' && - e.data.startsWith('{') && - e.data.endsWith('}') && - JSON.parse(e.data).type === 'mcp-request' - ) { - const { requestId, action, data } = JSON.parse(e.data) - if (action === 'tool-call') { - window.store.handleMcpToolCall(requestId, data.toolName, data.args) - } - } - }) -} - -loginExtend(Store) - -const store = manage(new Store()) - -window.store = store -export default store From 7cdbcc0a6c28f68e14439d354582cc8fb5902e98 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 16:25:13 +0800 Subject: [PATCH 43/52] fix3 --- .github/workflows/build-web.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 343040a..ae2b56b 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -10,6 +10,7 @@ on: push: branches: - dev2 + - build workflow_dispatch: # Cancel previous runs on the same branch From 4d15c8d8a975caeffa8db469302bc839f9067ddd Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 16:25:53 +0800 Subject: [PATCH 44/52] Fix 4 --- .gitignore | 1 + package-lock.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 268b16c..00fd44d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ entry/oh_modules/ # generated local SDK paths (written by scripts/build-web-app.sh) local.properties build/.verify-tmp +/src \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bc38fc3..ae27c43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "electerm-android", + "name": "electerm-harmony", "version": "5.3.16", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "electerm-android", + "name": "electerm-harmony", "version": "5.3.16", "hasInstallScript": true, "license": "MIT", From 8e62302bb9027c134496920a6667996fd0b4d604 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:00:27 +0800 Subject: [PATCH 45/52] Fix db wrapper --- .github/workflows/build-web.yml | 5 + .github/workflows/build.yml | 335 ------------------------- build/bin/install.js | 28 ++- build/replace/app/lib/db.js | 17 ++ build/replace/app/lib/nedb.js | 122 +++++++++ build/replace/app/lib/safe-storage.js | 48 ++++ package-lock.json | 29 +++ package.json | 3 +- scripts/inject-safe-storage-secret.mjs | 17 ++ 9 files changed, 265 insertions(+), 339 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 build/replace/app/lib/db.js create mode 100644 build/replace/app/lib/nedb.js create mode 100644 build/replace/app/lib/safe-storage.js create mode 100644 scripts/inject-safe-storage-secret.mjs diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ae2b56b..d41cbae 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -178,6 +178,11 @@ jobs: path: .cache/node-runtime key: ohos-node-${{ env.NODE_VERSION }} + - name: Inject safe-storage secret + env: + STORAGE_SECRET: ${{ secrets.OHOS_SERVER_SECRET }} + run: node scripts/inject-safe-storage-secret.mjs + # ── Step 3: Build web app (frontend + backend bundle → resfile) ────── - name: Prepare web app run: ./scripts/prepare-web.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 0ddb1e0..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,335 +0,0 @@ -name: Build HarmonyOS APP - -on: - push: - branches: - - build - - dev - - dev1 - # NOTE: dev2 intentionally excluded — that branch only builds the - # web variant (build-web.yml). Keeping it here would fire the full - # HarmonyOS APP build on every dev2 push, which we don't want. - -# Cancel previous runs on the same branch/tag -concurrency: - group: build-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -# Note: The Node.js runtime (libnode.so) is downloaded from the -# electerm/electerm-harmony release published manually via -# temp/bak/publish-node-release.sh — see the "Prepare Node.js runtime (arm64)" -# step below. - -jobs: - build: - # HarmonyOS Command Line Tools are x64-only. - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - # ── Checkout ────────────────────────────────────────────────────────── - - name: Checkout electerm-harmony - uses: actions/checkout@v4 - - # ── Setup Node.js (for building the web app) ───────────────────────── - - name: Setup Node.js 24 - uses: actions/setup-node@v4 - with: - node-version: '24' - - # ── Install system deps for native modules ──────────────────────────── - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - unzip \ - jq \ - python3 \ - make \ - g++ \ - libsecret-1-dev - - # ── Setup JDK (for hap-sign-tool.jar) ──────────────────────────────── - - name: Setup JDK 21 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - # ── Step 1: Prepare Node.js runtime ──────────────────────────────────── - # Downloads our own real shared libnode.so (built with --shared via - # scripts/build-node-ohos.sh, archived in temp/bak/, published manually - # via temp/bak/publish-node-release.sh) from the electerm/electerm-harmony - # release. arm64-v8a is the device ABI for phones/tablets/2in1; this is - # the half that used to be a dlopen-crashing PIE executable - # (hqzing/ohos-node). - - name: Prepare Node.js runtime (arm64) - run: ./scripts/prepare-node.sh arm64 - - # ── Step 2: Build web app (frontend + backend bundle) ─────────── - - name: Prepare web app - run: ./scripts/prepare-web.sh - env: - OHOS_SERVER_SECRET: ${{ secrets.OHOS_SERVER_SECRET }} - - # ── Step 3: Cache / download HarmonyOS Command Line Tools (~2 GB) ──── - - name: Compute Command Line Tools cache key - id: cmdkey - env: - OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} - run: | - if [ -z "${OHOS_CMDLINE_TOOLS_URL}" ]; then - echo "::error::OHOS_CMDLINE_TOOLS_URL secret is not set." - exit 1 - fi - # Hash the URL so the cache invalidates automatically when the - # secret points to a new version of the tools. - HASH="$(echo -n "${OHOS_CMDLINE_TOOLS_URL}" | md5sum | cut -d' ' -f1)" - echo "key=cmdline-tools-${HASH}" >> "$GITHUB_OUTPUT" - - - name: Restore HarmonyOS Command Line Tools cache - id: cmdline_cache - uses: actions/cache/restore@v4 - with: - path: .cache/commandline-tools - key: ${{ steps.cmdkey.outputs.key }} - - - name: Download & extract HarmonyOS Command Line Tools - if: steps.cmdline_cache.outputs.cache-hit != 'true' - env: - OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} - run: | - set -euo pipefail - mkdir -p .cache - ZIP=".cache/commandline-tools.zip" - echo "Cache miss — downloading HarmonyOS Command Line Tools (~2 GB) ..." - curl -L --retry 10 --retry-all-errors --retry-delay 5 -C - \ - -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ - -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" - echo "Verifying archive integrity ..." - if ! unzip -t "$ZIP" >/dev/null 2>&1; then - echo "::error::Downloaded archive is corrupt; retrying once without resume." - curl -L --retry 10 --retry-all-errors --retry-delay 5 \ - -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ - -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" - unzip -t "$ZIP" >/dev/null 2>&1 || { echo "::error::Still corrupt after retry"; exit 1; } - fi - rm -rf .cache/commandline-tools - mkdir -p .cache/commandline-tools - unzip -o -q "$ZIP" -d .cache/commandline-tools - rm -f "$ZIP" - echo "Downloaded and extracted HarmonyOS Command Line Tools" - - - name: Save HarmonyOS Command Line Tools cache - if: steps.cmdline_cache.outputs.cache-hit != 'true' && always() - uses: actions/cache/save@v4 - with: - path: .cache/commandline-tools - key: ${{ steps.cmdkey.outputs.key }} - - - name: Configure Command Line Tools environment - run: | - set -euo pipefail - COMMANDLINE_TOOLS="$(pwd)/.cache/commandline-tools/command-line-tools" - if [ ! -d "$COMMANDLINE_TOOLS" ]; then - COMMANDLINE_TOOLS="$(cd "$(dirname "$(find .cache/commandline-tools -name ohpm -type f | head -1)")/.." && pwd)" - fi - # Fix: Project root package.json has "type": "module", which makes - # Node.js treat hvigorw.js as an ES module (breaks with "require is - # not defined"). Adding a CommonJS package.json to the tools dirs - # prevents Node from traversing up to the project root. - echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/hvigor/package.json" 2>/dev/null || true - echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/package.json" - echo "COMMANDLINE_TOOLS=$COMMANDLINE_TOOLS" >> "$GITHUB_ENV" - echo "OHOS_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" - echo "DEVECO_NODE_HOME=$COMMANDLINE_TOOLS/tool/node" >> "$GITHUB_ENV" - echo "DEVECO_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" - echo "$COMMANDLINE_TOOLS/bin" >> "$GITHUB_PATH" - echo "$COMMANDLINE_TOOLS/hvigor/bin" >> "$GITHUB_PATH" - echo "HarmonyOS Command Line Tools ready at $COMMANDLINE_TOOLS" - - - name: Configure ohpm registry - run: | - ohpm config set registry https://ohpm.openharmony.cn/ohpm/ || true - ohpm --version || true - - - name: Restore ohpm modules cache - id: ohpm_cache - uses: actions/cache/restore@v4 - with: - path: | - oh_modules - entry/oh_modules - ~/.ohpm - key: ohpm-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} - restore-keys: | - ohpm- - - # ── Step 4: Decode signing materials ──────────────────────────────── - - name: Decode signing materials - env: - OHOS_KEYSTORE_B64: ${{ secrets.OHOS_KEYSTORE_B64 }} - OHOS_CERT_B64: ${{ secrets.OHOS_CERT_B64 }} - OHOS_PROFILE_B64: ${{ secrets.OHOS_PROFILE_B64 }} - run: | - mkdir -p signing - if [ -z "${OHOS_KEYSTORE_B64}" ] || [ -z "${OHOS_CERT_B64}" ] || [ -z "${OHOS_PROFILE_B64}" ]; then - echo "One or more signing material secrets are not set" - exit 1 - fi - echo "${OHOS_KEYSTORE_B64}" | base64 -d > signing/electerm.p12 - echo "${OHOS_CERT_B64}" | base64 -d > signing/electerm_publish.cer - echo "${OHOS_PROFILE_B64}" | base64 -d > signing/electermRelease.p7b - - ls -la signing/ - for f in signing/electerm.p12 signing/electerm_publish.cer signing/electermRelease.p7b; do - if [ ! -s "${f}" ]; then - echo "::error::Failed to decode ${f} — check GitHub Secrets." - exit 1 - fi - echo " ✓ $(basename ${f}): $(du -h ${f} | cut -f1)" - done - - # ── Step 5: Set bundle name from secret ────────────────────────────── - - name: Configure bundle name - env: - BUNDLE_NAME: ${{ secrets.OHOS_BUNDLE_NAME }} - run: | - if [ -n "${BUNDLE_NAME}" ]; then - sed -i "s/\"bundleName\": \".*\"/\"bundleName\": \"${BUNDLE_NAME}\"/" \ - AppScope/app.json5 - echo "Bundle name set to: ${BUNDLE_NAME}" - else - echo "Using default bundle name from app.json5" - fi - cat AppScope/app.json5 - - # ── Step 6: Build & sign the APP ───────────────────────────────────── - # APP_ARCH=arm64: the device ABI (phones/tablets/2in1). The entry module - # abiFilters cover arm64-v8a + x86_64; build-app.sh selects - # entry/libs// by APP_ARCH. - - name: Build HarmonyOS app - run: ./scripts/build-app.sh --${{ github.event.inputs.build_mode || 'release' }} - env: - COMMANDLINE_TOOLS: ${{ env.COMMANDLINE_TOOLS }} - OHOS_SDK_HOME: ${{ env.OHOS_SDK_HOME }} - APP_ARCH: arm64 - KEYSTORE_PASSWORD: ${{ secrets.OHOS_KEYSTORE_PASSWORD }} - KEY_PASSWORD: ${{ secrets.OHOS_KEY_PASSWORD }} - KEY_ALIAS: ${{ secrets.OHOS_KEY_ALIAS }} - - # ── Step 7: Upload artifact ────────────────────────────────────────── - - name: Save ohpm modules cache - if: steps.ohpm_cache.outputs.cache-hit != 'true' && always() - uses: actions/cache/save@v4 - with: - path: | - oh_modules - entry/oh_modules - ~/.ohpm - key: ohpm-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} - - - name: Find APP file - id: find_app - run: | - APP_FILE=$(find build/outputs -name "*.app" -type f | head -1) - if [ -z "${APP_FILE}" ]; then - echo "::error::No .app file found!" - exit 1 - fi - APP_NAME=$(basename "${APP_FILE}") - ARTIFACT_NAME="${APP_NAME%.app}" - echo "app_path=${APP_FILE}" >> $GITHUB_OUTPUT - echo "app_name=${APP_NAME}" >> $GITHUB_OUTPUT - echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT - echo "Found APP: ${APP_FILE} ($(du -h ${APP_FILE} | cut -f1))" - - # ── Step 7b: Verify APP contents independently ────────────────────── - # build-app.sh already verifies HAP contents, but this step provides - # a clear pass/fail signal in the CI log and adds the results to the - # GitHub Step Summary for quick inspection. - # Layout is the dev2 (ArkWeb + Node.js backend) one: the electerm web - # app lives at resources/resfile/electerm/ and the runtime .so files at - # libs//. - - name: Verify APP contents - run: | - set -euo pipefail - APP_FILE=$(find build/outputs -name "*.app" -type f | head -1) - echo "Verifying: ${APP_FILE}" - TMPDIR=$(mktemp -d) - trap 'rm -rf "${TMPDIR}"' EXIT - unzip -q "${APP_FILE}" -d "${TMPDIR}" - HAP_FILE=$(find "${TMPDIR}" -name "*.hap" -type f | head -1) - HAP_DIR="${TMPDIR}/hap" - unzip -q "${HAP_FILE}" -d "${HAP_DIR}" - APP_DIR="${HAP_DIR}/resources/resfile/electerm" - ERRORS="" - for f in \ - "index.js" \ - "app.bundle.mjs" \ - "package.json" \ - "views/index.pug"; do - if [ ! -f "${APP_DIR}/${f}" ]; then - ERRORS="${ERRORS}\n ✗ MISSING: ${f}" - else - echo " ✓ ${f}" - fi - done - JS_COUNT=$(find "${APP_DIR}/dist/assets/js" -name "*.js" 2>/dev/null | wc -l) - CSS_COUNT=$(find "${APP_DIR}/dist/assets/css" -name "*.css" 2>/dev/null | wc -l) - CHUNK_COUNT=$(find "${APP_DIR}/dist/assets/chunk" -name "*.js" 2>/dev/null | wc -l) - echo " ✓ assets/js: ${JS_COUNT} files" - echo " ✓ assets/css: ${CSS_COUNT} files" - echo " ✓ assets/chunk: ${CHUNK_COUNT} files" - if [ "${JS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No JS files in dist/assets/js/"; fi - if [ "${CSS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No CSS files in dist/assets/css/"; fi - if [ "${CHUNK_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No chunk files in dist/assets/chunk/"; fi - # Native runtime libs (real shared libnode.so — see prepare-node.sh) - LIB_NODE_COUNT=$(find "${HAP_DIR}/libs" -name "libnode.so" 2>/dev/null | wc -l) - LIB_CTL_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_ctl.so" 2>/dev/null | wc -l) - LIB_LAUNCHER_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_launcher.so" 2>/dev/null | wc -l) - echo " ✓ libs/libnode.so: ${LIB_NODE_COUNT} arch(s)" - echo " ✓ libs/libnode_ctl.so: ${LIB_CTL_COUNT} arch(s)" - echo " ✓ libs/libnode_launcher.so: ${LIB_LAUNCHER_COUNT} arch(s)" - if [ "${LIB_NODE_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode.so missing from libs/"; fi - if [ "${LIB_CTL_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_ctl.so missing from libs/"; fi - if [ "${LIB_LAUNCHER_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_launcher.so missing from libs/"; fi - if [ -n "${ERRORS}" ]; then - echo -e "::error::APP content verification failed:${ERRORS}" - exit 1 - fi - echo "✓ All critical files verified in APP" - # Add to GitHub Step Summary - echo "| JS files | \`${JS_COUNT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| CSS files | \`${CSS_COUNT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Chunk files | \`${CHUNK_COUNT}\` |" >> $GITHUB_STEP_SUMMARY - - - name: Upload APP artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.find_app.outputs.artifact_name }} - path: ${{ steps.find_app.outputs.app_path }} - retention-days: 30 - - # ── Summary ────────────────────────────────────────────────────────── - - name: Build summary - if: always() - run: | - echo "## Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY - echo "|------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Branch/Tag | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Runtime | \`Node.js 24 (--shared libnode.so) + ArkWeb\` |" >> $GITHUB_STEP_SUMMARY - echo "| Web app | \`electerm source (direct run)\` |" >> $GITHUB_STEP_SUMMARY - echo "| App version | \`${{ env.APP_VERSION || 'unknown' }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Build mode | \`${{ github.event.inputs.build_mode || 'release' }}\` |" >> $GITHUB_STEP_SUMMARY - if [ -f "${{ steps.find_app.outputs.app_path }}" ]; then - echo "| APP file | \`${{ steps.find_app.outputs.app_name }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| APP size | \`$(du -h ${{ steps.find_app.outputs.app_path }} | cut -f1)\` |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/build/bin/install.js b/build/bin/install.js index 867b4ae..eadb0c6 100644 --- a/build/bin/install.js +++ b/build/bin/install.js @@ -12,7 +12,7 @@ * After the source sync we also copy the @electerm/electerm-react client * from node_modules (same as the original install step). */ -import { writeFile, mkdir } from 'node:fs/promises' +import { copyFile, readdir, writeFile, mkdir } from 'node:fs/promises' import { existsSync } from 'node:fs' import { resolve } from 'node:path' import pkg from 'shelljs' @@ -25,9 +25,23 @@ const BRANCH = 'main' const URL = `https://codeload.github.com/${REPO}/tar.gz/refs/heads/${BRANCH}` const TMP = resolve('temp/electerm-android-src') const TMP_FILE = resolve(TMP, 'electerm-android.tar.gz') +const REPLACE_DIR = resolve('build/replace') echo('install required modules') +async function copyReplacements (from, to) { + await mkdir(to, { recursive: true }) + for (const entry of await readdir(from, { withFileTypes: true })) { + const source = resolve(from, entry.name) + const destination = resolve(to, entry.name) + if (entry.isDirectory()) { + await copyReplacements(source, destination) + } else { + await copyFile(source, destination) + } + } +} + // --------------------------------------------------------------------------- // 1. Download the latest electerm-android source archive // --------------------------------------------------------------------------- @@ -73,14 +87,22 @@ if (downloaded) { } // --------------------------------------------------------------------------- -// 3. Copy @electerm/electerm-react client from node_modules +// 3. Apply tracked HarmonyOS source replacements +// --------------------------------------------------------------------------- +if (existsSync(REPLACE_DIR)) { + echo('applying HarmonyOS source replacements…') + await copyReplacements(REPLACE_DIR, resolve('src')) +} + +// --------------------------------------------------------------------------- +// 4. Copy @electerm/electerm-react client from node_modules // --------------------------------------------------------------------------- echo('installing electerm-react module') shellRm('-rf', 'src/client/electerm-react') cp('-r', 'node_modules/@electerm/electerm-react/client', 'src/client/electerm-react') // --------------------------------------------------------------------------- -// 4. Cleanup temp files +// 5. Cleanup temp files // --------------------------------------------------------------------------- shellRm('-rf', TMP) diff --git a/build/replace/app/lib/db.js b/build/replace/app/lib/db.js new file mode 100644 index 0000000..66db87e --- /dev/null +++ b/build/replace/app/lib/db.js @@ -0,0 +1,17 @@ +/** + * db loader + */ + +let dbModule = null + +async function getDbModule () { + if (!dbModule) { + dbModule = await import('./nedb.js') + } + return dbModule +} + +export async function dbAction (...args) { + const db = await getDbModule() + return db.dbAction ? db.dbAction(...args) : db.default.dbAction(...args) +} diff --git a/build/replace/app/lib/nedb.js b/build/replace/app/lib/nedb.js new file mode 100644 index 0000000..73fa242 --- /dev/null +++ b/build/replace/app/lib/nedb.js @@ -0,0 +1,122 @@ +/** + * NeDB API wrapper compatible with legacy electerm user data. + */ + +import fs from 'fs' +import { resolve } from 'path' +import Datastore from '@electerm/nedb' +import nedbStorage from '@electerm/nedb/lib/storage.js' +import { cwd, defaultUserName } from '../common/runtime-constants.js' +import { safeDecrypt, safeEncrypt } from './safe-storage.js' + +const originalFlush = nedbStorage.flushToStorage +const encryptedTables = new Set(['bookmarks', 'profiles', 'data', 'history', 'terminalCommandHistory', 'aiChatHistory']) +const encryptedDataId = 'userConfig' +const encryptedPrefix = 'enc:' + +nedbStorage.flushToStorage = function (options, callback) { + originalFlush.call(nedbStorage, options, () => callback(null)) +} + +export const tables = [ + 'bookmarks', + 'bookmarkGroups', + 'addressBookmarks', + 'terminalThemes', + 'lastStates', + 'data', + 'quickCommands', + 'log', + 'dbUpgradeLog', + 'profiles', + 'workspaces', + 'history', + 'terminalCommandHistory', + 'aiChatHistory', + 'autoRunWidgets' +] + +const dbPath = process.env.DB_PATH || resolve(cwd, 'data') +const dbDir = resolve(dbPath, 'users', defaultUserName) +fs.mkdirSync(dbDir, { recursive: true }) + +const db = Object.fromEntries(tables.map(table => [ + table, + new Datastore({ + filename: resolve(dbDir, `electerm.${table}.nedb`), + autoload: true, + onload: (err) => { + if (err && !db[table].executor.ready) { + db[table].executor.processBuffer() + } + } + }) +])) + +function needsEncryption (dbName, id) { + return dbName === 'data' + ? id === encryptedDataId + : encryptedTables.has(dbName) +} + +function encryptDoc (dbName, doc) { + if (!needsEncryption(dbName, doc._id)) return doc + const { _id, ...payload } = doc + return { + ...(_id === undefined ? {} : { _id }), + _encdata: encryptedPrefix + safeEncrypt(JSON.stringify(payload)) + } +} + +function decryptDoc (dbName, doc) { + if (!doc || !needsEncryption(dbName, doc._id) || !doc._encdata) return doc + try { + const decrypted = doc._encdata.startsWith(encryptedPrefix) + ? safeDecrypt(doc._encdata.slice(encryptedPrefix.length)) + : doc._encdata + const { _encdata, ...rest } = doc + return { ...rest, ...JSON.parse(decrypted) } + } catch { + return doc + } +} + +export function dbAction (dbName, op, ...args) { + if (!db[dbName]) { + throw new Error(`Table ${dbName} does not exist`) + } + if (op === 'compactDatafile') { + db[dbName].persistence.compactDatafile() + return + } + return new Promise((resolve, reject) => { + const callback = (err, result) => { + if (err) return reject(err) + if (op === 'find') return resolve((result || []).map(doc => decryptDoc(dbName, doc))) + if (op === 'findOne') return resolve(decryptDoc(dbName, result)) + resolve(result) + } + if (op === 'insert') { + const original = args[0] + const encrypted = Array.isArray(original) + ? original.map(doc => encryptDoc(dbName, doc)) + : encryptDoc(dbName, original) + db[dbName].insert(encrypted, (err, inserted) => { + if (err) return reject(err) + if (Array.isArray(original)) { + return resolve(inserted.map((doc, index) => ({ ...original[index], _id: doc._id }))) + } + resolve({ ...original, _id: inserted._id }) + }) + return + } + if (op === 'update' && needsEncryption(dbName, args[0]._id || args[0].id)) { + const [query, update, options] = args + const payload = update.$set || update + const encrypted = encryptDoc(dbName, { _id: query._id || query.id, ...payload }) + db[dbName].update(query, update.$set ? { $set: encrypted } : encrypted, options || {}, callback) + return + } + db[dbName][op](...args, callback) + }) +} diff --git a/build/replace/app/lib/safe-storage.js b/build/replace/app/lib/safe-storage.js new file mode 100644 index 0000000..cb6f73e --- /dev/null +++ b/build/replace/app/lib/safe-storage.js @@ -0,0 +1,48 @@ +/** + * Safe storage compatible with legacy HarmonyOS NeDB records. + */ + +import crypto from 'crypto' + +const SAFE_PREFIX = 'v2:safe:' +const ALGORITHM = 'aes-256-gcm' +const IV_LENGTH = 12 +const STORAGE_SECRET = process.env.STORAGE_SECRET || 'static-secret-string-safe-storage' + +function getKey () { + return crypto.createHash('sha256').update(STORAGE_SECRET).digest() +} + +export function safeEncrypt (value) { + if (typeof value !== 'string' || !value) return value + try { + const iv = crypto.randomBytes(IV_LENGTH) + const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv) + const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]) + return SAFE_PREFIX + [ + iv.toString('base64'), + encrypted.toString('base64'), + cipher.getAuthTag().toString('base64') + ].join(':') + } catch (err) { + console.error('[safe-storage] encrypt error:', err.message) + return value + } +} + +export function safeDecrypt (value) { + if (typeof value !== 'string' || !value || !value.startsWith(SAFE_PREFIX)) return value + try { + const [iv, encrypted, authTag] = value.slice(SAFE_PREFIX.length).split(':') + if (!iv || !encrypted || !authTag) return value + const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), Buffer.from(iv, 'base64')) + decipher.setAuthTag(Buffer.from(authTag, 'base64')) + return Buffer.concat([ + decipher.update(Buffer.from(encrypted, 'base64')), + decipher.final() + ]).toString('utf8') + } catch (err) { + console.error('[safe-storage] decrypt error:', err.message) + return value + } +} diff --git a/package-lock.json b/package-lock.json index ae27c43..f24c56c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@electerm/electerm-locales": "2.3.10", "@electerm/electerm-themes": "^1.0.1", "@electerm/ftp-srv": "1.0.5", + "@electerm/nedb": "^2.0.0", "@electerm/ssh2": "1.22.0", "@xterm/headless": "6.1.0-beta.292", "axios": "1.18.1", @@ -539,6 +540,16 @@ "node": ">=16" } }, + "node_modules/@electerm/nedb": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@electerm/nedb/-/nedb-2.0.0.tgz", + "integrity": "sha512-3u60Dnjs4OFMsAmBhFZ4JsMrpVetY+S0LcGRSW0/15LAiI6+DqAwvxiY4HxC1S5I21nMHWMjgV228U3zb/F78w==", + "license": "MIT", + "dependencies": { + "@yetzt/binary-search-tree": "^0.2.6", + "mkdirp": "^1.0.4" + } + }, "node_modules/@electerm/ssh2": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@electerm/ssh2/-/ssh2-1.22.0.tgz", @@ -3234,6 +3245,12 @@ "addons/*" ] }, + "node_modules/@yetzt/binary-search-tree": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@yetzt/binary-search-tree/-/binary-search-tree-0.2.6.tgz", + "integrity": "sha512-e/8wt8AAumI8VK5sv09b3IgWuRoblXJ5z0SQYfrL2nap89oKihvVaP1zy3FzD5NaeRi1X0gdXZA9lB3QAZILBg==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8651,6 +8668,18 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/morgan": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", diff --git a/package.json b/package.json index 4d2862c..5788ae6 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@electerm/electerm-locales": "2.3.10", "@electerm/electerm-themes": "^1.0.1", "@electerm/ftp-srv": "1.0.5", + "@electerm/nedb": "^2.0.0", "@electerm/ssh2": "1.22.0", "@xterm/headless": "6.1.0-beta.292", "axios": "1.18.1", @@ -147,4 +148,4 @@ ], "sourceType": "module" } -} \ No newline at end of file +} diff --git a/scripts/inject-safe-storage-secret.mjs b/scripts/inject-safe-storage-secret.mjs new file mode 100644 index 0000000..69d72db --- /dev/null +++ b/scripts/inject-safe-storage-secret.mjs @@ -0,0 +1,17 @@ +import fs from 'node:fs' + +const file = process.env.SAFE_STORAGE_FILE || 'build/replace/app/lib/safe-storage.js' +const marker = "process.env.STORAGE_SECRET || 'static-secret-string-safe-storage'" +const secret = process.env.STORAGE_SECRET + +if (!secret) { + throw new Error('STORAGE_SECRET is not set') +} + +const source = fs.readFileSync(file, 'utf8') +if (!source.includes(marker)) { + throw new Error(`safe-storage marker not found in ${file}`) +} + +fs.writeFileSync(file, source.replace(marker, JSON.stringify(secret))) +console.log('safe-storage secret injected') From f06860a6e131b565dcd5eb6fc4f98a6cc0e8ca10 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:00:36 +0800 Subject: [PATCH 46/52] fix 5 --- .github/workflows/build.yml | 340 ++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..01d7a50 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,340 @@ +name: Build HarmonyOS APP + +on: + push: + branches: + - build + - dev + - dev1 + # NOTE: dev2 intentionally excluded — that branch only builds the + # web variant (build-web.yml). Keeping it here would fire the full + # HarmonyOS APP build on every dev2 push, which we don't want. + +# Cancel previous runs on the same branch/tag +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# Note: The Node.js runtime (libnode.so) is downloaded from the +# electerm/electerm-harmony release published manually via +# temp/bak/publish-node-release.sh — see the "Prepare Node.js runtime (arm64)" +# step below. + +jobs: + build: + # HarmonyOS Command Line Tools are x64-only. + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + # ── Checkout ────────────────────────────────────────────────────────── + - name: Checkout electerm-harmony + uses: actions/checkout@v4 + + # ── Setup Node.js (for building the web app) ───────────────────────── + - name: Setup Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: '24' + + # ── Install system deps for native modules ──────────────────────────── + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + unzip \ + jq \ + python3 \ + make \ + g++ \ + libsecret-1-dev + + # ── Setup JDK (for hap-sign-tool.jar) ──────────────────────────────── + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + # ── Step 1: Prepare Node.js runtime ──────────────────────────────────── + # Downloads our own real shared libnode.so (built with --shared via + # scripts/build-node-ohos.sh, archived in temp/bak/, published manually + # via temp/bak/publish-node-release.sh) from the electerm/electerm-harmony + # release. arm64-v8a is the device ABI for phones/tablets/2in1; this is + # the half that used to be a dlopen-crashing PIE executable + # (hqzing/ohos-node). + - name: Prepare Node.js runtime (arm64) + run: ./scripts/prepare-node.sh arm64 + + - name: Inject safe-storage secret + env: + STORAGE_SECRET: ${{ secrets.OHOS_SERVER_SECRET }} + run: node scripts/inject-safe-storage-secret.mjs + + # ── Step 2: Build web app (frontend + backend bundle) ─────────── + - name: Prepare web app + run: ./scripts/prepare-web.sh + env: + OHOS_SERVER_SECRET: ${{ secrets.OHOS_SERVER_SECRET }} + + # ── Step 3: Cache / download HarmonyOS Command Line Tools (~2 GB) ──── + - name: Compute Command Line Tools cache key + id: cmdkey + env: + OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} + run: | + if [ -z "${OHOS_CMDLINE_TOOLS_URL}" ]; then + echo "::error::OHOS_CMDLINE_TOOLS_URL secret is not set." + exit 1 + fi + # Hash the URL so the cache invalidates automatically when the + # secret points to a new version of the tools. + HASH="$(echo -n "${OHOS_CMDLINE_TOOLS_URL}" | md5sum | cut -d' ' -f1)" + echo "key=cmdline-tools-${HASH}" >> "$GITHUB_OUTPUT" + + - name: Restore HarmonyOS Command Line Tools cache + id: cmdline_cache + uses: actions/cache/restore@v4 + with: + path: .cache/commandline-tools + key: ${{ steps.cmdkey.outputs.key }} + + - name: Download & extract HarmonyOS Command Line Tools + if: steps.cmdline_cache.outputs.cache-hit != 'true' + env: + OHOS_CMDLINE_TOOLS_URL: ${{ secrets.OHOS_CMDLINE_TOOLS_URL }} + run: | + set -euo pipefail + mkdir -p .cache + ZIP=".cache/commandline-tools.zip" + echo "Cache miss — downloading HarmonyOS Command Line Tools (~2 GB) ..." + curl -L --retry 10 --retry-all-errors --retry-delay 5 -C - \ + -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ + -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" + echo "Verifying archive integrity ..." + if ! unzip -t "$ZIP" >/dev/null 2>&1; then + echo "::error::Downloaded archive is corrupt; retrying once without resume." + curl -L --retry 10 --retry-all-errors --retry-delay 5 \ + -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" \ + -o "$ZIP" "${OHOS_CMDLINE_TOOLS_URL}" + unzip -t "$ZIP" >/dev/null 2>&1 || { echo "::error::Still corrupt after retry"; exit 1; } + fi + rm -rf .cache/commandline-tools + mkdir -p .cache/commandline-tools + unzip -o -q "$ZIP" -d .cache/commandline-tools + rm -f "$ZIP" + echo "Downloaded and extracted HarmonyOS Command Line Tools" + + - name: Save HarmonyOS Command Line Tools cache + if: steps.cmdline_cache.outputs.cache-hit != 'true' && always() + uses: actions/cache/save@v4 + with: + path: .cache/commandline-tools + key: ${{ steps.cmdkey.outputs.key }} + + - name: Configure Command Line Tools environment + run: | + set -euo pipefail + COMMANDLINE_TOOLS="$(pwd)/.cache/commandline-tools/command-line-tools" + if [ ! -d "$COMMANDLINE_TOOLS" ]; then + COMMANDLINE_TOOLS="$(cd "$(dirname "$(find .cache/commandline-tools -name ohpm -type f | head -1)")/.." && pwd)" + fi + # Fix: Project root package.json has "type": "module", which makes + # Node.js treat hvigorw.js as an ES module (breaks with "require is + # not defined"). Adding a CommonJS package.json to the tools dirs + # prevents Node from traversing up to the project root. + echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/hvigor/package.json" 2>/dev/null || true + echo '{"type":"commonjs"}' > "$COMMANDLINE_TOOLS/package.json" + echo "COMMANDLINE_TOOLS=$COMMANDLINE_TOOLS" >> "$GITHUB_ENV" + echo "OHOS_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" + echo "DEVECO_NODE_HOME=$COMMANDLINE_TOOLS/tool/node" >> "$GITHUB_ENV" + echo "DEVECO_SDK_HOME=$COMMANDLINE_TOOLS/sdk" >> "$GITHUB_ENV" + echo "$COMMANDLINE_TOOLS/bin" >> "$GITHUB_PATH" + echo "$COMMANDLINE_TOOLS/hvigor/bin" >> "$GITHUB_PATH" + echo "HarmonyOS Command Line Tools ready at $COMMANDLINE_TOOLS" + + - name: Configure ohpm registry + run: | + ohpm config set registry https://ohpm.openharmony.cn/ohpm/ || true + ohpm --version || true + + - name: Restore ohpm modules cache + id: ohpm_cache + uses: actions/cache/restore@v4 + with: + path: | + oh_modules + entry/oh_modules + ~/.ohpm + key: ohpm-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} + restore-keys: | + ohpm- + + # ── Step 4: Decode signing materials ──────────────────────────────── + - name: Decode signing materials + env: + OHOS_KEYSTORE_B64: ${{ secrets.OHOS_KEYSTORE_B64 }} + OHOS_CERT_B64: ${{ secrets.OHOS_CERT_B64 }} + OHOS_PROFILE_B64: ${{ secrets.OHOS_PROFILE_B64 }} + run: | + mkdir -p signing + if [ -z "${OHOS_KEYSTORE_B64}" ] || [ -z "${OHOS_CERT_B64}" ] || [ -z "${OHOS_PROFILE_B64}" ]; then + echo "One or more signing material secrets are not set" + exit 1 + fi + echo "${OHOS_KEYSTORE_B64}" | base64 -d > signing/electerm.p12 + echo "${OHOS_CERT_B64}" | base64 -d > signing/electerm_publish.cer + echo "${OHOS_PROFILE_B64}" | base64 -d > signing/electermRelease.p7b + + ls -la signing/ + for f in signing/electerm.p12 signing/electerm_publish.cer signing/electermRelease.p7b; do + if [ ! -s "${f}" ]; then + echo "::error::Failed to decode ${f} — check GitHub Secrets." + exit 1 + fi + echo " ✓ $(basename ${f}): $(du -h ${f} | cut -f1)" + done + + # ── Step 5: Set bundle name from secret ────────────────────────────── + - name: Configure bundle name + env: + BUNDLE_NAME: ${{ secrets.OHOS_BUNDLE_NAME }} + run: | + if [ -n "${BUNDLE_NAME}" ]; then + sed -i "s/\"bundleName\": \".*\"/\"bundleName\": \"${BUNDLE_NAME}\"/" \ + AppScope/app.json5 + echo "Bundle name set to: ${BUNDLE_NAME}" + else + echo "Using default bundle name from app.json5" + fi + cat AppScope/app.json5 + + # ── Step 6: Build & sign the APP ───────────────────────────────────── + # APP_ARCH=arm64: the device ABI (phones/tablets/2in1). The entry module + # abiFilters cover arm64-v8a + x86_64; build-app.sh selects + # entry/libs// by APP_ARCH. + - name: Build HarmonyOS app + run: ./scripts/build-app.sh --${{ github.event.inputs.build_mode || 'release' }} + env: + COMMANDLINE_TOOLS: ${{ env.COMMANDLINE_TOOLS }} + OHOS_SDK_HOME: ${{ env.OHOS_SDK_HOME }} + APP_ARCH: arm64 + KEYSTORE_PASSWORD: ${{ secrets.OHOS_KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.OHOS_KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.OHOS_KEY_ALIAS }} + + # ── Step 7: Upload artifact ────────────────────────────────────────── + - name: Save ohpm modules cache + if: steps.ohpm_cache.outputs.cache-hit != 'true' && always() + uses: actions/cache/save@v4 + with: + path: | + oh_modules + entry/oh_modules + ~/.ohpm + key: ohpm-${{ hashFiles('**/oh-package.json5', '**/oh-package.json') }} + + - name: Find APP file + id: find_app + run: | + APP_FILE=$(find build/outputs -name "*.app" -type f | head -1) + if [ -z "${APP_FILE}" ]; then + echo "::error::No .app file found!" + exit 1 + fi + APP_NAME=$(basename "${APP_FILE}") + ARTIFACT_NAME="${APP_NAME%.app}" + echo "app_path=${APP_FILE}" >> $GITHUB_OUTPUT + echo "app_name=${APP_NAME}" >> $GITHUB_OUTPUT + echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT + echo "Found APP: ${APP_FILE} ($(du -h ${APP_FILE} | cut -f1))" + + # ── Step 7b: Verify APP contents independently ────────────────────── + # build-app.sh already verifies HAP contents, but this step provides + # a clear pass/fail signal in the CI log and adds the results to the + # GitHub Step Summary for quick inspection. + # Layout is the dev2 (ArkWeb + Node.js backend) one: the electerm web + # app lives at resources/resfile/electerm/ and the runtime .so files at + # libs//. + - name: Verify APP contents + run: | + set -euo pipefail + APP_FILE=$(find build/outputs -name "*.app" -type f | head -1) + echo "Verifying: ${APP_FILE}" + TMPDIR=$(mktemp -d) + trap 'rm -rf "${TMPDIR}"' EXIT + unzip -q "${APP_FILE}" -d "${TMPDIR}" + HAP_FILE=$(find "${TMPDIR}" -name "*.hap" -type f | head -1) + HAP_DIR="${TMPDIR}/hap" + unzip -q "${HAP_FILE}" -d "${HAP_DIR}" + APP_DIR="${HAP_DIR}/resources/resfile/electerm" + ERRORS="" + for f in \ + "index.js" \ + "app.bundle.mjs" \ + "package.json" \ + "views/index.pug"; do + if [ ! -f "${APP_DIR}/${f}" ]; then + ERRORS="${ERRORS}\n ✗ MISSING: ${f}" + else + echo " ✓ ${f}" + fi + done + JS_COUNT=$(find "${APP_DIR}/dist/assets/js" -name "*.js" 2>/dev/null | wc -l) + CSS_COUNT=$(find "${APP_DIR}/dist/assets/css" -name "*.css" 2>/dev/null | wc -l) + CHUNK_COUNT=$(find "${APP_DIR}/dist/assets/chunk" -name "*.js" 2>/dev/null | wc -l) + echo " ✓ assets/js: ${JS_COUNT} files" + echo " ✓ assets/css: ${CSS_COUNT} files" + echo " ✓ assets/chunk: ${CHUNK_COUNT} files" + if [ "${JS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No JS files in dist/assets/js/"; fi + if [ "${CSS_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No CSS files in dist/assets/css/"; fi + if [ "${CHUNK_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ No chunk files in dist/assets/chunk/"; fi + # Native runtime libs (real shared libnode.so — see prepare-node.sh) + LIB_NODE_COUNT=$(find "${HAP_DIR}/libs" -name "libnode.so" 2>/dev/null | wc -l) + LIB_CTL_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_ctl.so" 2>/dev/null | wc -l) + LIB_LAUNCHER_COUNT=$(find "${HAP_DIR}/libs" -name "libnode_launcher.so" 2>/dev/null | wc -l) + echo " ✓ libs/libnode.so: ${LIB_NODE_COUNT} arch(s)" + echo " ✓ libs/libnode_ctl.so: ${LIB_CTL_COUNT} arch(s)" + echo " ✓ libs/libnode_launcher.so: ${LIB_LAUNCHER_COUNT} arch(s)" + if [ "${LIB_NODE_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode.so missing from libs/"; fi + if [ "${LIB_CTL_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_ctl.so missing from libs/"; fi + if [ "${LIB_LAUNCHER_COUNT}" -eq 0 ]; then ERRORS="${ERRORS}\n ✗ libnode_launcher.so missing from libs/"; fi + if [ -n "${ERRORS}" ]; then + echo -e "::error::APP content verification failed:${ERRORS}" + exit 1 + fi + echo "✓ All critical files verified in APP" + # Add to GitHub Step Summary + echo "| JS files | \`${JS_COUNT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| CSS files | \`${CSS_COUNT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Chunk files | \`${CHUNK_COUNT}\` |" >> $GITHUB_STEP_SUMMARY + + - name: Upload APP artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.find_app.outputs.artifact_name }} + path: ${{ steps.find_app.outputs.app_path }} + retention-days: 30 + + # ── Summary ────────────────────────────────────────────────────────── + - name: Build summary + if: always() + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Branch/Tag | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | \`Node.js 24 (--shared libnode.so) + ArkWeb\` |" >> $GITHUB_STEP_SUMMARY + echo "| Web app | \`electerm source (direct run)\` |" >> $GITHUB_STEP_SUMMARY + echo "| App version | \`${{ env.APP_VERSION || 'unknown' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Build mode | \`${{ github.event.inputs.build_mode || 'release' }}\` |" >> $GITHUB_STEP_SUMMARY + if [ -f "${{ steps.find_app.outputs.app_path }}" ]; then + echo "| APP file | \`${{ steps.find_app.outputs.app_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| APP size | \`$(du -h ${{ steps.find_app.outputs.app_path }} | cut -f1)\` |" >> $GITHUB_STEP_SUMMARY + fi From 95fb50a570fafee1b809caef48564ee6c7189426 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:15:40 +0800 Subject: [PATCH 47/52] Fix db path --- entry/src/main/ets/pages/Index.ets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 060b348..04255a2 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -143,8 +143,8 @@ struct Index { const filesDir: string = context.filesDir; // el2 junction path (visible in the child too) when available this.dataDir = this.dirExists(EL2_FILES_DIR) - ? `${EL2_FILES_DIR}/electerm-data` - : `${filesDir}/electerm-data`; + ? EL2_FILES_DIR + : filesDir; const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); const nodePath: string = this.resolveNodePath(context.bundleCodeDir); From 34d47a70cf1c4dac2c7f1e0dec407458e30a1b0f Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:21:31 +0800 Subject: [PATCH 48/52] Fix language select --- build/replace/client/entry-web/electerm.jsx | 14 +++++ .../client/harmony/language-select.jsx | 56 +++++++++++++++++++ .../client/harmony/language-select.styl | 47 ++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 build/replace/client/entry-web/electerm.jsx create mode 100644 build/replace/client/harmony/language-select.jsx create mode 100644 build/replace/client/harmony/language-select.styl diff --git a/build/replace/client/entry-web/electerm.jsx b/build/replace/client/entry-web/electerm.jsx new file mode 100644 index 0000000..2ce7657 --- /dev/null +++ b/build/replace/client/entry-web/electerm.jsx @@ -0,0 +1,14 @@ +import { createRoot } from 'react-dom/client' +import '../../../node_modules/antd/dist/reset.css' +import '@fontsource/maple-mono/index.css' +import LanguageSelect from '../harmony/language-select.jsx' +import Main from '../web-components/web-main' + +const rootElement = document.getElementById('container') +const root = createRoot(rootElement) + +root.render( + +
+ +) diff --git a/build/replace/client/harmony/language-select.jsx b/build/replace/client/harmony/language-select.jsx new file mode 100644 index 0000000..f1a9fcd --- /dev/null +++ b/build/replace/client/harmony/language-select.jsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react' +import { GlobalOutlined } from '@ant-design/icons' +import './language-select.styl' + +const STORAGE_KEY = 'locale' + +export default function LanguageSelect ({ children }) { + const [langs, setLangs] = useState(() => window.et?.langs || []) + const [loaded, setLoaded] = useState(() => !!window.et?.langs?.length) + const selected = !!window.localStorage.getItem(STORAGE_KEY) + + useEffect(() => { + if (selected || langs.length) return + window.pre.runGlobalAsync('init') + .then(({ langMap, langs }) => { + window.langMap = langMap + window.et.langs = langs + setLangs(langs || []) + }) + .catch(err => console.error('[language-select] load languages failed', err)) + .finally(() => setLoaded(true)) + }, [langs.length, selected]) + + const choose = async langId => { + window.localStorage.setItem(STORAGE_KEY, langId) + try { + await window.pre.runGlobalAsync('saveUserConfig', { language: langId }) + } catch (err) { + console.error('[language-select] saveUserConfig failed', err) + } + window.location.reload() + } + + if (selected || (loaded && !langs.length)) return children + + return ( +
+
+ +
Select language / 选择语言
+
+ {langs.map(lang => ( + + ))} +
+
+
+ ) +} diff --git a/build/replace/client/harmony/language-select.styl b/build/replace/client/harmony/language-select.styl new file mode 100644 index 0000000..854a0be --- /dev/null +++ b/build/replace/client/harmony/language-select.styl @@ -0,0 +1,47 @@ +.language-select-wrap + position fixed + inset 0 + z-index 9999 + display flex + align-items center + justify-content center + background #fff + +.language-select-card + width 420px + max-width 90vw + padding 36px 28px + text-align center + +.language-select-icon + font-size 48px + color #08c + +.language-select-title + margin 16px 0 24px + font-size 18px + font-weight 600 + color #333 + +.language-select-list + display flex + flex-wrap wrap + gap 10px + justify-content center + +.language-select-item + min-width 120px + padding 10px 16px + font-size 14px + color #333 + background #f5f5f5 + border 1px solid #e0e0e0 + border-radius 8px + cursor pointer + transition all 0.15s ease + outline none + + &:hover + color #fff + background #08c + border-color #08c \ No newline at end of file From 1032ba450e4381ee03ac46beba267043c3d99650 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:42:55 +0800 Subject: [PATCH 49/52] Fix db path --- build/replace/app/lib/nedb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/replace/app/lib/nedb.js b/build/replace/app/lib/nedb.js index 73fa242..9531e2a 100644 --- a/build/replace/app/lib/nedb.js +++ b/build/replace/app/lib/nedb.js @@ -36,7 +36,7 @@ export const tables = [ 'autoRunWidgets' ] -const dbPath = process.env.DB_PATH || resolve(cwd, 'data') +const dbPath = process.env.DB_PATH || process.env.DATA_PATH || resolve(cwd, 'data') const dbDir = resolve(dbPath, 'users', defaultUserName) fs.mkdirSync(dbDir, { recursive: true }) From 933392439052ce29a0f9e9bbc983cfc29fb12824 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 17:57:55 +0800 Subject: [PATCH 50/52] FIx db path --- entry/src/main/ets/pages/Index.ets | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 04255a2..e378356 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -2,7 +2,7 @@ * Index page — ArkWeb host for the electerm web app. * * Startup sequence: - * 1. create the writable data dir (filesDir/electerm-data) + * 1. use the entry module data dir * 2. start the Node.js backend — primarily IN-PROCESS in this app * process (libnode_ctl.so startBackend → dlopen libnode.so → * node::Start; the electron-harmony pattern), falling back to a @@ -47,12 +47,11 @@ const BOOT_TIMEOUT_MS: number = 20_000; /** Never read more than this from node-boot.log — this runs on the UI thread. */ const MAX_BOOT_LOG_BYTES: number = 256 * 1024; /** - * Prefer the per-process el2 junction over the parent's filesDir string: - * /data/storage/el2/base/files points at the same storage but is mounted in - * EVERY process of the app (including the native child), while the - * sandbox-style absolute path the parent gets may not be. + * The main branch stores its data beside the installed entry module under + * /entry/files. Deriving it from bundleCodeDir keeps that + * layout independent of the device's per-user install prefix. */ -const EL2_FILES_DIR: string = '/data/storage/el2/base/files'; +const ENTRY_FILES_DIR = (bundleCodeDir: string): string => `${bundleCodeDir}/entry/files`; @Entry @Component @@ -140,11 +139,7 @@ struct Index { async startBackend(): Promise { try { const context = getContext(this) as common.Context; - const filesDir: string = context.filesDir; - // el2 junction path (visible in the child too) when available - this.dataDir = this.dirExists(EL2_FILES_DIR) - ? EL2_FILES_DIR - : filesDir; + this.dataDir = ENTRY_FILES_DIR(context.bundleCodeDir); const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); const nodePath: string = this.resolveNodePath(context.bundleCodeDir); From 4856db18ecc14cdeb93b9aab71319ccc7e7a362e Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Mon, 31 Aug 2026 18:30:12 +0800 Subject: [PATCH 51/52] FIx d --- docs/ARCHITECTURE.md | 2 +- entry/src/main/ets/pages/Index.ets | 118 +++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 593569a..42812d9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,7 +68,7 @@ Key design points: - **`AbilityStage.ets`** — standard `AbilityStage` (no Electron `WebAbilityStage`). - **`entryability/EntryAbility.ets`** — standard `UIAbility`; on `onDestroy()` it calls `BackendManager.killBackend()`. - **`pages/Index.ets`** — the boot orchestrator: - 1. creates the writable data dir (`filesDir/electerm-data`, with an `el2` junction fallback); + 1. resolves the writable data dir — **el2 only** (`/data/storage/el2/base/files/electerm-data`, else `/data/storage/el2/base/files`, else `filesDir` and its `electerm-data` sub-dir). A candidate that already holds `users/` wins, so an in-place upgrade keeps the previous build's db. `bundleCodeDir` (`el1/bundle`) is the read-only HAP install dir and must never be used for data — `mkdir` there fails with `13900012`; 2. calls `startBackend()` (in-process primary, native child fallback); 3. polls `http://127.0.0.1:5577` with plain HTTP until it answers; 4. once ready, `controller.loadUrl(SERVER_URL)` swaps the `Web` component from the local `loading.html` to the backend. diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index e378356..d1c16cf 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -2,7 +2,8 @@ * Index page — ArkWeb host for the electerm web app. * * Startup sequence: - * 1. use the entry module data dir + * 1. resolve the writable el2 data dir — the one the previous build used, + * so an in-place upgrade keeps its bookmarks * 2. start the Node.js backend — primarily IN-PROCESS in this app * process (libnode_ctl.so startBackend → dlopen libnode.so → * node::Start; the electron-harmony pattern), falling back to a @@ -47,11 +48,26 @@ const BOOT_TIMEOUT_MS: number = 20_000; /** Never read more than this from node-boot.log — this runs on the UI thread. */ const MAX_BOOT_LOG_BYTES: number = 256 * 1024; /** - * The main branch stores its data beside the installed entry module under - * /entry/files. Deriving it from bundleCodeDir keeps that - * layout independent of the device's per-user install prefix. + * The data dir must live under el2 — the app-owned area that an in-place + * upgrade keeps (same bundleName + same signing cert ⇒ same UID ⇒ the previous + * build's el2 tree is still there). + * + * `bundleCodeDir` is the *opposite*: the el1 HAP install dir. It is owned by + * the installer, mounted read-only, signature-verified and re-extracted on + * every install, so mkdir there fails with 13900012 (EPERM) and any data put + * there would be wiped by the next update anyway. + * + * Two el2 candidates are inherited from the previous (Electron) build: + * - app level /data/storage/el2/base/files (AbilityStage marker) + * - HAP level /data/storage/el2/base/haps/entry/files (context.filesDir) + * PickDataDir() prefers whichever already holds data, so an upgrade keeps its + * bookmarks instead of starting from an empty db. */ -const ENTRY_FILES_DIR = (bundleCodeDir: string): string => `${bundleCodeDir}/entry/files`; +const EL2_APP_FILES: string = '/data/storage/el2/base/files'; +/** Sub-dir used when neither candidate holds data yet (fresh install). */ +const DATA_SUBDIR: string = 'electerm-data'; +/** Marker an older build left behind with the dir it actually used. */ +const DATA_PATH_MARKER: string = '.electerm-data-path'; @Entry @Component @@ -139,7 +155,7 @@ struct Index { async startBackend(): Promise { try { const context = getContext(this) as common.Context; - this.dataDir = ENTRY_FILES_DIR(context.bundleCodeDir); + this.dataDir = this.pickDataDir(context); const scriptPath: string = this.resolveScriptPath(context.bundleCodeDir); const nodePath: string = this.resolveNodePath(context.bundleCodeDir); @@ -148,6 +164,7 @@ struct Index { if (!this.dirExists(this.dataDir)) { fs.mkdirSync(this.dataDir, true); } + this.pinDataDir(this.dataDir); // 2. start the backend // entryParams is a plain "key=value\n" string parsed by node_launcher.c @@ -327,6 +344,95 @@ struct Index { } } + /** el2 candidates, most preferred first. Never anything under el1/bundle. */ + dataDirCandidates(context: common.Context): string[] { + return [ + `${EL2_APP_FILES}/${DATA_SUBDIR}`, + EL2_APP_FILES, + `${context.filesDir}/${DATA_SUBDIR}`, + context.filesDir + ]; + } + + /** True when `dir` already holds an electerm nedb tree + * (/users//electerm.*.nedb) — i.e. an older build used it. */ + hasElectermData(dir: string): boolean { + return this.dirExists(`${dir}/users`); + } + + /** Contents of a marker left by an older build; '' when absent or not el2. */ + readMarker(path: string): string { + try { + const text: string = fs.readTextSync(path).trim(); + return text.startsWith('/data/storage/el2/') ? text : ''; + } catch { + return ''; + } + } + + /** Fail fast on a dir that exists but is not writable (locked el2, etc.). */ + writeProbe(dir: string): void { + const probe: string = `${dir}/.write-test`; + fs.closeSync(fs.openSync(probe, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)); + fs.unlinkSync(probe); + } + + /** + * Resolve the writable data dir, el2 only. + * + * An in-place upgrade keeps the app's el2 tree, so the previous build's nedb + * files are still on disk — the only thing that has to match is the + * directory. A dir that already holds `users/` therefore beats everything + * else; the marker is only a hint for layouts we no longer recognise. + */ + pickDataDir(context: common.Context): string { + const candidates: string[] = this.dataDirCandidates(context); + + for (let i = 0; i < candidates.length; i++) { + if (this.hasElectermData(candidates[i])) { + hilog.info(DOMAIN, TAG, 'reusing previous data dir: %{public}s', candidates[i]); + return candidates[i]; + } + } + for (let i = 0; i < candidates.length; i++) { + const marker: string = this.readMarker(`${candidates[i]}/${DATA_PATH_MARKER}`); + if (marker !== '') { + hilog.info(DOMAIN, TAG, 'using marker data dir: %{public}s', marker); + return marker; + } + } + for (let i = 0; i < candidates.length; i++) { + try { + if (!this.dirExists(candidates[i])) { + fs.mkdirSync(candidates[i], true); + } + this.writeProbe(candidates[i]); + hilog.info(DOMAIN, TAG, 'using fresh data dir: %{public}s', candidates[i]); + return candidates[i]; + } catch (e) { + hilog.warn(DOMAIN, TAG, 'data dir unusable: %{public}s (%{public}s)', + candidates[i], JSON.stringify(e)); + } + } + return context.tempDir; + } + + /** Record the dir we settled on so the next version resolves it directly. */ + pinDataDir(dir: string): void { + try { + if (!this.dirExists(EL2_APP_FILES)) { + fs.mkdirSync(EL2_APP_FILES, true); + } + const markerPath: string = `${EL2_APP_FILES}/${DATA_PATH_MARKER}`; + const file = fs.openSync(markerPath, + fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC); + fs.writeSync(file.fd, dir); + fs.closeSync(file); + } catch (e) { + hilog.warn(DOMAIN, TAG, 'could not pin data dir: %{public}s', JSON.stringify(e)); + } + } + /** Poll http://127.0.0.1:5577 until it answers or `timeoutMs` elapses. * While waiting, surface the child's latest boot-log line in the overlay so * a stuck boot is diagnosable from the screen alone. From 53cec9623114dbf8df3f53b58a19089918e29cbe Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Tue, 1 Sep 2026 19:57:17 +0800 Subject: [PATCH 52/52] Fix install src --- build/replace/app/lib/install-src.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 build/replace/app/lib/install-src.js diff --git a/build/replace/app/lib/install-src.js b/build/replace/app/lib/install-src.js new file mode 100644 index 0000000..6a827dd --- /dev/null +++ b/build/replace/app/lib/install-src.js @@ -0,0 +1,22 @@ +// install-src.js (HarmonyOS replacement) +// Determines the HarmonyOS release asset architecture identifier at runtime. +// Used to match the correct release asset when checking/downloading upgrades +// (see download-upgrade.js: `r.name.includes(installSrc)`). +// +// scripts/build-app.sh names release artifacts +// `electerm-harmony-${APP_ARCH}-${version}.app` with APP_ARCH being either +// `arm64` (arm64-v8a libs, real devices) or `x86_64` (emulator). We resolve +// at runtime from os.arch() so the same bundled code works for both without +// a build-time injection step. + +import os from 'os' + +const archMap = { + arm64: 'arm64', + x64: 'x86_64' +} + +const arch = os.arch() +const installSrc = 'electerm-harmony-' + (archMap[arch] || 'arm64') + +export default installSrc