mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
feat: add driver runtime manager
This commit is contained in:
Generated
+83
-6
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "导出数据库",
|
||||
|
||||
@@ -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: "匯出資料庫",
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user