From 3d7b149e53472a6e3a98de0fb555886214e6498a Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 13 May 2026 23:30:55 +0800 Subject: [PATCH] feat(agents): support system Java runtime --- crates/dbx-core/src/agent_manager.rs | 209 ++++++++++++++++++-- src-tauri/src/commands/agents.rs | 35 +++- src-tauri/src/lib.rs | 2 + src/components/config/DriverStoreDialog.vue | 96 +++++++++ 4 files changed, 321 insertions(+), 21 deletions(-) diff --git a/crates/dbx-core/src/agent_manager.rs b/crates/dbx-core/src/agent_manager.rs index fa942b9162..d594c63b9e 100644 --- a/crates/dbx-core/src/agent_manager.rs +++ b/crates/dbx-core/src/agent_manager.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; @@ -35,6 +36,85 @@ impl AgentRegistry { } } +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn test_manager(name: &str) -> AgentManager { + let dir = std::env::temp_dir().join(format!("dbx-agent-manager-{name}-{}", uuid::Uuid::new_v4())); + AgentManager::new_with_base_dir(dir) + } + + fn touch(path: &Path) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, b"").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).unwrap(); + } + } + + #[test] + fn resolves_managed_java_runtime_by_default() { + let manager = test_manager("managed"); + let java = manager.jre_java_path("17"); + touch(&java); + + let state = AgentState::default(); + + assert_eq!(manager.resolve_java_runtime(&state, "17").unwrap(), java); + } + + #[test] + fn resolves_custom_java_runtime_when_configured() { + let manager = test_manager("custom"); + let custom_java = manager.base_dir().join("custom").join("bin").join("java"); + touch(&custom_java); + let state = AgentState { + java_runtime: JavaRuntimeConfig { + mode: JavaRuntimeMode::Custom, + custom_java_path: Some(custom_java.to_string_lossy().to_string()), + }, + ..AgentState::default() + }; + + assert_eq!(manager.resolve_java_runtime(&state, "17").unwrap(), custom_java); + } + + #[test] + fn rejects_missing_custom_java_runtime() { + let manager = test_manager("missing-custom"); + let state = AgentState { + java_runtime: JavaRuntimeConfig { + mode: JavaRuntimeMode::Custom, + custom_java_path: Some(manager.base_dir().join("missing-java").to_string_lossy().to_string()), + }, + ..AgentState::default() + }; + + let err = manager.resolve_java_runtime(&state, "17").unwrap_err(); + + assert!(err.contains("Custom Java runtime does not exist")); + } + + #[test] + fn resolves_system_java_runtime_from_path() { + let manager = test_manager("system"); + let system_java = manager.base_dir().join("bin").join(if cfg!(windows) { "java.exe" } else { "java" }); + touch(&system_java); + let path = std::env::join_paths([system_java.parent().unwrap()]).unwrap(); + + assert_eq!(resolve_system_java_path(Some(path.as_os_str())).unwrap(), system_java); + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JreInfo { pub version: String, @@ -58,7 +138,7 @@ pub struct ArtifactInfo { pub size: u64, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AgentState { #[serde(default)] pub jre_version: Option, @@ -66,6 +146,8 @@ pub struct AgentState { pub jre_versions: std::collections::HashMap, #[serde(default)] pub installed_drivers: std::collections::HashMap, + #[serde(default)] + pub java_runtime: JavaRuntimeConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -76,6 +158,23 @@ pub struct InstalledDriver { pub jre: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum JavaRuntimeMode { + #[default] + Managed, + System, + Custom, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct JavaRuntimeConfig { + #[serde(default)] + pub mode: JavaRuntimeMode, + #[serde(default)] + pub custom_java_path: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentDriverInfo { pub db_type: String, @@ -98,10 +197,11 @@ impl AgentManager { pub fn new() -> Self { let home = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).unwrap_or_else(|_| ".".to_string()); - let mgr = Self { - base_dir: PathBuf::from(home).join(".dbx").join("agents"), - daemons: Mutex::new(std::collections::HashMap::new()), - }; + Self::new_with_base_dir(PathBuf::from(home).join(".dbx").join("agents")) + } + + pub fn new_with_base_dir(base_dir: PathBuf) -> Self { + let mgr = Self { base_dir, daemons: Mutex::new(std::collections::HashMap::new()) }; mgr.migrate_legacy_jre(); mgr } @@ -146,9 +246,7 @@ impl AgentManager { } pub fn load_state(&self) -> AgentState { - std::fs::read_to_string(self.state_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or( - AgentState { jre_version: None, jre_versions: Default::default(), installed_drivers: Default::default() }, - ) + std::fs::read_to_string(self.state_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default() } pub fn save_state(&self, state: &AgentState) -> Result<(), String> { @@ -166,6 +264,37 @@ impl AgentManager { self.driver_jar_path(db_type).exists() } + pub fn resolve_java_runtime(&self, state: &AgentState, jre_key: &str) -> Result { + match state.java_runtime.mode { + JavaRuntimeMode::Managed => { + if !self.is_jre_installed(jre_key) { + return Err(format!( + "JRE {jre_key} runtime is not installed. Please install it from the Driver Manager." + )); + } + Ok(self.jre_java_path(jre_key)) + } + JavaRuntimeMode::System => resolve_system_java_path(None).ok_or_else(|| { + "System Java runtime was not found on PATH. Please install Java or choose a custom Java executable." + .to_string() + }), + JavaRuntimeMode::Custom => { + let path = state + .java_runtime + .custom_java_path + .as_deref() + .map(str::trim) + .filter(|path| !path.is_empty()) + .ok_or_else(|| "Custom Java runtime path is empty. Please choose a Java executable.".to_string())?; + resolve_custom_java_path(path) + } + } + } + + pub async fn stop_daemons(&self) { + self.daemons.lock().await.clear(); + } + pub fn db_type_to_agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option<&'static str> { database_capabilities::agent_key(db_type, driver_profile) } @@ -185,14 +314,11 @@ impl AgentManager { let state = self.load_state(); let jre_key = state.installed_drivers.get(key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); - if !self.is_jre_installed(jre_key) { - return Err(format!("JRE {jre_key} runtime is not installed. Please install it from the Driver Manager.")); - } if !self.is_driver_installed(key) { return Err(format!("{key} driver is not installed. Please install it from the Driver Manager.")); } - let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); + let java = self.resolve_java_runtime(&state, jre_key)?.to_string_lossy().to_string(); let jar = self.driver_jar_path(key).to_string_lossy().to_string(); AgentDriverClient::spawn(&java, &jar).await } @@ -214,15 +340,10 @@ impl AgentManager { let state = self.load_state(); let jre_key = state.installed_drivers.get(&key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); - if !self.is_jre_installed(jre_key) { - return Err(format!( - "JRE {jre_key} runtime is not installed. Please install it from the Driver Manager." - )); - } if !self.is_driver_installed(&key) { return Err(format!("{key} driver is not installed. Please install it from the Driver Manager.")); } - let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); + let java = self.resolve_java_runtime(&state, jre_key)?.to_string_lossy().to_string(); let jar = self.driver_jar_path(&key).to_string_lossy().to_string(); let client = AgentDriverClient::spawn(&java, &jar).await?; daemons.insert(key.clone(), client); @@ -236,7 +357,7 @@ impl AgentManager { daemons.remove(&key); let state = self.load_state(); let jre_key = state.installed_drivers.get(&key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); - let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); + let java = self.resolve_java_runtime(&state, jre_key)?.to_string_lossy().to_string(); let jar = self.driver_jar_path(&key).to_string_lossy().to_string(); let mut new_client = AgentDriverClient::spawn(&java, &jar).await?; let result = new_client.call::(method, params).await?; @@ -276,3 +397,53 @@ impl AgentManager { } } } + +fn java_executable_name() -> &'static str { + if cfg!(windows) { + "java.exe" + } else { + "java" + } +} + +fn resolve_custom_java_path(path: &str) -> Result { + let raw = PathBuf::from(path); + if is_executable_file(&raw) { + return Ok(raw); + } + + let flat = raw.join("bin").join(java_executable_name()); + if is_executable_file(&flat) { + return Ok(flat); + } + + let macos = raw.join("Contents").join("Home").join("bin").join(java_executable_name()); + if is_executable_file(&macos) { + return Ok(macos); + } + + Err(format!("Custom Java runtime does not exist or is not a Java executable: {}", raw.display())) +} + +fn resolve_system_java_path(path_var: Option<&OsStr>) -> Option { + let path_var = path_var.map(|p| p.to_owned()).or_else(|| std::env::var_os("PATH"))?; + std::env::split_paths(&path_var) + .map(|dir| dir.join(java_executable_name())) + .find(|candidate| is_executable_file(candidate)) +} + +fn is_executable_file(path: &Path) -> bool { + if !path.is_file() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + return path.metadata().map(|meta| meta.permissions().mode() & 0o111 != 0).unwrap_or(false); + } + #[cfg(not(unix))] + { + true + } +} diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 2d50eb449b..ad1a07c355 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use tauri::{Emitter, State}; use tokio::sync::Mutex; -use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, DEFAULT_JRE_KEY}; +use dbx_core::agent_manager::{ + AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY, +}; use dbx_core::connection::AppState; const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json"; @@ -84,7 +86,7 @@ pub async fn install_agent( let driver = registry.drivers.get(&db_type).ok_or_else(|| format!("Unknown driver type: {db_type}"))?; let jre_key = &driver.jre; - let needs_jre = !am.is_jre_installed(jre_key); + let needs_jre = am.load_state().java_runtime.mode == JavaRuntimeMode::Managed && !am.is_jre_installed(jre_key); if needs_jre { let jre_info = @@ -161,6 +163,35 @@ pub async fn check_jre_installed(state: State<'_, Arc>, jre_key: Optio Ok(state.agent_manager.is_jre_installed(key)) } +#[tauri::command] +pub async fn get_agent_java_runtime_config(state: State<'_, Arc>) -> Result { + Ok(state.agent_manager.load_state().java_runtime) +} + +#[tauri::command] +pub async fn set_agent_java_runtime_config( + state: State<'_, Arc>, + mut config: JavaRuntimeConfig, +) -> Result { + let am = &state.agent_manager; + if config.mode == JavaRuntimeMode::Custom || config.mode == JavaRuntimeMode::System { + let candidate_state = dbx_core::agent_manager::AgentState { java_runtime: config.clone(), ..am.load_state() }; + let resolved = am.resolve_java_runtime(&candidate_state, DEFAULT_JRE_KEY)?; + if config.mode == JavaRuntimeMode::Custom { + config.custom_java_path = Some(resolved.to_string_lossy().to_string()); + } + } + if config.mode != JavaRuntimeMode::Custom { + config.custom_java_path = None; + } + + let mut local_state = am.load_state(); + local_state.java_runtime = config.clone(); + am.save_state(&local_state)?; + am.stop_daemons().await; + Ok(config) +} + #[tauri::command] pub async fn uninstall_jre(state: State<'_, Arc>, jre_key: String) -> Result<(), String> { let am = &state.agent_manager; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1c6c2a588e..b56f20b848 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -149,6 +149,8 @@ pub fn run() { commands::agents::install_agent, commands::agents::uninstall_agent, commands::agents::check_jre_installed, + commands::agents::get_agent_java_runtime_config, + commands::agents::set_agent_java_runtime_config, commands::agents::uninstall_jre, commands::agents::reinstall_jre, commands::agents::invalidate_agent_registry_cache, diff --git a/src/components/config/DriverStoreDialog.vue b/src/components/config/DriverStoreDialog.vue index 9d55165fca..e14c4dd573 100644 --- a/src/components/config/DriverStoreDialog.vue +++ b/src/components/config/DriverStoreDialog.vue @@ -8,6 +8,7 @@ import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check } fr import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import { useToast } from "@/composables/useToast"; @@ -39,11 +40,21 @@ interface InstallProgress { total?: number; } +type JavaRuntimeMode = "managed" | "system" | "custom"; + +interface JavaRuntimeConfig { + mode: JavaRuntimeMode; + custom_java_path: string | null; +} + const drivers = ref([]); const installing = ref(null); const reinstallingJre = ref(null); const refreshing = ref(false); const progress = ref(null); +const javaRuntimeConfig = ref({ mode: "managed", custom_java_path: null }); +const customJavaPath = ref(""); +const savingJavaRuntime = ref(false); let unlisten: UnlistenFn | null = null; @@ -91,6 +102,49 @@ async function forceRefresh() { } } +async function loadJavaRuntimeConfig() { + const config = await invoke("get_agent_java_runtime_config"); + javaRuntimeConfig.value = config; + customJavaPath.value = config.custom_java_path ?? ""; +} + +function setJavaRuntimeMode(value: any) { + if (value === "managed" || value === "system" || value === "custom") { + javaRuntimeConfig.value.mode = value; + } +} + +async function saveJavaRuntimeConfig() { + savingJavaRuntime.value = true; + try { + const config = await invoke("set_agent_java_runtime_config", { + config: { + mode: javaRuntimeConfig.value.mode, + custom_java_path: javaRuntimeConfig.value.mode === "custom" ? customJavaPath.value.trim() || null : null, + }, + }); + javaRuntimeConfig.value = config; + customJavaPath.value = config.custom_java_path ?? ""; + toast("Java 运行时设置已保存"); + } catch (e: any) { + toast(`Java 运行时设置失败: ${e}`); + } finally { + savingJavaRuntime.value = false; + } +} + +async function chooseCustomJavaPath() { + if (isWeb) return; + const { open } = await import("@tauri-apps/plugin-dialog"); + const selected = await open({ + title: "选择 Java 可执行文件", + multiple: false, + }); + if (typeof selected === "string") { + customJavaPath.value = selected; + } +} + async function installDriver(dbType: string) { const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType; installing.value = dbType; @@ -261,6 +315,7 @@ async function deleteJdbcDriver(path: string) { onMounted(async () => { drivers.value = await invoke("list_installed_agents_local"); + void loadJavaRuntimeConfig(); invoke("list_installed_agents").then((result) => { drivers.value = result; @@ -306,6 +361,47 @@ onUnmounted(() => { + +
+
+
+ + +
+ +
+
+ + +
+

+ 使用当前环境 PATH 中的 java。 +

+
+