feat(agent): pluggable database drivers via JDBC agent subprocess

Migrate domestic database support (DaMeng, KingBase, Vastbase, GoldenDB) from
built-in ODBC/protocol drivers to standalone Java agent processes communicating
via stdin/stdout JSON-RPC 2.0. Agents are distributed as fat JARs with embedded
JDBC drivers, downloaded on-demand from GitHub Releases with proxy fallback.

- Add AgentDriverClient for subprocess lifecycle + JSON-RPC communication
- Add AgentManager for JRE/JAR install state management
- Add Tauri commands for driver install/uninstall/list
- Add DriverManager UI panel in settings dialog
- Extend DatabaseType with Kingbase, Vastbase, Goldendb variants
- Migrate Dameng from ODBC (dm_driver.rs) to JDBC agent
- Delete dm_driver.rs (343 lines)
- Add GitHub proxy fallback for downloads (update.hwdns.net, gh-proxy.org)

Agent code lives at https://github.com/t8y2/dbx-agents
This commit is contained in:
t8y2
2026-05-13 04:44:52 +08:00
parent 8ef1999ec9
commit e85f9df1fe
20 changed files with 769 additions and 470 deletions
+171
View File
@@ -0,0 +1,171 @@
use std::sync::Arc;
use tauri::State;
use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver};
use dbx_core::connection::AppState;
const REGISTRY_URLS: &[&str] = &[
"https://update.hwdns.net/https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json",
"https://gh-proxy.org/https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json",
"https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json",
];
const DOWNLOAD_PROXIES: &[&str] = &["https://update.hwdns.net/", "https://gh-proxy.org/", ""];
#[tauri::command]
pub async fn list_installed_agents(state: State<'_, Arc<AppState>>) -> Result<Vec<AgentDriverInfo>, String> {
let am = &state.agent_manager;
let local_state = am.load_state();
let registry = fetch_registry().await.ok();
let agent_types = [
("dameng", "达梦 DM8"),
("kingbase", "人大金仓 KingbaseES"),
("vastbase", "Vastbase"),
("goldendb", "GoldenDB"),
];
Ok(agent_types
.iter()
.map(|(key, label)| {
let installed = am.is_driver_installed(key);
let local = local_state.installed_drivers.get(*key);
let remote = registry.as_ref().and_then(|r| r.drivers.get(*key));
AgentDriverInfo {
db_type: key.to_string(),
label: label.to_string(),
version: remote.map(|r| r.version.clone()).unwrap_or_default(),
size: remote.map(|r| r.jar.size).unwrap_or(0),
installed,
installed_version: local.map(|l| l.version.clone()),
update_available: match (local, remote) {
(Some(l), Some(r)) => l.version != r.version,
_ => false,
},
}
})
.collect())
}
#[tauri::command]
pub async fn install_agent(state: State<'_, Arc<AppState>>, db_type: String) -> Result<(), String> {
let am = &state.agent_manager;
let registry = fetch_registry().await?;
if !am.is_jre_installed() {
let platform = AgentManager::current_platform();
let jre_info =
registry.jre.platforms.get(platform).ok_or_else(|| format!("No JRE available for platform: {platform}"))?;
let jre_archive = am.base_dir().join("jre-download.tar.gz");
download_with_proxy(&jre_info.url, &jre_archive).await?;
extract_archive(&jre_archive, &am.base_dir().join("jre"))?;
std::fs::remove_file(&jre_archive).ok();
}
let driver = registry.drivers.get(&db_type).ok_or_else(|| format!("Unknown driver type: {db_type}"))?;
let jar_path = am.driver_jar_path(&db_type);
download_with_proxy(&driver.jar.url, &jar_path).await?;
let mut local_state = am.load_state();
local_state.jre_version = Some(registry.jre.version.clone());
local_state.installed_drivers.insert(
db_type,
InstalledDriver { version: driver.version.clone(), installed_at: chrono::Utc::now().to_rfc3339() },
);
am.save_state(&local_state)?;
Ok(())
}
#[tauri::command]
pub async fn uninstall_agent(state: State<'_, Arc<AppState>>, db_type: String) -> Result<(), String> {
let am = &state.agent_manager;
let jar_path = am.driver_jar_path(&db_type);
if jar_path.exists() {
std::fs::remove_file(&jar_path).map_err(|e| e.to_string())?;
}
let driver_dir = jar_path.parent().unwrap();
if driver_dir.exists() {
std::fs::remove_dir_all(driver_dir).map_err(|e| e.to_string())?;
}
let mut local_state = am.load_state();
local_state.installed_drivers.remove(&db_type);
am.save_state(&local_state)?;
Ok(())
}
#[tauri::command]
pub async fn check_jre_installed(state: State<'_, Arc<AppState>>) -> Result<bool, String> {
Ok(state.agent_manager.is_jre_installed())
}
async fn fetch_registry() -> Result<AgentRegistry, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
let mut last_err = String::new();
for url in REGISTRY_URLS {
match client
.get(*url)
.header(reqwest::header::USER_AGENT, "dbx-agent-manager")
.send()
.await
.and_then(|r| r.error_for_status())
{
Ok(resp) => {
return resp.json().await.map_err(|e| format!("Failed to parse registry: {e}"));
}
Err(e) => {
last_err = format!("{e}");
}
}
}
Err(format!("Failed to fetch agent registry: {last_err}"))
}
async fn download_with_proxy(url: &str, dest: &std::path::Path) -> Result<(), String> {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
let mut last_err = String::new();
for proxy in DOWNLOAD_PROXIES {
let full_url = format!("{proxy}{url}");
log::info!("[agent] downloading from {full_url}");
match client
.get(&full_url)
.header(reqwest::header::USER_AGENT, "dbx-agent-manager")
.send()
.await
.and_then(|r| r.error_for_status())
{
Ok(resp) => {
let bytes = resp.bytes().await.map_err(|e| format!("Download read failed: {e}"))?;
std::fs::write(dest, &bytes).map_err(|e| format!("Failed to write file: {e}"))?;
return Ok(());
}
Err(e) => {
last_err = format!("{e}");
log::warn!("[agent] download failed from {full_url}: {last_err}");
}
}
}
Err(format!("Failed to download {url}: {last_err}"))
}
fn extract_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
use std::process::Command;
std::fs::create_dir_all(dest).map_err(|e| e.to_string())?;
let status = Command::new("tar")
.args(["xzf", &archive.to_string_lossy(), "-C", &dest.to_string_lossy(), "--strip-components=1"])
.status()
.map_err(|e| format!("Failed to extract archive: {e}"))?;
if !status.success() {
return Err("Failed to extract JRE archive".to_string());
}
Ok(())
}
+32 -20
View File
@@ -112,15 +112,22 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
db::elasticsearch_driver::EsClient::new(&url, Some(&config.username), Some(&config.password));
db::elasticsearch_driver::test_connection(&client).await.map(|_| "Connection successful".to_string())
}
DatabaseType::Dameng => db::dm_driver::connect(
&host,
port,
config.database.as_deref().unwrap_or(""),
&config.username,
&config.password,
)
.await
.map(|_| "Connection successful".to_string()),
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => {
let mut client = state.agent_manager.spawn(&config.db_type).await?;
client
.call::<serde_json::Value>(
"test_connection",
serde_json::json!({
"host": host,
"port": port,
"database": config.database.as_deref().unwrap_or(""),
"username": config.username,
"password": config.password,
}),
)
.await?;
Ok("Connection successful".to_string())
}
DatabaseType::Gaussdb => db::gaussdb_driver::connect(
&host,
port,
@@ -220,16 +227,21 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
db::elasticsearch_driver::test_connection(&client).await?;
PoolKind::Elasticsearch(client)
}
DatabaseType::Dameng => {
let client = db::dm_driver::connect(
&host,
port,
db_config.database.as_deref().unwrap_or(""),
&db_config.username,
&db_config.password,
)
.await?;
PoolKind::Dameng(std::sync::Arc::new(std::sync::Mutex::new(client)))
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => {
let mut client = state.agent_manager.spawn(&db_config.db_type).await?;
client
.call::<serde_json::Value>(
"connect",
serde_json::json!({
"host": host,
"port": port,
"database": db_config.effective_database().unwrap_or(""),
"username": db_config.username,
"password": db_config.password,
}),
)
.await?;
PoolKind::Agent(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
}
DatabaseType::Gaussdb => {
let client = db::gaussdb_driver::connect(
@@ -269,7 +281,7 @@ pub async fn disconnect_db(state: State<'_, Arc<AppState>>, connection_id: Strin
PoolKind::SqlServer(_) => {}
PoolKind::Oracle(_) => {}
PoolKind::Elasticsearch(_) => {}
PoolKind::Dameng(_) => {}
PoolKind::Agent(_) => {}
PoolKind::Gaussdb(_) => {}
PoolKind::ExternalTabular(_) => {}
PoolKind::ExternalDriver { .. } => {}
+1
View File
@@ -1,3 +1,4 @@
pub mod agents;
pub mod ai;
pub mod connection;
#[allow(dead_code, unused_imports)]
+4
View File
@@ -143,6 +143,10 @@ pub fn run() {
commands::transfer::cancel_transfer,
commands::database_export::export_database_sql,
commands::database_export::cancel_database_export,
commands::agents::list_installed_agents,
commands::agents::install_agent,
commands::agents::uninstall_agent,
commands::agents::check_jre_installed,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")