From a799958328212cbf86aee7334db5be59896b1b09 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 4 Jun 2026 23:20:15 +0800 Subject: [PATCH] feat: add driver runtime manager --- Cargo.lock | 89 ++++- .../components/config/DriverStoreDialog.vue | 311 +++++++++++++++++- apps/desktop/src/i18n/locales/en.ts | 29 ++ apps/desktop/src/i18n/locales/es.ts | 29 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 29 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 29 ++ apps/desktop/src/lib/api.ts | 7 + .../src/lib/driverRuntimePresentation.ts | 59 ++++ apps/desktop/src/lib/http.ts | 13 + apps/desktop/src/lib/tauri.ts | 41 +++ crates/dbx-core/Cargo.toml | 1 + crates/dbx-core/src/agent_manager.rs | 4 + crates/dbx-core/src/agent_runtime.rs | 7 + crates/dbx-core/src/connection.rs | 2 +- crates/dbx-core/src/db/agent_driver.rs | 8 + crates/dbx-core/src/driver_runtime.rs | 309 +++++++++++++++++ crates/dbx-core/src/lib.rs | 1 + crates/dbx-core/src/plugins.rs | 5 + crates/dbx-web/src/main.rs | 3 + crates/dbx-web/src/routes/agents.rs | 29 ++ .../driverRuntimePresentation.test.ts | 32 ++ src-tauri/src/commands/agents.rs | 16 + src-tauri/src/lib.rs | 3 + 23 files changed, 1044 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/lib/driverRuntimePresentation.ts create mode 100644 crates/dbx-core/src/driver_runtime.rs create mode 100644 packages/app-tests/driverRuntimePresentation.test.ts diff --git a/Cargo.lock b/Cargo.lock index 3936cc3f79..27a5d72974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,6 +1920,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sqlparser", + "sysinfo", "tiberius", "tokio", "tokio-postgres", @@ -4782,6 +4783,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -7655,6 +7665,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -9440,8 +9464,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -9537,6 +9561,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -9580,14 +9614,26 @@ dependencies = [ "windows-core 0.62.2", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -9599,8 +9645,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -9628,6 +9674,17 @@ dependencies = [ "windows-threading 0.2.1", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -9639,6 +9696,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -9704,6 +9772,15 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 4ea68570c1..9b218df3a7 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -1,7 +1,21 @@ @@ -612,7 +760,7 @@ onUnmounted(() => {
- +
{{ t("driverStore.usageTitle") }}
@@ -646,8 +794,11 @@ onUnmounted(() => { {{ t("driverStore.jdbcDrivers") }} + + {{ t("driverStore.runtimeDrivers") }} + -
+
+ + + +
+
+
+ + + +
+
{{ t("driverStore.runtimeTitle") }}
+
+ + {{ item.label }} + {{ item.value }} + +
+
+
+ +
+ +
+
+ {{ t("driverStore.runtimeLastError") }} +
+
{{
+                  runtimeSummary.last_error
+                }}
+
+ +
+ {{ t("common.loading") }} +
+
+ {{ runtimeError }} +
+
+ {{ t("driverStore.runtimeEmpty") }} +
+
+ +
+
+
+
+ + {{ runtime.label }} + + v{{ runtime.version }} + +
+
+ {{ runtimeKindLabel(runtime) }} + / + {{ runtimeSourceLabel(runtime) }} +
+
+ +
+ {{ + t("driverStore.runtimeHealth") + }} + + {{ runtimeStatusLabel(runtime.status) }} + +
+
+ {{ t("driverStore.runtimePid") }}: {{ runtime.pid ?? "-" }} +
+
+ + {{ formatRuntimeBytes(runtime.memory_bytes) }} +
+
+ + {{ formatRuntimeCpu(runtime.cpu_percent) }} +
+
+ {{ t("driverStore.runtimeUptime") }}: + {{ formatRuntimeUptime(runtime.uptime_seconds) }} +
+
+ + + + {{ runtimeControlUnavailableReasonLabel(runtime.control_unavailable_reason) }} + +
+ +
+
{{
+                        runtime.last_error
+                      }}
+
+
+
+
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index bbf14d440f..dc2172eec6 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1849,6 +1849,35 @@ export default { updating: "Updating", update: "Update", localInstall: "Local install", + runtimeDrivers: "Runtime", + runtimeTitle: "Driver runtime", + runtimeHint: "Runtime metrics are collected only while this panel is open.", + runtimeRunning: "Running", + runtimeMemory: "Memory", + runtimeHealth: "Health", + runtimeHealth_healthy: "Healthy", + runtimeHealth_warning: "Warning", + runtimeHealth_error: "Error", + runtimeLastError: "Last error", + runtimeEmpty: "No driver runtimes yet. Open an agent-driven connection to start one.", + runtimeKindAgent: "Agent", + runtimeKindPlugin: "Plugin", + runtimeSourceDaemon: "Daemon", + runtimeSourceConnection: "Connection", + runtimeStatus_running: "Running", + runtimeStatus_stopped: "Stopped", + runtimeStatus_error: "Error", + runtimeStatus_unknown: "Unknown", + runtimePid: "PID", + runtimeUptime: "Uptime", + runtimeActions: "Actions", + runtimeStop: "Stop", + runtimeRestart: "Restart", + runtimeControlConnectionOwned: "Close the database connection to stop this runtime.", + runtimeStopSuccess: "{label} runtime stopped", + runtimeStopFailed: "Failed to stop {label}: {error}", + runtimeRestartSuccess: "{label} runtime restarted", + runtimeRestartFailed: "Failed to restart {label}: {error}", }, databaseExport: { title: "Export Database", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 163410592f..c93427fd9b 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1742,6 +1742,35 @@ export default { updating: "Actualizando", update: "Actualizar", localInstall: "Instalación local", + runtimeDrivers: "Runtime", + runtimeTitle: "Runtime de drivers", + runtimeHint: "Las métricas de runtime se recopilan solo mientras este panel está abierto.", + runtimeRunning: "En ejecución", + runtimeMemory: "Memoria", + runtimeHealth: "Salud", + runtimeHealth_healthy: "Correcto", + runtimeHealth_warning: "Advertencia", + runtimeHealth_error: "Error", + runtimeLastError: "Último error", + runtimeEmpty: "Aún no hay runtimes de drivers. Abre una conexión con driver Agent para iniciar uno.", + runtimeKindAgent: "Agent", + runtimeKindPlugin: "Plugin", + runtimeSourceDaemon: "Daemon", + runtimeSourceConnection: "Conexión", + runtimeStatus_running: "En ejecución", + runtimeStatus_stopped: "Detenido", + runtimeStatus_error: "Error", + runtimeStatus_unknown: "Desconocido", + runtimePid: "PID", + runtimeUptime: "Tiempo activo", + runtimeActions: "Acciones", + runtimeStop: "Detener", + runtimeRestart: "Reiniciar", + runtimeControlConnectionOwned: "Cierra la conexión de base de datos para detener este runtime.", + runtimeStopSuccess: "Runtime de {label} detenido", + runtimeStopFailed: "Error al detener {label}: {error}", + runtimeRestartSuccess: "Runtime de {label} reiniciado", + runtimeRestartFailed: "Error al reiniciar {label}: {error}", }, databaseExport: { title: "Exportar base de datos", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 2bace7a399..d64b291aa9 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1807,6 +1807,35 @@ export default { updating: "更新中", update: "更新", localInstall: "本地安装", + runtimeDrivers: "运行时", + runtimeTitle: "驱动运行时", + runtimeHint: "仅在打开此面板时采集运行时指标。", + runtimeRunning: "运行中", + runtimeMemory: "内存", + runtimeHealth: "健康状态", + runtimeHealth_healthy: "正常", + runtimeHealth_warning: "警告", + runtimeHealth_error: "错误", + runtimeLastError: "最近错误", + runtimeEmpty: "暂无驱动运行时。打开一个 Agent 驱动连接后会自动启动。", + runtimeKindAgent: "Agent", + runtimeKindPlugin: "插件", + runtimeSourceDaemon: "守护进程", + runtimeSourceConnection: "连接", + runtimeStatus_running: "运行中", + runtimeStatus_stopped: "已停止", + runtimeStatus_error: "错误", + runtimeStatus_unknown: "未知", + runtimePid: "PID", + runtimeUptime: "运行时长", + runtimeActions: "操作", + runtimeStop: "停止", + runtimeRestart: "重启", + runtimeControlConnectionOwned: "关闭数据库连接后即可停止此运行时。", + runtimeStopSuccess: "{label} 运行时已停止", + runtimeStopFailed: "停止 {label} 失败: {error}", + runtimeRestartSuccess: "{label} 运行时已重启", + runtimeRestartFailed: "重启 {label} 失败: {error}", }, databaseExport: { title: "导出数据库", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index b4feff6609..792b1d1b25 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1754,6 +1754,35 @@ export default { updating: "更新中", update: "更新", localInstall: "本機安裝", + runtimeDrivers: "執行環境", + runtimeTitle: "驅動程式執行環境", + runtimeHint: "僅在開啟此面板時收集執行環境指標。", + runtimeRunning: "執行中", + runtimeMemory: "記憶體", + runtimeHealth: "健康狀態", + runtimeHealth_healthy: "正常", + runtimeHealth_warning: "警告", + runtimeHealth_error: "錯誤", + runtimeLastError: "最近錯誤", + runtimeEmpty: "暫無驅動程式執行環境。開啟一個 Agent 驅動連線後會自動啟動。", + runtimeKindAgent: "Agent", + runtimeKindPlugin: "外掛程式", + runtimeSourceDaemon: "守護程序", + runtimeSourceConnection: "連線", + runtimeStatus_running: "執行中", + runtimeStatus_stopped: "已停止", + runtimeStatus_error: "錯誤", + runtimeStatus_unknown: "未知", + runtimePid: "PID", + runtimeUptime: "執行時長", + runtimeActions: "操作", + runtimeStop: "停止", + runtimeRestart: "重新啟動", + runtimeControlConnectionOwned: "關閉資料庫連線後即可停止此執行環境。", + runtimeStopSuccess: "{label} 執行環境已停止", + runtimeStopFailed: "停止 {label} 失敗: {error}", + runtimeRestartSuccess: "{label} 執行環境已重新啟動", + runtimeRestartFailed: "重新啟動 {label} 失敗: {error}", }, databaseExport: { title: "匯出資料庫", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 5709d21a7f..35a76b9134 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -49,6 +49,9 @@ export const uninstallJdbcPlugin = forward("uninstallJdbcPlugin"); export const listInstalledAgentsLocal = forward("listInstalledAgentsLocal"); export const listInstalledAgents = forward("listInstalledAgents"); export const getDriverStoreUsage = forward("getDriverStoreUsage"); +export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary"); +export const stopDriverRuntime = forward("stopDriverRuntime"); +export const restartDriverRuntime = forward("restartDriverRuntime"); export const installAgent = forward("installAgent"); export const upgradeAllAgents = forward("upgradeAllAgents"); export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers"); @@ -257,6 +260,10 @@ export type { AgentDriverInfo, DriverStoreUsage, DriverStoreUsageItem, + DriverRuntimeHealth, + DriverRuntimeStatus, + DriverRuntimeInfo, + DriverRuntimeSummary, JavaRuntimeMode, JavaRuntimeConfig, DriverInstallProgress, diff --git a/apps/desktop/src/lib/driverRuntimePresentation.ts b/apps/desktop/src/lib/driverRuntimePresentation.ts new file mode 100644 index 0000000000..16b8ce4558 --- /dev/null +++ b/apps/desktop/src/lib/driverRuntimePresentation.ts @@ -0,0 +1,59 @@ +import type { DriverRuntimeHealth, DriverRuntimeStatus } from "@/lib/api"; + +export function formatRuntimeBytes(bytes: number | null | undefined): string { + if (!bytes) return "-"; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +export function formatRuntimeCpu(value: number | null | undefined): string { + if (value === null || value === undefined || Number.isNaN(value)) return "-"; + return `${Math.max(0, value).toFixed(1)}%`; +} + +export function formatRuntimeUptime(seconds: number | null | undefined): string { + if (!seconds) return "-"; + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + const remMinutes = minutes % 60; + return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`; +} + +export function runtimeStatusClass(status: DriverRuntimeStatus): string { + switch (status) { + case "running": + return "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400"; + case "error": + return "bg-destructive/10 text-destructive"; + case "unknown": + return "bg-amber-500/10 text-amber-700 dark:text-amber-400"; + default: + return "bg-muted text-muted-foreground"; + } +} + +export function runtimeStatusDotClass(status: DriverRuntimeStatus): string { + switch (status) { + case "running": + return "bg-emerald-500"; + case "error": + return "bg-destructive"; + case "unknown": + return "bg-amber-500"; + default: + return "bg-muted-foreground/40"; + } +} + +export function runtimeHealthClass(health: DriverRuntimeHealth): string { + switch (health) { + case "error": + return "text-destructive"; + case "warning": + return "text-amber-700 dark:text-amber-400"; + default: + return "text-green-700 dark:text-green-400"; + } +} diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index d71341f774..5d00cc00aa 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -28,6 +28,7 @@ import type { AiConversation, AiModelInfo, DriverStoreUsage, + DriverRuntimeSummary, UpgradeAllAgentDriversResult, AgentUpdateBlocker, DesktopSettings, @@ -240,6 +241,18 @@ export async function getDriverStoreUsage(): Promise { return get("/api/agents/storage-usage"); } +export async function getDriverRuntimeSummary(): Promise { + return get("/api/agents/runtime"); +} + +export async function stopDriverRuntime(runtimeId: string): Promise { + await post("/api/agents/runtime/stop", { runtimeId }); +} + +export async function restartDriverRuntime(runtimeId: string): Promise { + await post("/api/agents/runtime/restart", { runtimeId }); +} + export async function installAgent(dbType: string): Promise { await post("/api/agents/install", { dbType }); } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 2c93ee777a..74477e532d 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -112,6 +112,35 @@ export interface DriverStoreUsage { agent_drivers: DriverStoreUsageItem[]; } +export type DriverRuntimeHealth = "healthy" | "warning" | "error"; +export type DriverRuntimeStatus = "running" | "stopped" | "error" | "unknown"; + +export interface DriverRuntimeInfo { + id: string; + driver_key: string; + label: string; + kind: string; + source: string; + status: DriverRuntimeStatus; + pid: number | null; + memory_bytes: number | null; + cpu_percent: number | null; + uptime_seconds: number | null; + version: string | null; + last_error: string | null; + can_stop: boolean; + can_restart: boolean; + control_unavailable_reason: string | null; +} + +export interface DriverRuntimeSummary { + running_count: number; + total_memory_bytes: number; + last_error: string | null; + health: DriverRuntimeHealth; + runtimes: DriverRuntimeInfo[]; +} + export interface DesktopSettings { show_tray_icon: boolean; icon_theme: "default" | "black"; @@ -860,6 +889,18 @@ export async function getDriverStoreUsage(): Promise { return invoke("get_driver_store_usage"); } +export async function getDriverRuntimeSummary(): Promise { + return invoke("get_driver_runtime_summary"); +} + +export async function stopDriverRuntime(runtimeId: string): Promise { + return invoke("stop_driver_runtime", { runtimeId }); +} + +export async function restartDriverRuntime(runtimeId: string): Promise { + return invoke("restart_driver_runtime", { runtimeId }); +} + export async function installAgent(dbType: string): Promise { return invoke("install_agent", { dbType }); } diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 535aa406a7..8bf803ebc0 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -49,3 +49,4 @@ async-trait = "0.1" bytes = "1" font-kit = "0.14.3" zip = { version = "4", default-features = false, features = ["deflate"] } +sysinfo = { version = "0.32", features = ["system"] } diff --git a/crates/dbx-core/src/agent_manager.rs b/crates/dbx-core/src/agent_manager.rs index 438c800e29..79ee1947e4 100644 --- a/crates/dbx-core/src/agent_manager.rs +++ b/crates/dbx-core/src/agent_manager.rs @@ -458,6 +458,10 @@ impl AgentManager { crate::agent_runtime::stop_daemon_by_key(self, agent_key).await; } + pub async fn restart_daemon_by_key(&self, agent_key: &str) -> Result<(), String> { + crate::agent_runtime::restart_daemon_by_key(self, agent_key).await + } + pub async fn active_daemon_keys(&self) -> Vec { self.daemons.lock().await.keys().cloned().collect() } diff --git a/crates/dbx-core/src/agent_runtime.rs b/crates/dbx-core/src/agent_runtime.rs index d55677d372..1b63a65d00 100644 --- a/crates/dbx-core/src/agent_runtime.rs +++ b/crates/dbx-core/src/agent_runtime.rs @@ -21,6 +21,13 @@ pub async fn stop_daemon_by_key(manager: &AgentManager, agent_key: &str) { manager.daemons.lock().await.remove(agent_key); } +pub async fn restart_daemon_by_key(manager: &AgentManager, agent_key: &str) -> Result<(), String> { + manager.daemons.lock().await.remove(agent_key); + let client = spawn_client_for_key(manager, agent_key).await?; + manager.daemons.lock().await.insert(agent_key.to_string(), client); + Ok(()) +} + pub async fn spawn_connection_client( manager: &AgentManager, db_type: &DatabaseType, diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 5272eadcc8..1b3e3339e7 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -880,7 +880,7 @@ fn session_scoped_pool_key(base_pool_key: String, client_session_id: Option<&str .unwrap_or(base_pool_key) } -fn config_for_pool_key<'a>( +pub(crate) fn config_for_pool_key<'a>( pool_key: &str, configs: &'a HashMap, ) -> Option<&'a ConnectionConfig> { diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index 88fc29fd2f..92d1d348d1 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -640,6 +640,14 @@ impl AgentDriverClient { // Reap the child to avoid zombie processes let _ = self.child.wait(); } + + pub fn pid(&self) -> u32 { + self.child.id() + } + + pub fn stderr_tail_snapshot(&self) -> String { + self.stderr_tail.lock().map(|tail| tail.snapshot()).unwrap_or_default() + } } pub fn agent_handshake_params(app_version: &str) -> Value { diff --git a/crates/dbx-core/src/driver_runtime.rs b/crates/dbx-core/src/driver_runtime.rs new file mode 100644 index 0000000000..58a2bc5f48 --- /dev/null +++ b/crates/dbx-core/src/driver_runtime.rs @@ -0,0 +1,309 @@ +use std::collections::{HashMap, HashSet}; + +use serde::{Deserialize, Serialize}; +use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; + +use crate::agent_catalog; +use crate::connection::{config_for_pool_key, AppState, PoolKind}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DriverRuntimeInfo { + pub id: String, + pub driver_key: String, + pub label: String, + pub kind: String, + pub source: String, + pub status: String, + pub pid: Option, + pub memory_bytes: Option, + pub cpu_percent: Option, + pub uptime_seconds: Option, + pub version: Option, + pub last_error: Option, + pub can_stop: bool, + pub can_restart: bool, + pub control_unavailable_reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct DriverRuntimeSummary { + pub running_count: usize, + pub total_memory_bytes: u64, + pub last_error: Option, + pub health: String, + pub runtimes: Vec, +} + +#[derive(Debug, Clone)] +struct RuntimeSeed { + id: String, + driver_key: String, + label: String, + kind: String, + source: String, + status: String, + pid: Option, + version: Option, + last_error: Option, + can_stop: bool, + can_restart: bool, + control_unavailable_reason: Option, +} + +#[derive(Debug, Clone, Default)] +struct ProcessStats { + memory_bytes: u64, + cpu_percent: f32, + uptime_seconds: u64, +} + +pub async fn collect_driver_runtime_summary(state: &AppState) -> DriverRuntimeSummary { + let seeds = collect_runtime_seeds(state).await; + let pids = seeds.iter().filter_map(|seed| seed.pid).collect::>(); + let stats = collect_process_stats(pids).await; + build_summary(seeds, &stats) +} + +pub async fn stop_driver_runtime(state: &AppState, runtime_id: &str) -> Result<(), String> { + let agent_key = + runtime_id.strip_prefix("agent:").ok_or_else(|| "Only agent daemon runtimes can be stopped".to_string())?; + state.agent_manager.stop_daemon_by_key(agent_key).await; + Ok(()) +} + +pub async fn restart_driver_runtime(state: &AppState, runtime_id: &str) -> Result<(), String> { + let agent_key = + runtime_id.strip_prefix("agent:").ok_or_else(|| "Only agent daemon runtimes can be restarted".to_string())?; + state.agent_manager.restart_daemon_by_key(agent_key).await +} + +async fn collect_runtime_seeds(state: &AppState) -> Vec { + let agent_state = state.agent_manager.load_state(); + let mut seeds = HashMap::::new(); + + for (key, installed) in &agent_state.installed_drivers { + seeds.insert( + format!("agent:{key}"), + RuntimeSeed { + id: format!("agent:{key}"), + driver_key: key.clone(), + label: agent_catalog::label_for_key(key).unwrap_or(key).to_string(), + kind: "agent".to_string(), + source: "daemon".to_string(), + status: "stopped".to_string(), + pid: None, + version: Some(installed.version.clone()), + last_error: None, + can_stop: false, + can_restart: state.agent_manager.is_driver_installed(key), + control_unavailable_reason: None, + }, + ); + } + + { + let daemons = state.agent_manager.daemons.lock().await; + for (key, client) in daemons.iter() { + let last_error = non_empty(client.stderr_tail_snapshot()); + seeds.insert( + format!("agent:{key}"), + RuntimeSeed { + id: format!("agent:{key}"), + driver_key: key.clone(), + label: agent_catalog::label_for_key(key).unwrap_or(key).to_string(), + kind: "agent".to_string(), + source: "daemon".to_string(), + status: "running".to_string(), + pid: Some(client.pid()), + version: agent_state.installed_drivers.get(key).map(|driver| driver.version.clone()), + last_error, + can_stop: true, + can_restart: true, + control_unavailable_reason: None, + }, + ); + } + } + + let configs = state.configs.read().await; + let connections = state.connections.read().await; + for (pool_key, pool) in connections.iter() { + match pool { + PoolKind::Agent(client) => { + let Some(config) = config_for_pool_key(pool_key, &configs) else { + continue; + }; + let key = crate::agent_manager::AgentManager::db_type_to_agent_key( + &config.db_type, + config.driver_profile.as_deref(), + ) + .unwrap_or("agent"); + let client = client.lock().await; + let last_error = non_empty(client.stderr_tail_snapshot()); + seeds.insert( + format!("agent-connection:{pool_key}"), + RuntimeSeed { + id: format!("agent-connection:{pool_key}"), + driver_key: key.to_string(), + label: agent_catalog::label_for_key(key).unwrap_or(key).to_string(), + kind: "agent".to_string(), + source: "connection".to_string(), + status: "running".to_string(), + pid: Some(client.pid()), + version: agent_state.installed_drivers.get(key).map(|driver| driver.version.clone()), + last_error, + can_stop: false, + can_restart: false, + control_unavailable_reason: Some("connection-owned".to_string()), + }, + ); + } + PoolKind::ExternalDriver { driver_id, session, .. } => { + seeds.insert( + format!("plugin-connection:{pool_key}"), + RuntimeSeed { + id: format!("plugin-connection:{pool_key}"), + driver_key: driver_id.clone(), + label: driver_id.clone(), + kind: "plugin".to_string(), + source: "connection".to_string(), + status: "running".to_string(), + pid: session.pid().await, + version: None, + last_error: None, + can_stop: false, + can_restart: false, + control_unavailable_reason: Some("connection-owned".to_string()), + }, + ); + } + _ => {} + } + } + + let mut output = seeds.into_values().collect::>(); + output.sort_by(|left, right| { + left.kind + .cmp(&right.kind) + .then(left.label.cmp(&right.label)) + .then(left.source.cmp(&right.source)) + .then(left.id.cmp(&right.id)) + }); + output +} + +async fn collect_process_stats(pids: HashSet) -> HashMap { + if pids.is_empty() { + return HashMap::new(); + } + + tokio::task::spawn_blocking(move || { + let mut system = System::new_with_specifics( + RefreshKind::new().with_processes(ProcessRefreshKind::new().with_cpu().with_memory().with_disk_usage()), + ); + system.refresh_processes_specifics( + ProcessesToUpdate::All, + true, + ProcessRefreshKind::new().with_cpu().with_memory().with_disk_usage(), + ); + + let mut stats = HashMap::new(); + for pid in pids { + let Some(process) = system.process(Pid::from(pid as usize)) else { + continue; + }; + stats.insert( + pid, + ProcessStats { + memory_bytes: process.memory(), + cpu_percent: process.cpu_usage(), + uptime_seconds: process.run_time(), + }, + ); + } + stats + }) + .await + .unwrap_or_default() +} + +fn build_summary(seeds: Vec, stats: &HashMap) -> DriverRuntimeSummary { + let runtimes = seeds + .into_iter() + .map(|seed| { + let stat = seed.pid.and_then(|pid| stats.get(&pid)); + DriverRuntimeInfo { + id: seed.id, + driver_key: seed.driver_key, + label: seed.label, + kind: seed.kind, + source: seed.source, + status: seed.status, + pid: seed.pid, + memory_bytes: stat.map(|value| value.memory_bytes), + cpu_percent: stat.map(|value| value.cpu_percent), + uptime_seconds: stat.map(|value| value.uptime_seconds), + version: seed.version, + last_error: seed.last_error, + can_stop: seed.can_stop, + can_restart: seed.can_restart, + control_unavailable_reason: seed.control_unavailable_reason, + } + }) + .collect::>(); + + let running_count = runtimes.iter().filter(|runtime| runtime.status == "running").count(); + let total_memory_bytes = runtimes.iter().filter_map(|runtime| runtime.memory_bytes).sum(); + let last_error = runtimes.iter().rev().find_map(|runtime| runtime.last_error.clone()); + let health = if runtimes.iter().any(|runtime| runtime.status == "error") { + "error" + } else if last_error.is_some() { + "warning" + } else { + "healthy" + } + .to_string(); + + DriverRuntimeSummary { running_count, total_memory_bytes, last_error, health, runtimes } +} + +fn non_empty(value: String) -> Option { + let trimmed = value.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) +} + +#[cfg(test)] +mod tests { + use super::{build_summary, ProcessStats, RuntimeSeed}; + use std::collections::HashMap; + + fn seed(id: &str, status: &str, pid: Option) -> RuntimeSeed { + RuntimeSeed { + id: id.to_string(), + driver_key: id.to_string(), + label: id.to_string(), + kind: "agent".to_string(), + source: "daemon".to_string(), + status: status.to_string(), + pid, + version: None, + last_error: None, + can_stop: true, + can_restart: true, + control_unavailable_reason: None, + } + } + + #[test] + fn summary_counts_running_runtimes_and_memory() { + let mut stats = HashMap::new(); + stats.insert(42, ProcessStats { memory_bytes: 1024, cpu_percent: 0.5, uptime_seconds: 7 }); + + let summary = build_summary(vec![seed("oracle", "running", Some(42)), seed("h2", "stopped", None)], &stats); + + assert_eq!(summary.running_count, 1); + assert_eq!(summary.total_memory_bytes, 1024); + assert_eq!(summary.health, "healthy"); + assert_eq!(summary.runtimes[0].memory_bytes, Some(1024)); + } +} diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index dde11ab0e1..468579dd65 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -15,6 +15,7 @@ pub mod database_export; pub mod database_search_sql; pub mod db; pub mod db_admin_sql; +pub mod driver_runtime; pub mod external; pub mod history; pub mod jdbc; diff --git a/crates/dbx-core/src/plugins.rs b/crates/dbx-core/src/plugins.rs index 8af6cc6d15..ecc80fb1f3 100644 --- a/crates/dbx-core/src/plugins.rs +++ b/crates/dbx-core/src/plugins.rs @@ -338,6 +338,11 @@ impl PluginDriverSession { let mut process = self.process.lock().await; let _ = process.child.kill().await; } + + pub async fn pid(&self) -> Option { + let process = self.process.lock().await; + process.child.id() + } } async fn invoke_plugin( diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 44ae802088..94699e802f 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -112,6 +112,9 @@ async fn main() { .route("/agents/installed-local", get(routes::agents::list_installed_agents_local)) .route("/agents/installed", get(routes::agents::list_installed_agents)) .route("/agents/storage-usage", get(routes::agents::get_driver_store_usage)) + .route("/agents/runtime", get(routes::agents::get_driver_runtime_summary)) + .route("/agents/runtime/stop", post(routes::agents::stop_driver_runtime)) + .route("/agents/runtime/restart", post(routes::agents::restart_driver_runtime)) .route("/agents/install", post(routes::agents::install_agent)) .route("/agents/upgrade-all", post(routes::agents::upgrade_all_agents)) .route("/agents/uninstall", post(routes::agents::uninstall_agent)) diff --git a/crates/dbx-web/src/routes/agents.rs b/crates/dbx-web/src/routes/agents.rs index e58ab6a963..606f27c5c8 100644 --- a/crates/dbx-web/src/routes/agents.rs +++ b/crates/dbx-web/src/routes/agents.rs @@ -11,6 +11,7 @@ use dbx_core::agent_service::{ invalidate_registry_cache, reinstall_agent_jre, uninstall_agent_driver, uninstall_agent_jre, upgrade_all_agent_drivers, AgentProgressEvent, }; +use dbx_core::driver_runtime::DriverRuntimeSummary; use futures::Stream; use serde::Deserialize; use tokio::io::AsyncWriteExt; @@ -52,6 +53,34 @@ pub async fn get_driver_store_usage(State(state): State>) -> Resul Ok(Json(state.app.agent_manager.collect_driver_store_usage(state.app.plugins.root_dir()))) } +pub async fn get_driver_runtime_summary( + State(state): State>, +) -> Result, AppError> { + Ok(Json(dbx_core::driver_runtime::collect_driver_runtime_summary(&state.app).await)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DriverRuntimeRequest { + pub runtime_id: String, +} + +pub async fn stop_driver_runtime( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + dbx_core::driver_runtime::stop_driver_runtime(&state.app, &req.runtime_id).await.map_err(AppError)?; + Ok(Json(serde_json::json!({ "ok": true }))) +} + +pub async fn restart_driver_runtime( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + dbx_core::driver_runtime::restart_driver_runtime(&state.app, &req.runtime_id).await.map_err(AppError)?; + Ok(Json(serde_json::json!({ "ok": true }))) +} + pub async fn install_agent( State(state): State>, Json(req): Json, diff --git a/packages/app-tests/driverRuntimePresentation.test.ts b/packages/app-tests/driverRuntimePresentation.test.ts new file mode 100644 index 0000000000..412373a44a --- /dev/null +++ b/packages/app-tests/driverRuntimePresentation.test.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + formatRuntimeBytes, + formatRuntimeCpu, + formatRuntimeUptime, + runtimeHealthClass, + runtimeStatusDotClass, + runtimeStatusClass, +} from "../../apps/desktop/src/lib/driverRuntimePresentation.ts"; + +test("formats runtime byte counts compactly", () => { + assert.equal(formatRuntimeBytes(null), "-"); + assert.equal(formatRuntimeBytes(12_288), "12 KB"); + assert.equal(formatRuntimeBytes(2_621_440), "2.5 MB"); +}); + +test("formats runtime CPU and uptime", () => { + assert.equal(formatRuntimeCpu(undefined), "-"); + assert.equal(formatRuntimeCpu(1.234), "1.2%"); + assert.equal(formatRuntimeUptime(45), "45s"); + assert.equal(formatRuntimeUptime(125), "2m"); + assert.equal(formatRuntimeUptime(7_500), "2h 5m"); +}); + +test("maps runtime state to visual classes", () => { + assert.match(runtimeStatusClass("running"), /emerald/); + assert.match(runtimeStatusDotClass("running"), /emerald/); + assert.match(runtimeStatusClass("stopped"), /muted/); + assert.match(runtimeHealthClass("warning"), /amber/); +}); diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 34a69c7cde..dc7b94fc49 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -9,6 +9,7 @@ use dbx_core::agent_service::{ upgrade_all_agent_drivers, AgentProgressEvent, UpgradeAllAgentDriversResult, }; use dbx_core::connection::AppState; +use dbx_core::driver_runtime::DriverRuntimeSummary; #[derive(Debug, Clone, serde::Serialize)] pub struct AgentUpdateBlocker { @@ -32,6 +33,21 @@ pub async fn get_driver_store_usage(state: State<'_, Arc>) -> Result>) -> Result { + Ok(dbx_core::driver_runtime::collect_driver_runtime_summary(state.inner().as_ref()).await) +} + +#[tauri::command] +pub async fn stop_driver_runtime(state: State<'_, Arc>, runtime_id: String) -> Result<(), String> { + dbx_core::driver_runtime::stop_driver_runtime(state.inner().as_ref(), &runtime_id).await +} + +#[tauri::command] +pub async fn restart_driver_runtime(state: State<'_, Arc>, runtime_id: String) -> Result<(), String> { + dbx_core::driver_runtime::restart_driver_runtime(state.inner().as_ref(), &runtime_id).await +} + #[tauri::command] pub async fn install_agent( app: tauri::AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 208ae82bef..edf1fa0505 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -510,6 +510,9 @@ pub fn run() { commands::agents::list_installed_agents, commands::agents::list_installed_agents_local, commands::agents::get_driver_store_usage, + commands::agents::get_driver_runtime_summary, + commands::agents::stop_driver_runtime, + commands::agents::restart_driver_runtime, commands::agents::install_agent, commands::agents::upgrade_all_agents, commands::agents::check_agent_update_blockers,