From b54cbafc540875e59ff89ccb28de58d1f3c78cd6 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 11 Jun 2026 22:33:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9C=AC=E5=9C=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E7=AE=A1=E7=90=86=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 egui 服务器管理面板并支持 SSH alias 多服务器巡检 接入硬件状态、服务状态、HTTP 探测和生产巡检状态展示 增加受控 systemd 启动关闭重启操作和中文字体注入 补充本地服务器面板技术方案与团队共享记忆 --- .hermes/shared-memory/decision-log.md | 8 + .hermes/shared-memory/development-workflow.md | 9 + docs/README.md | 2 + ...】本地SSH服务器管理面板技术方案-2026-06-11.md | 82 + package.json | 1 + server-rs/Cargo.lock | 1779 ++++++++++++++++- server-rs/Cargo.toml | 1 + .../crates/server-manager-panel/Cargo.toml | 13 + .../crates/server-manager-panel/src/app.rs | 577 ++++++ .../crates/server-manager-panel/src/fonts.rs | 128 ++ .../crates/server-manager-panel/src/health.rs | 474 +++++ .../crates/server-manager-panel/src/lib.rs | 5 + .../crates/server-manager-panel/src/main.rs | 21 + .../crates/server-manager-panel/src/remote.rs | 231 +++ .../server-manager-panel/src/ssh_config.rs | 143 ++ 15 files changed, 3455 insertions(+), 19 deletions(-) create mode 100644 docs/technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md create mode 100644 server-rs/crates/server-manager-panel/Cargo.toml create mode 100644 server-rs/crates/server-manager-panel/src/app.rs create mode 100644 server-rs/crates/server-manager-panel/src/fonts.rs create mode 100644 server-rs/crates/server-manager-panel/src/health.rs create mode 100644 server-rs/crates/server-manager-panel/src/lib.rs create mode 100644 server-rs/crates/server-manager-panel/src/main.rs create mode 100644 server-rs/crates/server-manager-panel/src/remote.rs create mode 100644 server-rs/crates/server-manager-panel/src/ssh_config.rs diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index dacfd637..9b254533 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-11 本地服务器管理入口采用 SSH alias + egui 桌面面板 + +- 背景:release / dev 等服务器的日常巡检已有 systemd、健康巡检 timer 和 HTTP 探测口径,但开发者本地仍需要在多个 SSH alias 间手工切换命令并重复执行启停操作。 +- 决策:新增 `server-rs/crates/server-manager-panel` 作为本地 egui 桌面工具;服务器来源只读取本机 `~/.ssh/config` 的具体 `Host` alias,不保存服务器密钥或凭据;巡检通过 `ssh sh -s` 执行只读脚本,服务操作只允许 `start`、`stop`、`restart` 并限制 systemd unit 名字符集。 +- 影响范围:本地运维工具入口、`package.json` 的 `server-manager:panel`、开发运维文档和团队共享工作流。 +- 验证方式:`cargo check -p server-manager-panel --manifest-path server-rs/Cargo.toml`、`cargo test -p server-manager-panel --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md`。 + ## 2026-06-10 公开作品互动能力进入后台全局配置 - 背景:作品详情页的点赞和改造能力原本由前端和各玩法 handler 的硬编码能力矩阵决定,后台无法临时关闭某类公开作品的互动入口,直接关闭创作入口又会误伤已有作品读取和游玩。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index f34cf07f..706dec37 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -95,6 +95,15 @@ npm run dev:web npm run dev:admin-web ``` +本地 SSH 服务器管理面板: + +```bash +npm run server-manager:panel +``` + +该命令启动 `server-rs/crates/server-manager-panel` 的 egui 桌面工具,从本机 `~/.ssh/config` 读取可用 `Host` alias,支持多服务器健康巡检、可折叠侧边栏和受控 systemd 服务启停。服务操作通过远端 `sudo -n systemctl start|stop|restart ` 执行,目标服务器需要提前配置对应 unit 的免交互 sudo 权限。 +面板启动时会自动注入本机中文字体;如开发机中文仍显示为方块,可设置 `GENARRATIVE_SERVER_PANEL_CJK_FONT=/path/to/font.ttc|index` 指向本机 CJK 字体。 + `npm run dev:api-server` 会保留终端实时输出,并把同一份输出持久化到 `logs/api-server/api-server-.log`。完整联调入口 `npm run dev` 启动的 Rust `api-server` 使用同一套日志规则。如需改写路径,可设置 `GENARRATIVE_API_SERVER_LOG_FILE`;如只改目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR`。 开发态 `npm run dev` / `npm run dev:api-server` 默认打开 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true`,密码入口可以直接注册未知手机号账号;生产默认仍关闭该开关。 diff --git a/docs/README.md b/docs/README.md index 00c2f843..a5c6b407 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,8 @@ 微信小程序虚拟支付接入、`wechat_mp_virtual` 渠道、`wx.requestVirtualPayment` 承接页和后端签名配置见 [【技术方案】微信虚拟支付接入-2026-05-26.md](./%E3%80%90%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88%E3%80%91%E5%BE%AE%E4%BF%A1%E8%99%9A%E6%8B%9F%E6%94%AF%E4%BB%98%E6%8E%A5%E5%85%A5-2026-05-26.md)。 +本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。 + 生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。 diff --git a/docs/technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md b/docs/technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md new file mode 100644 index 00000000..aeecda90 --- /dev/null +++ b/docs/technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md @@ -0,0 +1,82 @@ +# 本地 SSH 服务器管理面板技术方案 + +日期:`2026-06-11` + +## 背景 + +release / dev 等服务器的日常巡检已经有 `genarrative-health-patrol.timer`、`/readyz`、`/healthz`、SpacetimeDB `/v1/ping` 和 systemd 状态文件,但开发者本地仍需要在多个 SSH alias 之间切换命令。服务器管理面板用于把这些只读巡检和少量 systemd 服务操作收敛到一个本地桌面入口。 + +## 范围 + +- 使用 Rust `egui` / `eframe` 实现本地桌面面板,不接入线上 Web 后台,不暴露公网端口。 +- 从本机 `~/.ssh/config` 的 `Host` alias 发现服务器;只展示不含通配符的 alias。 +- 支持多个服务器,左侧服务器侧边栏可收起。 +- 主面板展示硬件状态、服务状态、HTTP 健康探测和生产健康巡检状态。 +- 支持对允许的 systemd unit 执行启动、关闭、重启。 + +## 命令入口 + +```bash +npm run server-manager:panel +``` + +等价于: + +```bash +cargo run -p server-manager-panel --manifest-path server-rs/Cargo.toml +``` + +面板启动时会自动查找本机中文字体并注入 egui 字体集,优先使用 `Noto Sans CJK SC`,其次使用文泉驿 / Droid fallback。若某台开发机字体路径特殊,可用 `GENARRATIVE_SERVER_PANEL_CJK_FONT=/path/to/font.ttc|index` 指定;普通 `.ttf` 可省略 `|index`。 + +## SSH 约定 + +本地 `~/.ssh/config` 中需要存在类似: + +```sshconfig +Host dev + HostName 10.2.0.10 + User genarrative + +Host release + HostName genarrative.world + User genarrative +``` + +面板通过 `ssh sh -s` 执行远端只读巡检脚本。服务操作使用: + +```bash +sudo -n systemctl +``` + +若 SSH 用户是 root,则直接执行 `systemctl`。非 root 用户需要提前配置只允许目标 unit 的无密码 sudo;否则面板会显示 sudo 权限错误,不会弹出交互密码输入。 + +## 健康检查内容 + +只读巡检覆盖: + +- 主机名、内核、运行时长、CPU 核数 / 型号、load average。 +- 内存 / swap 使用情况。 +- `/`、`/var`、`/opt`、`/stdb`、`/data` 中存在路径的磁盘使用率。 +- `genarrative-api.service`、`spacetimedb.service`、`nginx.service`、`genarrative-health-patrol.timer`、`genarrative-database-backup.timer` 的 systemd 状态。 +- `http://127.0.0.1:8082/healthz`、`/readyz`、`http://127.0.0.1:3101/v1/ping` 和代表性公开接口。 +- `/var/lib/genarrative/health-patrol/status.json` 的最近巡检状态。 +- 若服务器安装了 `sensors`,附带温度 / 风扇等硬件传感器摘要。 + +## 服务操作安全边界 + +面板只允许 `start`、`stop`、`restart` 三种动作,并且 unit 名必须匹配安全字符集: + +```text +A-Z a-z 0-9 . _ - @ : +``` + +服务操作会先出现确认弹窗,避免误点。第一版默认列出 Genarrative 生产相关 unit,并提供“其他 unit”输入框;该输入框仍只会执行 `systemctl` 的三种受控动作,不提供任意命令执行入口。 + +## 状态判定 + +- service / HTTP 探测失败:`CRITICAL`。 +- 磁盘使用率 `>= 95%`:`CRITICAL`,`>= 85%`:`WARNING`。 +- 内存使用率 `>= 95%`:`CRITICAL`,`>= 85%`:`WARNING`。 +- 生产健康巡检状态沿用 `OK / WARNING / CRITICAL`。 + +面板状态只是本地巡检视图,最终运维事实仍以服务器上的 systemd、journal、Nginx 日志、`production-health-patrol.mjs` 输出和现有部署文档为准。 diff --git a/package.json b/package.json index b9055ebf..c4b39b8d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev:api-server": "node scripts/dev.mjs api-server", "dev:web": "node scripts/dev.mjs web", "dev:admin-web": "node scripts/dev.mjs admin-web", + "server-manager:panel": "cargo run -p server-manager-panel --manifest-path server-rs/Cargo.toml", "dev:spacetime:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh", "otel:debug": "node scripts/run-otelcol.mjs debug", "otel:rider": "node scripts/run-otelcol.mjs rider", diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index f285faaf..99cf595c 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "adler2" version = "2.0.1" @@ -35,6 +51,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -64,6 +81,31 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -173,6 +215,26 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "x11rb", +] + [[package]] name = "argon2" version = "0.5.3" @@ -197,6 +259,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "async-stream" version = "0.3.6" @@ -324,6 +392,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -377,6 +460,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "brotli" version = "3.5.0" @@ -409,6 +501,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder-lite" @@ -432,6 +538,57 @@ dependencies = [ "serde_core", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + [[package]] name = "castaway" version = "0.2.4" @@ -474,6 +631,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chrono" version = "0.4.44" @@ -498,6 +664,43 @@ dependencies = [ "inout", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.16.3" @@ -540,12 +743,46 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -607,6 +844,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -665,9 +908,15 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "darling" version = "0.23.0" @@ -763,6 +1012,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -774,24 +1039,183 @@ dependencies = [ "syn", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecolor" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" +dependencies = [ + "ahash", + "bitflags 2.11.1", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.18", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" +dependencies = [ + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "emath" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" +dependencies = [ + "bytemuck", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -839,6 +1263,30 @@ dependencies = [ "syn", ] +[[package]] +name = "epaint" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" + [[package]] name = "equivalent" version = "1.0.2" @@ -852,9 +1300,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "ethnum" version = "1.5.3" @@ -879,6 +1333,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -922,13 +1382,40 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -937,6 +1424,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1044,6 +1537,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1084,12 +1587,101 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.11.1", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + [[package]] name = "h2" version = "0.3.27" @@ -1109,6 +1701,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1121,7 +1725,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1131,6 +1735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "equivalent", + "foldhash 0.2.0", "rayon", "serde", "serde_core", @@ -1154,12 +1759,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hmac" version = "0.12.1" @@ -1342,7 +1959,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1499,6 +2116,7 @@ dependencies = [ "moxcms", "num-traits", "png", + "tiff", "zune-core", "zune-jpeg", ] @@ -1593,6 +2211,64 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1639,6 +2315,12 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "langchainrust" version = "0.2.18" @@ -1690,6 +2372,34 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.8.1", +] + [[package]] name = "libwebp-sys" version = "0.9.6" @@ -1712,6 +2422,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1724,6 +2440,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1794,6 +2516,24 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2074,6 +2814,31 @@ dependencies = [ "pxfm", ] +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap 2.14.0", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "thiserror 2.0.18", + "unicode-ident", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -2091,6 +2856,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -2113,7 +2908,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2148,6 +2943,300 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.1", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", ] [[package]] @@ -2164,7 +3253,7 @@ checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.11.1", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -2284,6 +3373,25 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2302,7 +3410,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -2397,6 +3505,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "platform-agent" version = "0.1.0" @@ -2543,12 +3657,41 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "pom" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postscript" version = "0.14.1" @@ -2589,6 +3732,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2598,6 +3750,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "prometheus" version = "0.14.0" @@ -2668,6 +3826,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2679,9 +3846,9 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2699,7 +3866,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -2718,7 +3885,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.59.0", ] @@ -2809,6 +3976,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.12.0" @@ -2829,6 +4002,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2838,6 +4020,15 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -2887,6 +4078,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.11.27" @@ -2987,6 +4184,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3002,6 +4205,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3011,8 +4227,8 @@ dependencies = [ "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.59.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -3071,6 +4287,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -3153,7 +4378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -3292,6 +4517,13 @@ dependencies = [ "syn", ] +[[package]] +name = "server-manager-panel" +version = "0.1.0" +dependencies = [ + "eframe", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3384,6 +4616,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -3408,12 +4656,93 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.10" @@ -3776,6 +5105,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -3854,7 +5189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3877,8 +5212,8 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -3943,6 +5278,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -4303,6 +5652,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "tungstenite" version = "0.27.0" @@ -4340,6 +5695,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.2", +] + [[package]] name = "type1-encoding-parser" version = "0.1.1" @@ -4382,6 +5746,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4454,6 +5824,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4589,6 +5969,141 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.95" @@ -4609,6 +6124,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + [[package]] name = "webp" version = "0.3.1" @@ -4643,13 +6174,109 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libloading", + "log", + "naga", + "portable-atomic", + "portable-atomic-util", + "raw-window-handle", + "renderdoc-sys", + "thiserror 2.0.18", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.18", + "web-sys", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -4868,6 +6495,57 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.1", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "1.0.1" @@ -4987,6 +6665,69 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "yoke" version = "0.8.2" diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index cdc461bd..41834fac 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -40,6 +40,7 @@ members = [ "crates/platform-wechat", "crates/platform-speech", "crates/platform-agent", + "crates/server-manager-panel", "crates/shared-contracts", "crates/shared-kernel", "crates/shared-logging", diff --git a/server-rs/crates/server-manager-panel/Cargo.toml b/server-rs/crates/server-manager-panel/Cargo.toml new file mode 100644 index 00000000..cf41e79f --- /dev/null +++ b/server-rs/crates/server-manager-panel/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "server-manager-panel" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +eframe = { version = "0.33", default-features = false, features = [ + "default_fonts", + "glow", + "wayland", + "x11", +] } diff --git a/server-rs/crates/server-manager-panel/src/app.rs b/server-rs/crates/server-manager-panel/src/app.rs new file mode 100644 index 00000000..29463956 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/app.rs @@ -0,0 +1,577 @@ +use eframe::egui; + +use crate::health::{ + DiskSnapshot, HealthLevel, MemorySnapshot, ProbeSnapshot, ServerHealthReport, ServiceSnapshot, +}; +use crate::remote::{ + RemoteEvent, RemoteReceiver, RemoteSender, ServiceAction, channel, spawn_health_check, + spawn_service_action, +}; +use crate::ssh_config::{SshAlias, discover_ssh_aliases}; + +const DEFAULT_MANAGED_SERVICES: &[&str] = &[ + "genarrative-api.service", + "spacetimedb.service", + "nginx.service", + "genarrative-health-patrol.timer", + "genarrative-database-backup.timer", +]; + +#[derive(Debug)] +pub struct ServerManagerApp { + servers: Vec, + selected_alias: Option, + sidebar_collapsed: bool, + tx: RemoteSender, + rx: RemoteReceiver, + pending_confirmation: Option, + custom_service_name: String, +} + +impl Default for ServerManagerApp { + fn default() -> Self { + let (tx, rx) = channel(); + let aliases = discover_ssh_aliases(); + let selected_alias = aliases.first().map(|alias| alias.name.clone()); + Self { + servers: aliases.into_iter().map(ServerState::new).collect(), + selected_alias, + sidebar_collapsed: false, + tx, + rx, + pending_confirmation: None, + custom_service_name: String::new(), + } + } +} + +impl eframe::App for ServerManagerApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.drain_remote_events(ctx); + self.render_confirm_dialog(ctx); + + egui::TopBottomPanel::top("top_bar").show(ctx, |ui| { + ui.horizontal(|ui| { + if ui + .button(if self.sidebar_collapsed { + "展开侧栏" + } else { + "收起侧栏" + }) + .clicked() + { + self.sidebar_collapsed = !self.sidebar_collapsed; + } + if ui.button("重新读取 SSH alias").clicked() { + self.reload_aliases(); + } + if let Some(alias) = self.selected_alias.clone() { + if ui.button("刷新当前服务器").clicked() { + self.refresh_server(&alias); + } + } + ui.separator(); + ui.label("本地 SSH alias 管理"); + }); + }); + + if !self.sidebar_collapsed { + egui::SidePanel::left("server_sidebar") + .resizable(true) + .default_width(260.0) + .width_range(180.0..=360.0) + .show(ctx, |ui| self.render_sidebar(ui)); + } + + egui::CentralPanel::default().show(ctx, |ui| { + if self.servers.is_empty() { + self.render_empty_state(ui); + return; + } + + let Some(alias) = self.selected_alias.clone() else { + self.render_empty_state(ui); + return; + }; + + if let Some(index) = self.server_index(&alias) { + self.render_server_detail(ui, index); + } else { + ui.label("请选择服务器"); + } + }); + } +} + +impl ServerManagerApp { + fn drain_remote_events(&mut self, ctx: &egui::Context) { + while let Ok(event) = self.rx.try_recv() { + match event { + RemoteEvent::Health { alias, result } => { + if let Some(server) = self.server_mut(&alias) { + server.loading = false; + match result { + Ok(report) => { + server.error = None; + server.report = Some(report); + } + Err(error) => { + server.error = Some(error); + } + } + } + } + RemoteEvent::ServiceAction { + alias, + service, + action, + result, + } => { + if let Some(server) = self.server_mut(&alias) { + server.action_in_progress = None; + server.action_log = Some(format!( + "{} {}: {}\n{}{}", + action.label(), + service, + result.summary, + result.stdout, + result.stderr + )); + server.loading = true; + spawn_health_check(alias, self.tx.clone()); + } + } + } + ctx.request_repaint(); + } + } + + fn render_sidebar(&mut self, ui: &mut egui::Ui) { + ui.heading("服务器"); + ui.add_space(8.0); + let mut refresh_alias: Option = None; + + for server in &mut self.servers { + let selected = self.selected_alias.as_deref() == Some(server.alias.name.as_str()); + let response = ui.selectable_label(selected, server_label(server)); + if response.clicked() { + self.selected_alias = Some(server.alias.name.clone()); + } + response.on_hover_text(server.alias.source.display().to_string()); + ui.horizontal(|ui| { + let status = server.status(); + ui.colored_label(level_color(status), status.label()); + if server.loading { + ui.spinner(); + } + if ui.small_button("刷新").clicked() { + refresh_alias = Some(server.alias.name.clone()); + } + }); + ui.add_space(6.0); + } + + if let Some(alias) = refresh_alias { + self.refresh_server(&alias); + } + } + + fn render_empty_state(&mut self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.heading("未发现 SSH alias"); + ui.label("请在 ~/.ssh/config 中配置 Host alias 后重新读取。"); + if ui.button("重新读取").clicked() { + self.reload_aliases(); + } + }); + } + + fn render_server_detail(&mut self, ui: &mut egui::Ui, index: usize) { + let alias = self.servers[index].alias.name.clone(); + let status = self.servers[index].status(); + let loading = self.servers[index].loading; + let report = self.servers[index].report.clone(); + let error = self.servers[index].error.clone(); + let action_log = self.servers[index].action_log.clone(); + + ui.horizontal(|ui| { + ui.heading(&alias); + ui.colored_label(level_color(status), status.label()); + if loading { + ui.spinner(); + } + }); + ui.add_space(8.0); + + if let Some(error) = error { + ui.colored_label(warning_color(), format!("SSH 巡检失败:{error}")); + ui.add_space(8.0); + } + + if let Some(report) = report { + self.render_report(ui, &alias, &report); + } else { + ui.label("尚未执行巡检。"); + } + + ui.add_space(12.0); + self.render_service_controls(ui, &alias, index); + + if let Some(log) = action_log { + ui.add_space(12.0); + egui::CollapsingHeader::new("最近一次服务操作输出") + .default_open(true) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut log.clone()) + .font(egui::TextStyle::Monospace) + .desired_rows(8) + .interactive(false), + ); + }); + } + } + + fn render_report(&self, ui: &mut egui::Ui, alias: &str, report: &ServerHealthReport) { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + info_chip(ui, "主机", value_or_dash(&report.host.hostname)); + info_chip(ui, "内核", value_or_dash(&report.host.kernel)); + info_chip(ui, "运行时间", value_or_dash(&report.host.uptime)); + info_chip(ui, "检查时间", value_or_dash(&report.checked_at)); + }); + + ui.add_space(10.0); + egui::CollapsingHeader::new("硬件状态") + .default_open(true) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + info_chip(ui, "CPU", value_or_dash(&report.hardware.cpu_model)); + info_chip(ui, "核心", value_or_dash(&report.hardware.cpu_cores)); + info_chip(ui, "负载", value_or_dash(&report.hardware.load_average)); + }); + ui.add_space(6.0); + memory_row(ui, "内存", &report.hardware.memory); + memory_row(ui, "Swap", &report.hardware.swap); + ui.add_space(6.0); + for disk in &report.hardware.disks { + disk_row(ui, disk); + } + ui.add_space(6.0); + for sensor in &report.hardware.sensors { + ui.label(sensor); + } + }); + + egui::CollapsingHeader::new("服务状态") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new(format!("{alias}_services")) + .striped(true) + .show(ui, |ui| { + ui.strong("服务"); + ui.strong("状态"); + ui.strong("子状态"); + ui.strong("Unit"); + ui.end_row(); + for service in &report.services { + service_row(ui, service); + } + }); + }); + + egui::CollapsingHeader::new("HTTP 探测") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new(format!("{alias}_probes")) + .striped(true) + .show(ui, |ui| { + ui.strong("探测"); + ui.strong("状态码"); + ui.strong("耗时"); + ui.strong("目标"); + ui.end_row(); + for probe in &report.probes { + probe_row(ui, probe); + } + }); + }); + + if let Some(patrol) = &report.health_patrol { + egui::CollapsingHeader::new("生产健康巡检") + .default_open(true) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.colored_label(level_color(patrol.level), &patrol.status); + ui.label(value_or_dash(&patrol.checked_at)); + ui.label(value_or_dash(&patrol.summary)); + }); + }); + } + + egui::CollapsingHeader::new("原始巡检输出").show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut report.raw_output.clone()) + .font(egui::TextStyle::Monospace) + .desired_rows(12) + .interactive(false), + ); + }); + }); + } + + fn render_service_controls(&mut self, ui: &mut egui::Ui, alias: &str, index: usize) { + ui.heading("服务控制"); + ui.add_space(4.0); + + let action_in_progress = self.servers[index].action_in_progress.clone(); + for service in DEFAULT_MANAGED_SERVICES { + ui.horizontal(|ui| { + ui.label(*service); + for action in [ + ServiceAction::Start, + ServiceAction::Stop, + ServiceAction::Restart, + ] { + let disabled = action_in_progress.is_some(); + if ui + .add_enabled(!disabled, egui::Button::new(action.label())) + .clicked() + { + self.pending_confirmation = Some(ServiceConfirmation { + alias: alias.to_owned(), + service: (*service).to_owned(), + action, + }); + } + } + }); + } + + ui.add_space(8.0); + ui.horizontal(|ui| { + ui.label("其他 unit"); + ui.text_edit_singleline(&mut self.custom_service_name); + if ui.button("启动").clicked() { + self.confirm_custom_service(alias, ServiceAction::Start); + } + if ui.button("关闭").clicked() { + self.confirm_custom_service(alias, ServiceAction::Stop); + } + if ui.button("重启").clicked() { + self.confirm_custom_service(alias, ServiceAction::Restart); + } + }); + + if let Some(action) = action_in_progress { + ui.label(format!("正在执行:{action}")); + } + } + + fn render_confirm_dialog(&mut self, ctx: &egui::Context) { + let Some(confirmation) = self.pending_confirmation.clone() else { + return; + }; + + egui::Window::new("确认服务操作") + .collapsible(false) + .resizable(false) + .show(ctx, |ui| { + ui.label(format!( + "确认在 {} 上{} {}?", + confirmation.alias, + confirmation.action.label(), + confirmation.service + )); + ui.add_space(8.0); + ui.horizontal(|ui| { + if ui.button("确认").clicked() { + self.execute_service_action(&confirmation); + self.pending_confirmation = None; + } + if ui.button("取消").clicked() { + self.pending_confirmation = None; + } + }); + }); + } + + fn reload_aliases(&mut self) { + let aliases = discover_ssh_aliases(); + self.servers = aliases.into_iter().map(ServerState::new).collect(); + self.selected_alias = self.servers.first().map(|server| server.alias.name.clone()); + } + + fn refresh_server(&mut self, alias: &str) { + if let Some(server) = self.server_mut(alias) { + server.loading = true; + server.error = None; + } + spawn_health_check(alias.to_owned(), self.tx.clone()); + } + + fn confirm_custom_service(&mut self, alias: &str, action: ServiceAction) { + let service = self.custom_service_name.trim(); + if service.is_empty() { + return; + } + self.pending_confirmation = Some(ServiceConfirmation { + alias: alias.to_owned(), + service: service.to_owned(), + action, + }); + } + + fn execute_service_action(&mut self, confirmation: &ServiceConfirmation) { + if let Some(server) = self.server_mut(&confirmation.alias) { + server.action_in_progress = Some(format!( + "{} {}", + confirmation.action.label(), + confirmation.service + )); + server.action_log = None; + } + spawn_service_action( + confirmation.alias.clone(), + confirmation.service.clone(), + confirmation.action, + self.tx.clone(), + ); + } + + fn server_index(&self, alias: &str) -> Option { + self.servers + .iter() + .position(|server| server.alias.name == alias) + } + + fn server_mut(&mut self, alias: &str) -> Option<&mut ServerState> { + self.servers + .iter_mut() + .find(|server| server.alias.name == alias) + } +} + +#[derive(Debug, Clone)] +struct ServiceConfirmation { + alias: String, + service: String, + action: ServiceAction, +} + +#[derive(Debug)] +struct ServerState { + alias: SshAlias, + report: Option, + loading: bool, + error: Option, + action_in_progress: Option, + action_log: Option, +} + +impl ServerState { + fn new(alias: SshAlias) -> Self { + Self { + alias, + report: None, + loading: false, + error: None, + action_in_progress: None, + action_log: None, + } + } + + fn status(&self) -> HealthLevel { + if self.error.is_some() { + HealthLevel::Critical + } else { + self.report + .as_ref() + .map(|report| report.status) + .unwrap_or(HealthLevel::Unknown) + } + } +} + +fn server_label(server: &ServerState) -> String { + let prefix = match server.status() { + HealthLevel::Ok => "[OK]", + HealthLevel::Warning => "[!]", + HealthLevel::Critical => "[X]", + HealthLevel::Unknown => "[?]", + }; + format!("{prefix} {}", server.alias.name) +} + +fn service_row(ui: &mut egui::Ui, service: &ServiceSnapshot) { + ui.label(&service.name); + ui.colored_label(level_color(service.level), &service.active); + ui.label(&service.sub); + ui.label(&service.unit_file); + ui.end_row(); +} + +fn probe_row(ui: &mut egui::Ui, probe: &ProbeSnapshot) { + ui.label(&probe.name); + ui.colored_label(level_color(probe.level), &probe.http_code); + ui.label( + probe + .elapsed_ms + .map(|elapsed| format!("{elapsed}ms")) + .unwrap_or_else(|| "-".to_owned()), + ); + ui.label(&probe.target); + ui.end_row(); +} + +fn memory_row(ui: &mut egui::Ui, label: &str, memory: &MemorySnapshot) { + let percent = memory.used_percent.unwrap_or_default(); + ui.horizontal(|ui| { + ui.label(label); + ui.add(egui::ProgressBar::new(f32::from(percent) / 100.0).text(format!("{percent}%"))); + ui.label(format!( + "已用 {} / 总计 {},可用 {}", + value_or_dash(&memory.used), + value_or_dash(&memory.total), + value_or_dash(&memory.available) + )); + }); +} + +fn disk_row(ui: &mut egui::Ui, disk: &DiskSnapshot) { + let percent = disk.used_percent.unwrap_or_default(); + ui.horizontal(|ui| { + ui.label(&disk.mount); + ui.add(egui::ProgressBar::new(f32::from(percent) / 100.0).text(format!("{percent}%"))); + ui.label(format!( + "{} 已用 {} / {},可用 {}", + disk.filesystem, disk.used, disk.size, disk.available + )); + }); +} + +fn info_chip(ui: &mut egui::Ui, label: &str, value: &str) { + ui.group(|ui| { + ui.vertical(|ui| { + ui.small(label); + ui.label(value); + }); + }); +} + +fn value_or_dash(value: &str) -> &str { + if value.trim().is_empty() { "-" } else { value } +} + +fn level_color(level: HealthLevel) -> egui::Color32 { + match level { + HealthLevel::Ok => egui::Color32::from_rgb(38, 166, 91), + HealthLevel::Warning => egui::Color32::from_rgb(214, 137, 16), + HealthLevel::Critical => egui::Color32::from_rgb(205, 66, 70), + HealthLevel::Unknown => egui::Color32::from_rgb(120, 126, 136), + } +} + +fn warning_color() -> egui::Color32 { + egui::Color32::from_rgb(205, 66, 70) +} diff --git a/server-rs/crates/server-manager-panel/src/fonts.rs b/server-rs/crates/server-manager-panel/src/fonts.rs new file mode 100644 index 00000000..298e0e04 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/fonts.rs @@ -0,0 +1,128 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; + +use eframe::egui::{FontData, FontDefinitions, FontFamily}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CjkFontCandidate { + pub path: PathBuf, + pub index: u32, +} + +pub fn install_cjk_font(ctx: &eframe::egui::Context) -> Option { + let candidate = find_cjk_font_candidate()?; + let bytes = std::fs::read(&candidate.path).ok()?; + let mut font_data = FontData::from_owned(bytes); + font_data.index = candidate.index; + + let mut definitions = FontDefinitions::default(); + definitions + .font_data + .insert("genarrative-cjk".to_owned(), Arc::new(font_data)); + + // 中文注释:作为 fallback 注入,保留 egui 默认拉丁/图标字体,同时补齐中文 glyph。 + for family in [FontFamily::Proportional, FontFamily::Monospace] { + definitions + .families + .entry(family) + .or_default() + .push("genarrative-cjk".to_owned()); + } + + ctx.set_fonts(definitions); + Some(candidate) +} + +pub fn find_cjk_font_candidate() -> Option { + if let Ok(path) = std::env::var("GENARRATIVE_SERVER_PANEL_CJK_FONT") { + if let Some(candidate) = parse_font_spec(&path) { + return Some(candidate); + } + } + + const KNOWN_PATHS: &[(&str, u32)] = &[ + ("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 2), + ("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc", 2), + ("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 0), + ("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", 0), + ( + "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", + 0, + ), + ( + "/home/dsk/.local/share/fonts/genarrative-cjk/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + 0, + ), + ]; + + for (path, index) in KNOWN_PATHS { + if Path::new(path).is_file() { + return Some(CjkFontCandidate { + path: PathBuf::from(path), + index: *index, + }); + } + } + + for family in [ + "Noto Sans CJK SC", + "WenQuanYi Zen Hei", + "Droid Sans Fallback", + ] { + if let Some(candidate) = find_with_fc_match(family) { + return Some(candidate); + } + } + + None +} + +fn parse_font_spec(raw: &str) -> Option { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return None; + } + let (path, index) = trimmed + .rsplit_once('|') + .and_then(|(path, index)| Some((path, index.parse().ok()?))) + .unwrap_or((trimmed, 0)); + let path = PathBuf::from(path); + path.is_file().then_some(CjkFontCandidate { path, index }) +} + +fn find_with_fc_match(family: &str) -> Option { + let output = Command::new("fc-match") + .arg("-f") + .arg("%{file}|%{index}\n") + .arg(family) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().find_map(parse_font_spec) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_font_path_with_index() { + let candidate = parse_font_spec("/tmp/missing-font.ttc|2"); + assert_eq!(candidate, None); + } + + #[test] + fn finds_existing_system_cjk_font() { + let candidate = find_cjk_font_candidate(); + assert!( + candidate + .as_ref() + .is_some_and(|candidate| candidate.path.is_file()), + "expected at least one CJK font on this development host" + ); + } +} diff --git a/server-rs/crates/server-manager-panel/src/health.rs b/server-rs/crates/server-manager-panel/src/health.rs new file mode 100644 index 00000000..beacc619 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/health.rs @@ -0,0 +1,474 @@ +use std::collections::BTreeMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum HealthLevel { + Unknown, + Ok, + Warning, + Critical, +} + +impl HealthLevel { + pub fn label(self) -> &'static str { + match self { + HealthLevel::Unknown => "未知", + HealthLevel::Ok => "正常", + HealthLevel::Warning => "警告", + HealthLevel::Critical => "异常", + } + } + + pub fn rank(self) -> u8 { + match self { + HealthLevel::Unknown => 1, + HealthLevel::Ok => 0, + HealthLevel::Warning => 2, + HealthLevel::Critical => 3, + } + } +} + +#[derive(Debug, Clone)] +pub struct ServerHealthReport { + pub status: HealthLevel, + pub checked_at: String, + pub host: HostSnapshot, + pub hardware: HardwareSnapshot, + pub services: Vec, + pub probes: Vec, + pub health_patrol: Option, + pub raw_output: String, +} + +#[derive(Debug, Clone, Default)] +pub struct HostSnapshot { + pub hostname: String, + pub kernel: String, + pub uptime: String, +} + +#[derive(Debug, Clone, Default)] +pub struct HardwareSnapshot { + pub cpu_model: String, + pub cpu_cores: String, + pub load_average: String, + pub memory: MemorySnapshot, + pub swap: MemorySnapshot, + pub disks: Vec, + pub sensors: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct MemorySnapshot { + pub total: String, + pub used: String, + pub free: String, + pub available: String, + pub used_percent: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct DiskSnapshot { + pub mount: String, + pub filesystem: String, + pub size: String, + pub used: String, + pub available: String, + pub used_percent: Option, +} + +#[derive(Debug, Clone)] +pub struct ServiceSnapshot { + pub name: String, + pub active: String, + pub sub: String, + pub unit_file: String, + pub level: HealthLevel, +} + +#[derive(Debug, Clone)] +pub struct ProbeSnapshot { + pub name: String, + pub target: String, + pub http_code: String, + pub elapsed_ms: Option, + pub level: HealthLevel, +} + +#[derive(Debug, Clone)] +pub struct HealthPatrolSnapshot { + pub status: String, + pub checked_at: String, + pub summary: String, + pub level: HealthLevel, +} + +pub fn parse_health_report(raw_output: &str) -> ServerHealthReport { + let mut sections: BTreeMap> = BTreeMap::new(); + let mut current = String::new(); + + for line in raw_output.lines() { + if let Some(name) = parse_section_marker(line) { + current = name.to_owned(); + sections.entry(current.clone()).or_default(); + } else if !current.is_empty() { + sections + .entry(current.clone()) + .or_default() + .push(line.to_owned()); + } + } + + let mut report = ServerHealthReport { + status: HealthLevel::Unknown, + checked_at: section_value(§ions, "checked_at").unwrap_or_default(), + host: parse_host(§ions), + hardware: parse_hardware(§ions), + services: parse_services(§ions), + probes: parse_probes(§ions), + health_patrol: parse_health_patrol(§ions), + raw_output: raw_output.to_owned(), + }; + report.status = summarize_report(&report); + report +} + +pub fn summarize_report(report: &ServerHealthReport) -> HealthLevel { + let mut status = HealthLevel::Ok; + for level in report + .services + .iter() + .map(|service| service.level) + .chain(report.probes.iter().map(|probe| probe.level)) + .chain(report.health_patrol.iter().map(|patrol| patrol.level)) + { + if level.rank() > status.rank() { + status = level; + } + } + + if let Some(used_percent) = report.hardware.memory.used_percent { + let memory_level = if used_percent >= 95 { + HealthLevel::Critical + } else if used_percent >= 85 { + HealthLevel::Warning + } else { + HealthLevel::Ok + }; + if memory_level.rank() > status.rank() { + status = memory_level; + } + } + + for disk in &report.hardware.disks { + let disk_level = match disk.used_percent { + Some(percent) if percent >= 95 => HealthLevel::Critical, + Some(percent) if percent >= 85 => HealthLevel::Warning, + _ => HealthLevel::Ok, + }; + if disk_level.rank() > status.rank() { + status = disk_level; + } + } + + status +} + +fn parse_section_marker(line: &str) -> Option<&str> { + line.strip_prefix("==GENARRATIVE_PANEL:") + .and_then(|rest| rest.strip_suffix("==")) +} + +fn section_value(sections: &BTreeMap>, name: &str) -> Option { + sections.get(name).and_then(|lines| { + lines + .iter() + .map(|line| line.trim()) + .find(|line| !line.is_empty()) + .map(str::to_owned) + }) +} + +fn parse_host(sections: &BTreeMap>) -> HostSnapshot { + HostSnapshot { + hostname: section_value(sections, "hostname").unwrap_or_default(), + kernel: section_value(sections, "kernel").unwrap_or_default(), + uptime: section_value(sections, "uptime").unwrap_or_default(), + } +} + +fn parse_hardware(sections: &BTreeMap>) -> HardwareSnapshot { + HardwareSnapshot { + cpu_model: section_value(sections, "cpu_model").unwrap_or_default(), + cpu_cores: section_value(sections, "cpu_cores").unwrap_or_default(), + load_average: section_value(sections, "load_average").unwrap_or_default(), + memory: parse_memory(section_value(sections, "memory").as_deref()), + swap: parse_memory(section_value(sections, "swap").as_deref()), + disks: parse_disks(sections), + sensors: sections.get("sensors").cloned().unwrap_or_default(), + } +} + +fn parse_memory(value: Option<&str>) -> MemorySnapshot { + let Some(value) = value else { + return MemorySnapshot::default(); + }; + let parts: Vec<&str> = value.split('|').collect(); + MemorySnapshot { + total: parts.first().copied().unwrap_or_default().to_owned(), + used: parts.get(1).copied().unwrap_or_default().to_owned(), + free: parts.get(2).copied().unwrap_or_default().to_owned(), + available: parts.get(3).copied().unwrap_or_default().to_owned(), + used_percent: parts.get(4).and_then(|value| parse_percent(value)), + } +} + +fn parse_disks(sections: &BTreeMap>) -> Vec { + sections + .get("disks") + .into_iter() + .flatten() + .filter_map(|line| { + let parts: Vec<&str> = line.split('|').collect(); + (parts.len() >= 6).then(|| DiskSnapshot { + filesystem: parts[0].to_owned(), + size: parts[1].to_owned(), + used: parts[2].to_owned(), + available: parts[3].to_owned(), + used_percent: parse_percent(parts[4]), + mount: parts[5].to_owned(), + }) + }) + .collect() +} + +fn parse_services(sections: &BTreeMap>) -> Vec { + sections + .get("services") + .into_iter() + .flatten() + .filter_map(|line| { + let parts: Vec<&str> = line.split('|').collect(); + (parts.len() >= 4).then(|| { + let active = parts[1].to_owned(); + let sub = parts[2].to_owned(); + let level = if active == "active" { + HealthLevel::Ok + } else if active == "unknown" || active == "inactive" { + HealthLevel::Warning + } else { + HealthLevel::Critical + }; + ServiceSnapshot { + name: parts[0].to_owned(), + active, + sub, + unit_file: parts[3].to_owned(), + level, + } + }) + }) + .collect() +} + +fn parse_probes(sections: &BTreeMap>) -> Vec { + sections + .get("probes") + .into_iter() + .flatten() + .filter_map(|line| { + let parts: Vec<&str> = line.split('|').collect(); + (parts.len() >= 4).then(|| { + let http_code = parts[2].to_owned(); + let elapsed_ms = parts[3].parse().ok(); + let level = if http_code.starts_with('2') { + HealthLevel::Ok + } else if http_code == "000" { + HealthLevel::Critical + } else { + HealthLevel::Critical + }; + ProbeSnapshot { + name: parts[0].to_owned(), + target: parts[1].to_owned(), + http_code, + elapsed_ms, + level, + } + }) + }) + .collect() +} + +fn parse_health_patrol(sections: &BTreeMap>) -> Option { + let line = section_value(sections, "health_patrol")?; + let parts: Vec<&str> = line.split('|').collect(); + let status = parts.first().copied().unwrap_or_default().to_owned(); + let level = match status.as_str() { + "OK" => HealthLevel::Ok, + "WARNING" => HealthLevel::Warning, + "CRITICAL" => HealthLevel::Critical, + _ => HealthLevel::Unknown, + }; + Some(HealthPatrolSnapshot { + status, + checked_at: parts.get(1).copied().unwrap_or_default().to_owned(), + summary: parts.get(2).copied().unwrap_or_default().to_owned(), + level, + }) +} + +fn parse_percent(value: &str) -> Option { + value.trim_end_matches('%').parse().ok() +} + +pub const HEALTH_SCRIPT: &str = r#"set -eu + +print_section() { + printf '==GENARRATIVE_PANEL:%s==\n' "$1" +} + +print_section checked_at +date -Is 2>/dev/null || date + +print_section hostname +hostname 2>/dev/null || true + +print_section kernel +uname -srmo 2>/dev/null || uname -a 2>/dev/null || true + +print_section uptime +uptime -p 2>/dev/null || uptime 2>/dev/null || true + +print_section cpu_model +awk -F: '/model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}' /proc/cpuinfo 2>/dev/null || true + +print_section cpu_cores +nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || true + +print_section load_average +cat /proc/loadavg 2>/dev/null | awk '{print $1" "$2" "$3}' || true + +print_section memory +awk ' + /^MemTotal:/ {total=$2} + /^MemFree:/ {free=$2} + /^MemAvailable:/ {available=$2} + END { + if (total > 0) { + used = total - free + percent = int((used * 100 + total / 2) / total) + printf "%.1f GiB|%.1f GiB|%.1f GiB|%.1f GiB|%d%%\n", total/1048576, used/1048576, free/1048576, available/1048576, percent + } + } +' /proc/meminfo 2>/dev/null || true + +print_section swap +awk ' + /^SwapTotal:/ {total=$2} + /^SwapFree:/ {free=$2} + END { + if (total > 0) { + used = total - free + percent = int((used * 100 + total / 2) / total) + printf "%.1f GiB|%.1f GiB|%.1f GiB|%.1f GiB|%d%%\n", total/1048576, used/1048576, free/1048576, free/1048576, percent + } else { + print "0 GiB|0 GiB|0 GiB|0 GiB|0%" + } + } +' /proc/meminfo 2>/dev/null || true + +print_section disks +for mount in / /var /opt /stdb /data; do + if [ -e "$mount" ]; then + df -hP "$mount" 2>/dev/null | awk 'NR == 2 {print $1"|"$2"|"$3"|"$4"|"$5"|"$6}' + fi +done | awk '!seen[$6]++' + +print_section sensors +if command -v sensors >/dev/null 2>&1; then + sensors 2>/dev/null | sed -n '1,20p' +else + echo "sensors 未安装" +fi + +print_section services +for service in genarrative-api.service spacetimedb.service nginx.service genarrative-health-patrol.timer genarrative-database-backup.timer; do + active=$(systemctl is-active "$service" 2>/dev/null || true) + sub=$(systemctl show "$service" -p SubState --value 2>/dev/null || true) + unit_file=$(systemctl show "$service" -p UnitFileState --value 2>/dev/null || true) + [ -n "$active" ] || active="unknown" + [ -n "$sub" ] || sub="unknown" + [ -n "$unit_file" ] || unit_file="unknown" + printf '%s|%s|%s|%s\n' "$service" "$active" "$sub" "$unit_file" +done + +print_section probes +probe() { + name="$1" + url="$2" + tmp=$(mktemp) + code=$(curl -fsS -m 5 -o /dev/null -w '%{http_code}|%{time_total}' "$url" 2>"$tmp" || true) + if [ -z "$code" ]; then + code="000|0" + fi + http_code=${code%%|*} + time_total=${code#*|} + elapsed_ms=$(awk "BEGIN {printf \"%d\", $time_total * 1000}") + printf '%s|%s|%s|%s\n' "$name" "$url" "$http_code" "$elapsed_ms" + rm -f "$tmp" +} +probe "api:/healthz" "http://127.0.0.1:8082/healthz" +probe "api:/readyz" "http://127.0.0.1:8082/readyz" +probe "spacetimedb:/v1/ping" "http://127.0.0.1:3101/v1/ping" +probe "public:/api/creation-entry/config" "http://127.0.0.1:8082/api/creation-entry/config" +probe "public:/api/runtime/puzzle/gallery" "http://127.0.0.1:8082/api/runtime/puzzle/gallery" + +print_section health_patrol +if [ -r /var/lib/genarrative/health-patrol/status.json ]; then + node -e ' + const fs = require("fs"); + const payload = JSON.parse(fs.readFileSync("/var/lib/genarrative/health-patrol/status.json", "utf8")); + const status = payload.status || "UNKNOWN"; + const checkedAt = payload.checkedAt || ""; + const checks = Array.isArray(payload.checks) ? payload.checks : []; + const summary = checks.filter((check) => check.status && check.status !== "OK").slice(0, 3).map((check) => `${check.name}:${check.status}`).join(","); + console.log(`${status}|${checkedAt}|${summary}`); + ' 2>/dev/null || echo "UNKNOWN||状态文件解析失败" +else + echo "UNKNOWN||未找到 /var/lib/genarrative/health-patrol/status.json" +fi +"#; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_report_sections() { + let report = parse_health_report( + r#"==GENARRATIVE_PANEL:checked_at== +2026-06-11T12:00:00+08:00 +==GENARRATIVE_PANEL:hostname== +release +==GENARRATIVE_PANEL:memory== +2.0 GiB|1.0 GiB|1.0 GiB|1.0 GiB|50% +==GENARRATIVE_PANEL:disks== +/dev/sda1|40G|20G|20G|50%|/ +==GENARRATIVE_PANEL:services== +genarrative-api.service|active|running|enabled +spacetimedb.service|failed|failed|enabled +==GENARRATIVE_PANEL:probes== +api:/readyz|http://127.0.0.1:8082/readyz|200|18 +==GENARRATIVE_PANEL:health_patrol== +WARNING|2026-06-11T11:59:00Z|journal:WARNING +"#, + ); + + assert_eq!(report.host.hostname, "release"); + assert_eq!(report.hardware.memory.used_percent, Some(50)); + assert_eq!(report.services.len(), 2); + assert_eq!(report.probes[0].http_code, "200"); + assert_eq!(report.status, HealthLevel::Critical); + } +} diff --git a/server-rs/crates/server-manager-panel/src/lib.rs b/server-rs/crates/server-manager-panel/src/lib.rs new file mode 100644 index 00000000..4b8838f4 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/lib.rs @@ -0,0 +1,5 @@ +pub mod app; +pub mod fonts; +pub mod health; +pub mod remote; +pub mod ssh_config; diff --git a/server-rs/crates/server-manager-panel/src/main.rs b/server-rs/crates/server-manager-panel/src/main.rs new file mode 100644 index 00000000..888f683b --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/main.rs @@ -0,0 +1,21 @@ +use eframe::egui; +use server_manager_panel::app::ServerManagerApp; +use server_manager_panel::fonts::install_cjk_font; + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([1180.0, 760.0]) + .with_min_inner_size([920.0, 620.0]), + ..Default::default() + }; + + eframe::run_native( + "Genarrative 服务器管理面板", + native_options, + Box::new(|cc| { + install_cjk_font(&cc.egui_ctx); + Ok(Box::new(ServerManagerApp::default())) + }), + ) +} diff --git a/server-rs/crates/server-manager-panel/src/remote.rs b/server-rs/crates/server-manager-panel/src/remote.rs new file mode 100644 index 00000000..61b9d5b7 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/remote.rs @@ -0,0 +1,231 @@ +use std::io::Write; +use std::process::{Command, Stdio}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +use crate::health::{HEALTH_SCRIPT, ServerHealthReport, parse_health_report}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceAction { + Start, + Stop, + Restart, +} + +impl ServiceAction { + pub fn as_systemctl_arg(self) -> &'static str { + match self { + ServiceAction::Start => "start", + ServiceAction::Stop => "stop", + ServiceAction::Restart => "restart", + } + } + + pub fn label(self) -> &'static str { + match self { + ServiceAction::Start => "启动", + ServiceAction::Stop => "关闭", + ServiceAction::Restart => "重启", + } + } +} + +#[derive(Debug, Clone)] +pub struct RemoteCommandResult { + pub success: bool, + pub summary: String, + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug)] +pub enum RemoteEvent { + Health { + alias: String, + result: Result, + }, + ServiceAction { + alias: String, + service: String, + action: ServiceAction, + result: RemoteCommandResult, + }, +} + +pub type RemoteSender = mpsc::Sender; +pub type RemoteReceiver = mpsc::Receiver; + +pub fn channel() -> (RemoteSender, RemoteReceiver) { + mpsc::channel() +} + +pub fn spawn_health_check(alias: String, tx: RemoteSender) { + thread::spawn(move || { + let result = + run_ssh_script(&alias, HEALTH_SCRIPT, Duration::from_secs(20)).and_then(|output| { + if output.success { + Ok(parse_health_report(&output.stdout)) + } else { + Err(format_remote_error(&output)) + } + }); + let _ = tx.send(RemoteEvent::Health { alias, result }); + }); +} + +pub fn spawn_service_action( + alias: String, + service: String, + action: ServiceAction, + tx: RemoteSender, +) { + thread::spawn(move || { + let result = if is_safe_service_name(&service) { + run_ssh_script( + &alias, + &build_service_action_script(&service, action), + Duration::from_secs(20), + ) + .unwrap_or_else(|error| RemoteCommandResult { + success: false, + summary: error, + stdout: String::new(), + stderr: String::new(), + }) + } else { + RemoteCommandResult { + success: false, + summary: "服务名包含不允许的字符".to_owned(), + stdout: String::new(), + stderr: String::new(), + } + }; + let _ = tx.send(RemoteEvent::ServiceAction { + alias, + service, + action, + result, + }); + }); +} + +pub fn is_safe_service_name(service: &str) -> bool { + !service.is_empty() + && service.len() <= 128 + && service.bytes().all(|byte| { + matches!( + byte, + b'a'..=b'z' + | b'A'..=b'Z' + | b'0'..=b'9' + | b'.' + | b'_' + | b'-' + | b'@' + | b':' + ) + }) +} + +fn build_service_action_script(service: &str, action: ServiceAction) -> String { + format!( + r#"set -eu +service='{service}' +action='{action}' +if [ "$(id -u)" = "0" ]; then + systemctl "$action" "$service" +else + sudo -n systemctl "$action" "$service" +fi +systemctl is-active "$service" || true +systemctl status "$service" --no-pager -l -n 12 || true +"#, + service = service, + action = action.as_systemctl_arg() + ) +} + +fn run_ssh_script( + alias: &str, + script: &str, + timeout: Duration, +) -> Result { + let started = Instant::now(); + let mut child = Command::new("ssh") + .arg("-o") + .arg("BatchMode=yes") + .arg("-o") + .arg("ConnectTimeout=8") + .arg(alias) + .arg("sh") + .arg("-s") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| format!("无法启动 ssh: {error}"))?; + + { + // 中文注释:写完脚本后必须关闭 stdin,让远端 `sh -s` 收到 EOF 并开始退出。 + let Some(mut stdin) = child.stdin.take() else { + return Err("无法写入 ssh stdin".to_owned()); + }; + stdin + .write_all(script.as_bytes()) + .map_err(|error| format!("写入远端脚本失败: {error}"))?; + } + + loop { + match child.try_wait() { + Ok(Some(_status)) => { + let output = child + .wait_with_output() + .map_err(|error| format!("读取 ssh 输出失败: {error}"))?; + let success = output.status.success(); + return Ok(RemoteCommandResult { + success, + summary: if success { + "执行成功".to_owned() + } else { + format!("ssh 退出码 {:?}", output.status.code()) + }, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + Ok(None) if started.elapsed() >= timeout => { + let _ = child.kill(); + let _ = child.wait(); + return Err(format!("ssh 执行超过 {} 秒", timeout.as_secs())); + } + Ok(None) => thread::sleep(Duration::from_millis(80)), + Err(error) => return Err(format!("等待 ssh 进程失败: {error}")), + } + } +} + +fn format_remote_error(result: &RemoteCommandResult) -> String { + let stderr = result.stderr.trim(); + let stdout = result.stdout.trim(); + if !stderr.is_empty() { + format!("{}: {}", result.summary, stderr) + } else if !stdout.is_empty() { + format!("{}: {}", result.summary, stdout) + } else { + result.summary.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn allows_systemd_unit_names_only() { + assert!(is_safe_service_name("genarrative-api.service")); + assert!(is_safe_service_name("worker@1.service")); + assert!(!is_safe_service_name("api.service;rm -rf /")); + assert!(!is_safe_service_name("")); + } +} diff --git a/server-rs/crates/server-manager-panel/src/ssh_config.rs b/server-rs/crates/server-manager-panel/src/ssh_config.rs new file mode 100644 index 00000000..163b1013 --- /dev/null +++ b/server-rs/crates/server-manager-panel/src/ssh_config.rs @@ -0,0 +1,143 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshAlias { + pub name: String, + pub source: PathBuf, +} + +pub fn discover_ssh_aliases() -> Vec { + let Some(home) = std::env::var_os("HOME") else { + return Vec::new(); + }; + let config_path = PathBuf::from(home).join(".ssh/config"); + discover_from_file(&config_path) +} + +pub fn discover_from_file(path: &Path) -> Vec { + let mut visited = HashSet::new(); + let mut aliases = Vec::new(); + discover_inner(path, &mut visited, &mut aliases); + dedupe_aliases(aliases) +} + +fn discover_inner(path: &Path, visited: &mut HashSet, aliases: &mut Vec) { + let Ok(canonical) = path.canonicalize() else { + return; + }; + if !visited.insert(canonical.clone()) { + return; + } + let Ok(content) = fs::read_to_string(&canonical) else { + return; + }; + + for line in content.lines() { + let trimmed = trim_comment(line); + let mut parts = trimmed.split_whitespace(); + let Some(keyword) = parts.next() else { + continue; + }; + if keyword.eq_ignore_ascii_case("host") { + aliases.extend(parts.filter_map(|name| { + is_concrete_alias(name).then(|| SshAlias { + name: name.to_owned(), + source: canonical.clone(), + }) + })); + } else if keyword.eq_ignore_ascii_case("include") { + for include in parts { + for include_path in expand_include_path(include, canonical.parent()) { + discover_inner(&include_path, visited, aliases); + } + } + } + } +} + +fn dedupe_aliases(aliases: Vec) -> Vec { + let mut seen = HashSet::new(); + let mut deduped = Vec::new(); + for alias in aliases { + if seen.insert(alias.name.clone()) { + deduped.push(alias); + } + } + deduped +} + +fn trim_comment(line: &str) -> &str { + line.split('#').next().unwrap_or("").trim() +} + +fn is_concrete_alias(value: &str) -> bool { + !value.is_empty() + && !value.starts_with('-') + && !value.starts_with('!') + && !value.contains('*') + && !value.contains('?') + && !value.contains('%') + && !value.contains('/') +} + +fn expand_include_path(raw: &str, parent: Option<&Path>) -> Vec { + if raw.contains('*') || raw.contains('?') { + // 中文注释:SSH Include 支持复杂 glob;面板只解析普通文件,避免误扫过大目录。 + return Vec::new(); + } + let expanded = if let Some(rest) = raw.strip_prefix("~/") { + std::env::var_os("HOME") + .map(PathBuf::from) + .map(|home| home.join(rest)) + } else { + let path = PathBuf::from(raw); + if path.is_absolute() { + Some(path) + } else { + parent.map(|base| base.join(path)) + } + }; + expanded.into_iter().collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn host_parser_ignores_wildcards_and_negations() { + let mut aliases = Vec::new(); + let source = PathBuf::from("/tmp/config"); + for line in [ + "Host dev release *.internal !blocked", + "Host github.com", + "Host ?pattern", + "Host -bad", + ] { + let trimmed = trim_comment(line); + let mut parts = trimmed.split_whitespace(); + let keyword = parts.next().unwrap(); + if keyword.eq_ignore_ascii_case("host") { + aliases.extend(parts.filter_map(|name| { + is_concrete_alias(name).then(|| SshAlias { + name: name.to_owned(), + source: source.clone(), + }) + })); + } + } + + let names: Vec<_> = dedupe_aliases(aliases) + .into_iter() + .map(|alias| alias.name) + .collect(); + assert_eq!(names, ["dev", "release", "github.com"]); + } + + #[test] + fn comment_trimming_keeps_plain_aliases() { + assert_eq!(trim_comment(" Host dev # release host "), "Host dev"); + } +}