feat: add driver runtime manager

This commit is contained in:
t8y2
2026-06-04 23:20:15 +08:00
parent a8c08cd7e9
commit a799958328
23 changed files with 1044 additions and 12 deletions
Generated
+83 -6
View File
@@ -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"
@@ -1,7 +1,21 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { ref, onMounted, onUnmounted, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "@lucide/vue";
import {
Activity,
Cpu,
FolderOpen,
MemoryStick,
Square,
Trash2,
Download,
RotateCcw,
Loader2,
RefreshCw,
Check,
Clock3,
FileUp,
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -14,7 +28,21 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
import { countAvailableDriverUpdates } from "@/lib/agentDriverUpdateBadge";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
import type { AgentDriverInfo, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/api";
import type {
AgentDriverInfo,
DriverRuntimeInfo,
DriverRuntimeSummary,
DriverStoreUsage,
JavaRuntimeConfig,
} from "@/lib/api";
import {
formatRuntimeBytes,
formatRuntimeCpu,
formatRuntimeUptime,
runtimeHealthClass,
runtimeStatusClass,
runtimeStatusDotClass,
} from "@/lib/driverRuntimePresentation";
import {
addDriverInstallQueue,
driverInstallProgressPercent,
@@ -32,6 +60,8 @@ const emit = defineEmits<{
"update-count-change": [count: number];
}>();
const driverStoreTab = ref("agent");
// ──────────── Agent drivers ────────────
const drivers = ref<AgentDriverInfo[]>([]);
@@ -48,6 +78,12 @@ const javaRuntimeConfig = ref<JavaRuntimeConfig>({ mode: "managed", custom_java_
const customJavaPath = ref("");
const savingJavaRuntime = ref(false);
const driverStoreUsage = ref<DriverStoreUsage | null>(null);
const runtimeSummary = ref<DriverRuntimeSummary | null>(null);
const runtimeLoading = ref(false);
const runtimeError = ref("");
const runtimeBusy = ref<string | null>(null);
let runtimeTimer: ReturnType<typeof setInterval> | null = null;
const DRIVER_RUNTIME_POLL_MS = 5000;
let unlisten: (() => void) | null = null;
@@ -427,6 +463,109 @@ function formatBytes(bytes: number) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
const runtimeOverview = computed(() => {
const summary = runtimeSummary.value;
return [
{
key: "running",
label: t("driverStore.runtimeRunning"),
value: String(summary?.running_count ?? 0),
},
{
key: "memory",
label: t("driverStore.runtimeMemory"),
value: formatRuntimeBytes(summary?.total_memory_bytes),
},
{
key: "health",
label: t("driverStore.runtimeHealth"),
value: t(`driverStore.runtimeHealth_${summary?.health ?? "healthy"}`),
class: runtimeHealthClass(summary?.health ?? "healthy"),
},
];
});
function runtimeKindLabel(runtime: DriverRuntimeInfo) {
return runtime.kind === "plugin" ? t("driverStore.runtimeKindPlugin") : t("driverStore.runtimeKindAgent");
}
function runtimeSourceLabel(runtime: DriverRuntimeInfo) {
return runtime.source === "connection"
? t("driverStore.runtimeSourceConnection")
: t("driverStore.runtimeSourceDaemon");
}
function runtimeStatusLabel(status: DriverRuntimeInfo["status"]) {
return t(`driverStore.runtimeStatus_${status}`);
}
function runtimeControlUnavailableReasonLabel(reason: string | null) {
if (reason === "connection-owned") return t("driverStore.runtimeControlConnectionOwned");
return reason || "-";
}
async function loadDriverRuntimeSummary(showLoading = false) {
if (showLoading) runtimeLoading.value = true;
try {
runtimeSummary.value = await api.getDriverRuntimeSummary();
runtimeError.value = "";
} catch (e: any) {
runtimeError.value = String(e?.message || e);
} finally {
runtimeLoading.value = false;
}
}
function startDriverRuntimePolling() {
if (runtimeTimer) return;
void loadDriverRuntimeSummary(true);
runtimeTimer = setInterval(() => {
if (driverStoreTab.value !== "runtime") {
stopDriverRuntimePolling();
return;
}
void loadDriverRuntimeSummary(false);
}, DRIVER_RUNTIME_POLL_MS);
}
function stopDriverRuntimePolling() {
if (runtimeTimer) {
clearInterval(runtimeTimer);
runtimeTimer = null;
}
}
async function refreshDriverRuntime() {
if (driverStoreTab.value !== "runtime") return;
await loadDriverRuntimeSummary(true);
}
async function stopRuntime(runtime: DriverRuntimeInfo) {
runtimeBusy.value = runtime.id;
try {
await api.stopDriverRuntime(runtime.id);
await loadDriverRuntimeSummary(false);
toast(t("driverStore.runtimeStopSuccess", { label: runtime.label }));
} catch (e: any) {
toast(t("driverStore.runtimeStopFailed", { label: runtime.label, error: e }));
} finally {
runtimeBusy.value = null;
}
}
async function restartRuntime(runtime: DriverRuntimeInfo) {
runtimeBusy.value = runtime.id;
try {
await api.restartDriverRuntime(runtime.id);
await loadDriverRuntimeSummary(false);
toast(t("driverStore.runtimeRestartSuccess", { label: runtime.label }));
} catch (e: any) {
toast(t("driverStore.runtimeRestartFailed", { label: runtime.label, error: e }));
} finally {
runtimeBusy.value = null;
}
}
function jreUsageLabel(key: string) {
const bytes = jreUsageByKey.value.get(String(key)) || 0;
return bytes > 0 ? formatBytes(bytes) : "";
@@ -605,6 +744,15 @@ onMounted(async () => {
onUnmounted(() => {
unlisten?.();
stopDriverRuntimePolling();
});
watch(driverStoreTab, (tab) => {
if (tab === "runtime") {
startDriverRuntimePolling();
} else {
stopDriverRuntimePolling();
}
});
</script>
@@ -612,7 +760,7 @@ onUnmounted(() => {
<div class="h-full flex flex-col">
<div class="flex-1 min-h-0 overflow-y-auto">
<div class="max-w-4xl mx-auto px-6 py-6">
<Tabs default-value="agent">
<Tabs v-model="driverStoreTab" default-value="agent">
<div class="mb-5 rounded-xl border bg-muted/20 p-4">
<div class="flex items-center justify-between gap-3">
<div class="text-sm font-medium">{{ t("driverStore.usageTitle") }}</div>
@@ -646,8 +794,11 @@ onUnmounted(() => {
{{ t("driverStore.jdbcDrivers") }}
<span v-if="jdbcTabUpdateCount > 0" class="inline-block h-2 w-2 rounded-full bg-red-500" />
</TabsTrigger>
<TabsTrigger value="runtime" class="gap-1.5 relative">
{{ t("driverStore.runtimeDrivers") }}
</TabsTrigger>
</TabsList>
<div class="flex items-center gap-2">
<div v-if="driverStoreTab !== 'runtime'" class="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
@@ -1056,6 +1207,156 @@ onUnmounted(() => {
</div>
</div>
</TabsContent>
<!-- Runtime Tab -->
<TabsContent value="runtime" class="mt-5">
<div class="overflow-hidden rounded-md border bg-background">
<div class="flex flex-col gap-3 border-b px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex min-w-0 items-center gap-2.5">
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted">
<Activity class="h-4 w-4 text-muted-foreground" />
</span>
<div class="min-w-0">
<div class="text-sm font-medium">{{ t("driverStore.runtimeTitle") }}</div>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span v-for="item in runtimeOverview" :key="item.key" class="inline-flex items-center gap-1.5">
<span>{{ item.label }}</span>
<span class="font-medium text-foreground" :class="item.class">{{ item.value }}</span>
</span>
</div>
</div>
</div>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 shrink-0 rounded-full text-muted-foreground"
:title="t('driverStore.refresh')"
:disabled="runtimeLoading"
@click="refreshDriverRuntime"
>
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': runtimeLoading }" />
</Button>
</div>
<div v-if="runtimeSummary?.last_error" class="border-b border-amber-500/20 bg-amber-500/10 px-4 py-2.5">
<div class="text-xs font-medium text-amber-700 dark:text-amber-300">
{{ t("driverStore.runtimeLastError") }}
</div>
<pre class="mt-1 max-h-20 overflow-auto whitespace-pre-wrap text-[11px] text-muted-foreground">{{
runtimeSummary.last_error
}}</pre>
</div>
<div v-if="runtimeLoading && !runtimeSummary" class="p-6 text-center text-sm text-muted-foreground">
{{ t("common.loading") }}
</div>
<div v-else-if="runtimeError" class="p-6 text-sm text-destructive">
{{ runtimeError }}
</div>
<div v-else-if="!runtimeSummary?.runtimes.length" class="p-6 text-center text-sm text-muted-foreground">
{{ t("driverStore.runtimeEmpty") }}
</div>
<div v-else>
<div
class="hidden grid-cols-[minmax(0,1.6fr)_72px_56px_76px_58px_76px_72px] gap-2 border-b bg-muted/30 px-4 py-2 text-[11px] font-medium text-muted-foreground lg:grid"
>
<div>{{ t("driverStore.runtimeDrivers") }}</div>
<div>{{ t("driverStore.runtimeHealth") }}</div>
<div>{{ t("driverStore.runtimePid") }}</div>
<div>{{ t("driverStore.runtimeMemory") }}</div>
<div>CPU</div>
<div>{{ t("driverStore.runtimeUptime") }}</div>
<div class="text-right">{{ t("driverStore.runtimeActions") }}</div>
</div>
<div class="divide-y">
<div
v-for="runtime in runtimeSummary.runtimes"
:key="runtime.id"
class="grid gap-2 px-4 py-3 transition hover:bg-muted/25 lg:grid-cols-[minmax(0,1.6fr)_72px_56px_76px_58px_76px_72px] lg:items-center"
>
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-2">
<span class="h-2 w-2 shrink-0 rounded-full" :class="runtimeStatusDotClass(runtime.status)" />
<span class="truncate text-sm font-medium">{{ runtime.label }}</span>
<span
v-if="runtime.version"
class="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground"
>
v{{ runtime.version }}
</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
<span>{{ runtimeKindLabel(runtime) }}</span>
<span class="text-muted-foreground/50">/</span>
<span>{{ runtimeSourceLabel(runtime) }}</span>
</div>
</div>
<div class="flex items-center gap-2 lg:block">
<span class="lg:hidden text-[11px] text-muted-foreground">{{
t("driverStore.runtimeHealth")
}}</span>
<span class="rounded-full px-2 py-0.5 text-[11px]" :class="runtimeStatusClass(runtime.status)">
{{ runtimeStatusLabel(runtime.status) }}
</span>
</div>
<div class="text-xs text-muted-foreground">
<span class="lg:hidden">{{ t("driverStore.runtimePid") }}: </span>{{ runtime.pid ?? "-" }}
</div>
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<MemoryStick class="h-3.5 w-3.5 lg:hidden" />
{{ formatRuntimeBytes(runtime.memory_bytes) }}
</div>
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<Cpu class="h-3.5 w-3.5 lg:hidden" />
{{ formatRuntimeCpu(runtime.cpu_percent) }}
</div>
<div class="text-xs text-muted-foreground">
<span class="lg:hidden">{{ t("driverStore.runtimeUptime") }}: </span>
{{ formatRuntimeUptime(runtime.uptime_seconds) }}
</div>
<div class="flex min-w-0 items-center gap-1.5 lg:justify-end">
<Button
v-if="runtime.can_stop"
variant="ghost"
size="icon"
class="h-7 w-7 rounded-full text-muted-foreground hover:text-destructive"
:title="t('driverStore.runtimeStop')"
:disabled="runtimeBusy === runtime.id"
@click="stopRuntime(runtime)"
>
<Square class="h-3.5 w-3.5" />
</Button>
<Button
v-if="runtime.can_restart"
variant="ghost"
size="icon"
class="h-7 w-7 rounded-full text-muted-foreground"
:title="t('driverStore.runtimeRestart')"
:disabled="runtimeBusy === runtime.id"
@click="restartRuntime(runtime)"
>
<RotateCcw class="h-3.5 w-3.5" :class="{ 'animate-spin': runtimeBusy === runtime.id }" />
</Button>
<span
v-if="!runtime.can_stop && !runtime.can_restart"
class="min-w-0 truncate text-[11px] text-muted-foreground lg:text-right"
:title="runtimeControlUnavailableReasonLabel(runtime.control_unavailable_reason)"
>
{{ runtimeControlUnavailableReasonLabel(runtime.control_unavailable_reason) }}
</span>
</div>
<div v-if="runtime.last_error" class="rounded-md bg-muted/60 p-2 lg:col-span-7">
<pre class="max-h-16 overflow-auto whitespace-pre-wrap text-[11px] text-muted-foreground">{{
runtime.last_error
}}</pre>
</div>
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
+29
View File
@@ -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",
+29
View File
@@ -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",
+29
View File
@@ -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: "导出数据库",
+29
View File
@@ -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: "匯出資料庫",
+7
View File
@@ -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,
@@ -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";
}
}
+13
View File
@@ -28,6 +28,7 @@ import type {
AiConversation,
AiModelInfo,
DriverStoreUsage,
DriverRuntimeSummary,
UpgradeAllAgentDriversResult,
AgentUpdateBlocker,
DesktopSettings,
@@ -240,6 +241,18 @@ export async function getDriverStoreUsage(): Promise<DriverStoreUsage> {
return get("/api/agents/storage-usage");
}
export async function getDriverRuntimeSummary(): Promise<DriverRuntimeSummary> {
return get("/api/agents/runtime");
}
export async function stopDriverRuntime(runtimeId: string): Promise<void> {
await post("/api/agents/runtime/stop", { runtimeId });
}
export async function restartDriverRuntime(runtimeId: string): Promise<void> {
await post("/api/agents/runtime/restart", { runtimeId });
}
export async function installAgent(dbType: string): Promise<void> {
await post("/api/agents/install", { dbType });
}
+41
View File
@@ -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<DriverStoreUsage> {
return invoke("get_driver_store_usage");
}
export async function getDriverRuntimeSummary(): Promise<DriverRuntimeSummary> {
return invoke("get_driver_runtime_summary");
}
export async function stopDriverRuntime(runtimeId: string): Promise<void> {
return invoke("stop_driver_runtime", { runtimeId });
}
export async function restartDriverRuntime(runtimeId: string): Promise<void> {
return invoke("restart_driver_runtime", { runtimeId });
}
export async function installAgent(dbType: string): Promise<void> {
return invoke("install_agent", { dbType });
}
+1
View File
@@ -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"] }
+4
View File
@@ -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<String> {
self.daemons.lock().await.keys().cloned().collect()
}
+7
View File
@@ -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,
+1 -1
View File
@@ -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<String, ConnectionConfig>,
) -> Option<&'a ConnectionConfig> {
+8
View File
@@ -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 {
+309
View File
@@ -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<u32>,
pub memory_bytes: Option<u64>,
pub cpu_percent: Option<f32>,
pub uptime_seconds: Option<u64>,
pub version: Option<String>,
pub last_error: Option<String>,
pub can_stop: bool,
pub can_restart: bool,
pub control_unavailable_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct DriverRuntimeSummary {
pub running_count: usize,
pub total_memory_bytes: u64,
pub last_error: Option<String>,
pub health: String,
pub runtimes: Vec<DriverRuntimeInfo>,
}
#[derive(Debug, Clone)]
struct RuntimeSeed {
id: String,
driver_key: String,
label: String,
kind: String,
source: String,
status: String,
pid: Option<u32>,
version: Option<String>,
last_error: Option<String>,
can_stop: bool,
can_restart: bool,
control_unavailable_reason: Option<String>,
}
#[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::<HashSet<_>>();
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<RuntimeSeed> {
let agent_state = state.agent_manager.load_state();
let mut seeds = HashMap::<String, RuntimeSeed>::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::<Vec<_>>();
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<u32>) -> HashMap<u32, ProcessStats> {
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<RuntimeSeed>, stats: &HashMap<u32, ProcessStats>) -> 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::<Vec<_>>();
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<String> {
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<u32>) -> 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));
}
}
+1
View File
@@ -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;
+5
View File
@@ -338,6 +338,11 @@ impl PluginDriverSession {
let mut process = self.process.lock().await;
let _ = process.child.kill().await;
}
pub async fn pid(&self) -> Option<u32> {
let process = self.process.lock().await;
process.child.id()
}
}
async fn invoke_plugin<T>(
+3
View File
@@ -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))
+29
View File
@@ -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<Arc<WebState>>) -> 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<Arc<WebState>>,
) -> Result<Json<DriverRuntimeSummary>, 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<Arc<WebState>>,
Json(req): Json<DriverRuntimeRequest>,
) -> Result<Json<serde_json::Value>, 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<Arc<WebState>>,
Json(req): Json<DriverRuntimeRequest>,
) -> Result<Json<serde_json::Value>, 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<Arc<WebState>>,
Json(req): Json<AgentTypeRequest>,
@@ -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/);
});
+16
View File
@@ -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<AppState>>) -> Result<D
Ok(state.agent_manager.collect_driver_store_usage(state.plugins.root_dir()))
}
#[tauri::command]
pub async fn get_driver_runtime_summary(state: State<'_, Arc<AppState>>) -> Result<DriverRuntimeSummary, String> {
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<AppState>>, 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<AppState>>, 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,
+3
View File
@@ -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,