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") }}
+
+
+
+
{{ t("driverStore.runtimeDrivers") }}
+
{{ t("driverStore.runtimeHealth") }}
+
{{ t("driverStore.runtimePid") }}
+
{{ t("driverStore.runtimeMemory") }}
+
CPU
+
{{ t("driverStore.runtimeUptime") }}
+
{{ t("driverStore.runtimeActions") }}
+
+
+
+
+
+
+ {{ 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,