mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
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:
@@ -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(())
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
pub mod agents;
|
||||
pub mod ai;
|
||||
pub mod connection;
|
||||
#[allow(dead_code, unused_imports)]
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user