diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index cd53545f9..4ea68570c 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -212,6 +212,11 @@ async function runDriverInstall(dbType: string) { installing.value = dbType; progress.value = null; try { + const blockers = await api.checkAgentUpdateBlockers([dbType]); + if (blockers.length > 0) { + toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") })); + return; + } await api.installAgent(dbType); await refreshAgents(); toast(t("driverStore.driverInstallSuccess", { label })); @@ -239,9 +244,22 @@ async function upgradeAll() { queuedDriverInstalls.value = []; progress.value = null; try { - const count = await api.upgradeAllAgents(); + const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type); + const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes); + if (blockers.length > 0) { + toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") })); + return; + } + const result = await api.upgradeAllAgents(); await refreshAgents(); - toast(t("driverStore.upgradeAllSuccess", { count })); + if (result.failed.length > 0) { + const failedLabels = result.failed + .map((item) => drivers.value.find((driver) => driver.db_type === item.db_type)?.label ?? item.db_type) + .join(", "); + toast(t("driverStore.upgradeAllPartial", { count: result.upgraded, failed: failedLabels })); + } else { + toast(t("driverStore.upgradeAllSuccess", { count: result.upgraded })); + } } catch (e: any) { toast(t("driverStore.upgradeAllFailed", { error: e })); } finally { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 6c459500e..7c16ee3f7 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1764,7 +1764,9 @@ export default { chooseJavaExecutable: "Choose Java executable", driverInstallSuccess: "{label} driver installed", driverInstallFailed: "Failed to install {label} driver: {error}", + driverUpdateBlocked: "Close these database connections before updating drivers: {labels}", upgradeAllSuccess: "{count} driver(s) upgraded", + upgradeAllPartial: "{count} driver(s) upgraded. Skipped: {failed}", upgradeAllFailed: "Batch upgrade failed: {error}", driverUninstallSuccess: "{label} driver uninstalled", driverUninstallFailed: "Failed to uninstall {label} driver: {error}", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index d98362462..8d624e1d4 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1655,7 +1655,9 @@ export default { chooseJavaExecutable: "Seleccionar ejecutable Java", driverInstallSuccess: "Driver {label} instalado", driverInstallFailed: "Error al instalar el driver {label}: {error}", + driverUpdateBlocked: "Cierra estas conexiones de base de datos antes de actualizar drivers: {labels}", upgradeAllSuccess: "{count} driver(s) actualizados", + upgradeAllPartial: "{count} driver(s) actualizados. Omitidos: {failed}", upgradeAllFailed: "Error al actualizar en lote: {error}", driverUninstallSuccess: "Driver {label} desinstalado", driverUninstallFailed: "Error al desinstalar el driver {label}: {error}", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index d13f7a956..b22f7cfff 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1725,7 +1725,9 @@ export default { chooseJavaExecutable: "选择 Java 可执行文件", driverInstallSuccess: "{label} 驱动安装成功", driverInstallFailed: "{label} 驱动安装失败: {error}", + driverUpdateBlocked: "请先关闭以下数据库连接后再更新驱动: {labels}", upgradeAllSuccess: "{count} 个驱动升级完成", + upgradeAllPartial: "{count} 个驱动升级完成,以下驱动已跳过: {failed}", upgradeAllFailed: "批量升级失败: {error}", driverUninstallSuccess: "{label} 驱动已卸载", driverUninstallFailed: "{label} 驱动卸载失败: {error}", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index e7a80922f..90cc15c87 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1691,7 +1691,9 @@ export default { chooseJavaExecutable: "選擇 Java 可執行檔", driverInstallSuccess: "{label} 驅動程式安裝成功", driverInstallFailed: "{label} 驅動程式安裝失敗: {error}", + driverUpdateBlocked: "請先關閉以下資料庫連線後再更新驅動程式: {labels}", upgradeAllSuccess: "{count} 個驅動程式更新完成", + upgradeAllPartial: "{count} 個驅動程式更新完成,以下驅動程式已略過: {failed}", upgradeAllFailed: "批次更新失敗: {error}", driverUninstallSuccess: "{label} 驅動程式已解除安裝", driverUninstallFailed: "{label} 驅動程式解除安裝失敗: {error}", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index a77715a8a..5709d21a7 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -51,6 +51,7 @@ export const listInstalledAgents = forward("listInstalledAgents"); export const getDriverStoreUsage = forward("getDriverStoreUsage"); export const installAgent = forward("installAgent"); export const upgradeAllAgents = forward("upgradeAllAgents"); +export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers"); export const uninstallAgent = forward("uninstallAgent"); export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig"); export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 08c0920a2..d6b430cb8 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -28,6 +28,8 @@ import type { AiConversation, AiModelInfo, DriverStoreUsage, + UpgradeAllAgentDriversResult, + AgentUpdateBlocker, DesktopSettings, DriverInstallProgress, JavaRuntimeConfig, @@ -242,9 +244,12 @@ export async function installAgent(dbType: string): Promise { await post("/api/agents/install", { dbType }); } -export async function upgradeAllAgents(): Promise { - const result: { count: number } = await post("/api/agents/upgrade-all", {}); - return result.count; +export async function upgradeAllAgents(): Promise { + return post("/api/agents/upgrade-all", {}); +} + +export async function checkAgentUpdateBlockers(_dbTypes: string[]): Promise { + return []; } export async function uninstallAgent(dbType: string): Promise { diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 9769d3a5f..f528767e6 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -74,6 +74,21 @@ export interface AgentDriverInfo { jre_installed: boolean; } +export interface AgentDriverUpdateIssue { + db_type: string; + error: string; +} + +export interface UpgradeAllAgentDriversResult { + upgraded: number; + failed: AgentDriverUpdateIssue[]; +} + +export interface AgentUpdateBlocker { + db_type: string; + label: string; +} + export type JavaRuntimeMode = "managed" | "system" | "custom"; export interface JavaRuntimeConfig { @@ -90,6 +105,7 @@ export interface DriverStoreUsage { total_bytes: number; jre_bytes: number; agent_driver_bytes: number; + download_cache_bytes?: number; jdbc_plugin_bytes: number; jdbc_driver_bytes: number; jres: DriverStoreUsageItem[]; @@ -847,10 +863,14 @@ export async function installAgent(dbType: string): Promise { return invoke("install_agent", { dbType }); } -export async function upgradeAllAgents(): Promise { +export async function upgradeAllAgents(): Promise { return invoke("upgrade_all_agents"); } +export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise { + return invoke("check_agent_update_blockers", { dbTypes }); +} + export async function uninstallAgent(dbType: string): Promise { return invoke("uninstall_agent", { dbType }); } diff --git a/crates/dbx-core/src/agent_manager.rs b/crates/dbx-core/src/agent_manager.rs index 7e1805f23..afb667cda 100644 --- a/crates/dbx-core/src/agent_manager.rs +++ b/crates/dbx-core/src/agent_manager.rs @@ -8,6 +8,8 @@ use crate::db::agent_driver::{AgentDriverClient, AgentMethod}; use crate::models::connection::DatabaseType; pub const DEFAULT_JRE_KEY: &str = "21"; +pub const DOWNLOAD_CACHE_DIR_NAME: &str = "download-cache"; +pub const DOWNLOAD_CACHE_MAX_AGE_DAYS: u64 = 7; fn default_jre_key() -> String { DEFAULT_JRE_KEY.to_string() @@ -245,6 +247,8 @@ pub struct DriverStoreUsage { pub total_bytes: u64, pub jre_bytes: u64, pub agent_driver_bytes: u64, + #[serde(default)] + pub download_cache_bytes: u64, pub jdbc_plugin_bytes: u64, pub jdbc_driver_bytes: u64, pub jres: Vec, @@ -320,6 +324,14 @@ impl AgentManager { self.base_dir.join("drivers").join(db_type).join("agent.jar") } + pub fn download_cache_dir(&self) -> PathBuf { + self.base_dir.join(DOWNLOAD_CACHE_DIR_NAME) + } + + pub fn download_cache_max_age_days(&self) -> u64 { + DOWNLOAD_CACHE_MAX_AGE_DAYS + } + fn state_path(&self) -> PathBuf { self.base_dir.join("state.json") } @@ -390,14 +402,17 @@ impl AgentManager { let jdbc_driver_bytes = path_size_bytes(&jdbc_driver_root); let jdbc_total_bytes = path_size_bytes(&jdbc_root); let jdbc_plugin_bytes = jdbc_total_bytes.saturating_sub(jdbc_driver_bytes); + let download_cache_bytes = path_size_bytes(&self.download_cache_dir()); DriverStoreUsage { total_bytes: jre_bytes .saturating_add(agent_driver_bytes) + .saturating_add(download_cache_bytes) .saturating_add(jdbc_plugin_bytes) .saturating_add(jdbc_driver_bytes), jre_bytes, agent_driver_bytes, + download_cache_bytes, jdbc_plugin_bytes, jdbc_driver_bytes, jres, @@ -440,6 +455,10 @@ impl AgentManager { crate::agent_runtime::stop_daemon_by_key(self, agent_key).await; } + pub async fn active_daemon_keys(&self) -> Vec { + self.daemons.lock().await.keys().cloned().collect() + } + pub fn db_type_to_agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option<&'static str> { crate::agent_runtime::db_type_to_agent_key(db_type, driver_profile) } diff --git a/crates/dbx-core/src/agent_service.rs b/crates/dbx-core/src/agent_service.rs index 8ae606d86..8647420ec 100644 --- a/crates/dbx-core/src/agent_service.rs +++ b/crates/dbx-core/src/agent_service.rs @@ -1,3 +1,4 @@ +use std::hash::{Hash, Hasher}; use std::io::Read; use std::path::{Path, PathBuf}; @@ -27,6 +28,18 @@ pub struct AgentProgressEvent { pub total_drivers: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct AgentDriverUpdateIssue { + pub db_type: String, + pub error: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)] +pub struct UpgradeAllAgentDriversResult { + pub upgraded: u32, + pub failed: Vec, +} + impl AgentProgressEvent { pub fn step(step: impl Into) -> Self { Self { step: step.into(), downloaded: None, total: None, db_type: None, current: None, total_drivers: None } @@ -163,14 +176,15 @@ pub async fn install_agent_driver( pub async fn upgrade_all_agent_drivers( am: &AgentManager, progress: impl Fn(AgentProgressEvent), -) -> Result { +) -> Result { let registry = fetch_registry().await?; let agents = build_agent_list(am, Some(®istry)); let updatable: Vec<&AgentDriverInfo> = agents.iter().filter(|agent| agent.update_available).collect(); let total = updatable.len() as u32; + let mut result = UpgradeAllAgentDriversResult::default(); for (index, agent) in updatable.iter().enumerate() { - install_agent_driver_from_registry( + match install_agent_driver_from_registry( am, ®istry, &agent.db_type, @@ -178,11 +192,18 @@ pub async fn upgrade_all_agent_drivers( Some((index + 1) as u32), Some(total), ) - .await?; + .await + { + Ok(()) => result.upgraded += 1, + Err(error) => { + log::warn!("Failed to update {} agent driver: {}", agent.db_type, error); + result.failed.push(AgentDriverUpdateIssue { db_type: agent.db_type.clone(), error }); + } + } } progress(AgentProgressEvent::step("all-done")); - Ok(total) + Ok(result) } pub async fn uninstall_agent_driver(am: &AgentManager, db_type: &str) -> Result<(), String> { @@ -238,6 +259,7 @@ pub async fn reinstall_agent_jre( .ok_or_else(|| format!("No JRE {jre_key} available for platform: {platform}"))?; let jre_archive = am.base_dir().join("jre-download.tar.gz"); download_with_progress( + am, &progress, "jre", &platform_jre.url, @@ -338,6 +360,7 @@ async fn install_agent_driver_from_registry( total_drivers, )); download_with_progress( + am, progress, "jre", &platform_jre.url, @@ -365,6 +388,7 @@ async fn install_agent_driver_from_registry( total_drivers, )); download_with_progress( + am, progress, "driver", &driver.jar.url, @@ -396,6 +420,7 @@ async fn install_agent_driver_from_registry( } async fn download_with_progress( + am: &AgentManager, progress: &impl Fn(AgentProgressEvent), step: &str, url: &str, @@ -410,6 +435,18 @@ async fn download_with_progress( std::fs::create_dir_all(parent).map_err(|err| err.to_string())?; } let tmp = download_temp_path(dest); + let cache_path = cached_download_path(am, url, total_size, dest); + prune_download_cache(am).ok(); + if cached_download_is_valid(am, &cache_path, total_size) { + std::fs::copy(&cache_path, &tmp).map_err(|err| format!("Failed to copy cached download: {err}"))?; + progress(AgentProgressEvent::transfer(step, total_size, total_size).with_batch( + db_type, + current, + total_drivers, + )); + return replace_download(&tmp, dest); + } + let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .build() @@ -431,9 +468,62 @@ async fn download_with_progress( } std::io::Write::flush(&mut file).map_err(|err| format!("Failed to flush temp file: {err}"))?; drop(file); + if let Some(parent) = cache_path.parent() { + if let Err(err) = std::fs::create_dir_all(parent) { + log::warn!("Failed to create agent download cache directory: {err}"); + } else if let Err(err) = std::fs::copy(&tmp, &cache_path) { + log::warn!("Failed to cache agent download: {err}"); + } + } replace_download(&tmp, dest) } +fn cached_download_path(am: &AgentManager, url: &str, total_size: u64, dest: &std::path::Path) -> std::path::PathBuf { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + url.hash(&mut hasher); + total_size.hash(&mut hasher); + let hash = hasher.finish(); + let file_name = dest.file_name().and_then(|name| name.to_str()).unwrap_or("download"); + am.download_cache_dir().join(format!("{hash:016x}-{file_name}")) +} + +fn cached_download_is_valid(am: &AgentManager, path: &std::path::Path, expected_size: u64) -> bool { + let Ok(meta) = std::fs::metadata(path) else { + return false; + }; + if !meta.is_file() { + return false; + } + if expected_size > 0 && meta.len() != expected_size { + let _ = std::fs::remove_file(path); + return false; + } + let max_age = std::time::Duration::from_secs(am.download_cache_max_age_days() * 24 * 60 * 60); + if meta.modified().ok().and_then(|modified| modified.elapsed().ok()).is_some_and(|age| age > max_age) { + let _ = std::fs::remove_file(path); + return false; + } + true +} + +fn prune_download_cache(am: &AgentManager) -> Result<(), String> { + let cache_dir = am.download_cache_dir(); + let max_age = std::time::Duration::from_secs(am.download_cache_max_age_days() * 24 * 60 * 60); + let Ok(entries) = std::fs::read_dir(&cache_dir) else { + return Ok(()); + }; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(meta) = entry.metadata() else { + continue; + }; + if meta.modified().ok().and_then(|modified| modified.elapsed().ok()).is_some_and(|age| age > max_age) { + let _ = if meta.is_dir() { std::fs::remove_dir_all(path) } else { std::fs::remove_file(path) }; + } + } + Ok(()) +} + pub fn github_url_to_r2_path(github_url: &str, category: &str) -> String { let filename = github_url.rsplit('/').next().unwrap_or(github_url); match category { diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index f34ce5296..2c1ff9fc1 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; @@ -650,6 +650,36 @@ impl AppState { Ok(closed) } + pub async fn active_agent_driver_keys(&self) -> HashSet { + let configs = self.configs.read().await; + let connections = self.connections.read().await; + let mut keys = HashSet::new(); + + for (pool_key, pool) in connections.iter() { + if !matches!(pool, PoolKind::Agent(_)) { + continue; + } + let Some(config) = config_for_pool_key(pool_key, &configs) else { + continue; + }; + if let Some(agent_key) = crate::agent_manager::AgentManager::db_type_to_agent_key( + &config.db_type, + config.driver_profile.as_deref(), + ) { + keys.insert(agent_key.to_string()); + } + } + + drop(connections); + drop(configs); + + for key in self.agent_manager.active_daemon_keys().await { + keys.insert(key); + } + + keys + } + pub async fn duckdb_existing_pool_is_usable_for_config(&self, config: &ConnectionConfig) -> Result { if config.db_type != DatabaseType::DuckDb { return Ok(false); @@ -800,6 +830,19 @@ 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>( + pool_key: &str, + configs: &'a HashMap, +) -> Option<&'a ConnectionConfig> { + configs + .iter() + .filter(|(connection_id, _)| { + pool_key.strip_prefix(connection_id.as_str()).is_some_and(|rest| rest.is_empty() || rest.starts_with(':')) + }) + .max_by_key(|(connection_id, _)| connection_id.len()) + .map(|(_, config)| config) +} + fn session_scoped_pool_key_for( db_type: Option, base_pool_key: String, diff --git a/crates/dbx-web/src/routes/agents.rs b/crates/dbx-web/src/routes/agents.rs index a1e9e081a..d86c3c9d8 100644 --- a/crates/dbx-web/src/routes/agents.rs +++ b/crates/dbx-web/src/routes/agents.rs @@ -55,6 +55,7 @@ pub async fn install_agent( State(state): State>, Json(req): Json, ) -> Result, AppError> { + ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&req.db_type)).await.map_err(AppError)?; let tx = progress_sender(&state, "global").await; install_agent_driver(&state.app.agent_manager, &req.db_type, |event| send_progress_event(&tx, event)) .await @@ -63,11 +64,16 @@ pub async fn install_agent( } pub async fn upgrade_all_agents(State(state): State>) -> Result, AppError> { + let registry = fetch_registry().await.map_err(AppError)?; + let agents = build_agent_list(&state.app.agent_manager, Some(®istry)); + let updatable: Vec = + agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect(); + ensure_no_agent_update_blockers(&state.app, &updatable).await.map_err(AppError)?; let tx = progress_sender(&state, "global").await; - let total = upgrade_all_agent_drivers(&state.app.agent_manager, |event| send_progress_event(&tx, event)) + let result = upgrade_all_agent_drivers(&state.app.agent_manager, |event| send_progress_event(&tx, event)) .await .map_err(AppError)?; - Ok(Json(serde_json::json!({ "count": total }))) + Ok(Json(serde_json::to_value(result).map_err(|err| AppError(err.to_string()))?)) } pub async fn uninstall_agent( @@ -224,3 +230,26 @@ fn send_progress_event(tx: &broadcast::Sender, event: AgentProgressEvent let _ = tx.send(payload); } } + +async fn ensure_no_agent_update_blockers( + state: &dbx_core::connection::AppState, + db_types: &[String], +) -> Result<(), String> { + let candidate_keys: std::collections::HashSet<&str> = db_types.iter().map(String::as_str).collect(); + if candidate_keys.is_empty() { + return Ok(()); + } + let mut blockers = state + .active_agent_driver_keys() + .await + .into_iter() + .filter(|key| candidate_keys.contains(key.as_str())) + .map(|key| dbx_core::agent_catalog::label_for_key(&key).unwrap_or(&key).to_string()) + .collect::>(); + blockers.sort(); + if blockers.is_empty() { + Ok(()) + } else { + Err(format!("请先关闭以下数据库连接后再更新驱动: {}", blockers.join(", "))) + } +} diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 8a0fe51af..34a69c7cd 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -6,10 +6,16 @@ use dbx_core::agent_manager::{AgentDriverInfo, DriverStoreUsage, JavaRuntimeConf use dbx_core::agent_service::{ build_agent_list, fetch_registry, import_agent_jar, import_agents_from_zip as import_agents_from_zip_core, install_agent_driver, invalidate_registry_cache, reinstall_agent_jre, uninstall_agent_driver, uninstall_agent_jre, - upgrade_all_agent_drivers, AgentProgressEvent, + upgrade_all_agent_drivers, AgentProgressEvent, UpgradeAllAgentDriversResult, }; use dbx_core::connection::AppState; +#[derive(Debug, Clone, serde::Serialize)] +pub struct AgentUpdateBlocker { + pub db_type: String, + pub label: String, +} + #[tauri::command] pub async fn list_installed_agents_local(state: State<'_, Arc>) -> Result, String> { Ok(build_agent_list(&state.agent_manager, None)) @@ -32,16 +38,33 @@ pub async fn install_agent( state: State<'_, Arc>, db_type: String, ) -> Result<(), String> { + ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?; let app_handle = app.clone(); install_agent_driver(&state.agent_manager, &db_type, move |event| emit_agent_progress(&app_handle, event)).await } #[tauri::command] -pub async fn upgrade_all_agents(app: tauri::AppHandle, state: State<'_, Arc>) -> Result { +pub async fn upgrade_all_agents( + app: tauri::AppHandle, + state: State<'_, Arc>, +) -> Result { + let registry = fetch_registry().await?; + let agents = build_agent_list(&state.agent_manager, Some(®istry)); + let updatable: Vec = + agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect(); + ensure_no_agent_update_blockers(state.inner().as_ref(), &updatable).await?; let app_handle = app.clone(); upgrade_all_agent_drivers(&state.agent_manager, move |event| emit_agent_progress(&app_handle, event)).await } +#[tauri::command] +pub async fn check_agent_update_blockers( + state: State<'_, Arc>, + db_types: Vec, +) -> Result, String> { + Ok(agent_update_blockers(state.inner().as_ref(), &db_types).await) +} + #[tauri::command] pub async fn uninstall_agent(state: State<'_, Arc>, db_type: String) -> Result<(), String> { uninstall_agent_driver(&state.agent_manager, &db_type).await @@ -131,3 +154,30 @@ pub async fn reinstall_jre( fn emit_agent_progress(app: &tauri::AppHandle, event: AgentProgressEvent) { let _ = app.emit("agent-install-progress", event); } + +async fn ensure_no_agent_update_blockers(state: &AppState, db_types: &[String]) -> Result<(), String> { + let blockers = agent_update_blockers(state, db_types).await; + if blockers.is_empty() { + return Ok(()); + } + let labels = blockers.into_iter().map(|blocker| blocker.label).collect::>().join(", "); + Err(format!("请先关闭以下数据库连接后再更新驱动: {labels}")) +} + +async fn agent_update_blockers(state: &AppState, db_types: &[String]) -> Vec { + let candidate_keys: std::collections::HashSet<&str> = db_types.iter().map(String::as_str).collect(); + if candidate_keys.is_empty() { + return Vec::new(); + } + let active_keys = state.active_agent_driver_keys().await; + let mut blockers = active_keys + .into_iter() + .filter(|key| candidate_keys.contains(key.as_str())) + .map(|db_type| AgentUpdateBlocker { + label: dbx_core::agent_catalog::label_for_key(&db_type).unwrap_or(&db_type).to_string(), + db_type, + }) + .collect::>(); + blockers.sort_by(|left, right| left.label.cmp(&right.label)); + blockers +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5855c200b..208ae82be 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -512,6 +512,7 @@ pub fn run() { commands::agents::get_driver_store_usage, commands::agents::install_agent, commands::agents::upgrade_all_agents, + commands::agents::check_agent_update_blockers, commands::agents::uninstall_agent, commands::agents::check_jre_installed, commands::agents::get_agent_java_runtime_config,