diff --git a/crates/dbx-core/src/jdbc.rs b/crates/dbx-core/src/jdbc.rs index b73d50d9c2..c01021d2cd 100644 --- a/crates/dbx-core/src/jdbc.rs +++ b/crates/dbx-core/src/jdbc.rs @@ -79,15 +79,24 @@ pub async fn get_jdbc_plugin_status(plugins_root: &Path) -> Result Result { let bytes = download_jdbc_plugin_zip().await?; let plugin_dir = plugins_root.join("jdbc"); - install_jdbc_plugin_zip(&bytes, &plugin_dir)?; - jdbc_plugin_status_from_dir(&plugin_dir).await + let status_dir = plugin_dir.clone(); + tokio::task::spawn_blocking(move || install_jdbc_plugin_zip(&bytes, &plugin_dir)) + .await + .map_err(|err| err.to_string())??; + jdbc_plugin_status_from_dir(&status_dir).await } pub async fn install_jdbc_plugin_from_file(plugins_root: &Path, file_path: &str) -> Result { - let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read file: {e}"))?; let plugin_dir = plugins_root.join("jdbc"); - install_jdbc_plugin_zip(&bytes, &plugin_dir)?; - jdbc_plugin_status_from_dir(&plugin_dir).await + let status_dir = plugin_dir.clone(); + let file_path = file_path.to_string(); + tokio::task::spawn_blocking(move || { + let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read file: {e}"))?; + install_jdbc_plugin_zip(&bytes, &plugin_dir) + }) + .await + .map_err(|err| err.to_string())??; + jdbc_plugin_status_from_dir(&status_dir).await } pub fn uninstall_jdbc_plugin(plugins_root: &Path) -> Result { diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs index d1b0e63b15..7b645b18ea 100644 --- a/src-tauri/src/commands/app_settings.rs +++ b/src-tauri/src/commands/app_settings.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use dbx_core::storage::DesktopSettings; use tauri::{AppHandle, Manager, State}; @@ -37,9 +37,15 @@ pub async fn save_pinned_tree_node_ids(state: State<'_, Arc>, ids: Vec #[tauri::command] pub async fn load_native_debug_logs(app: AppHandle) -> Result { + let log_dir = app.path().app_log_dir().map_err(|e| e.to_string())?; + tauri::async_runtime::spawn_blocking(move || load_native_debug_logs_from_dir(log_dir)) + .await + .map_err(|err| err.to_string())? +} + +fn load_native_debug_logs_from_dir(log_dir: PathBuf) -> Result { const MAX_FILES: usize = 6; const MAX_FILE_BYTES: u64 = 512 * 1024; - let log_dir = app.path().app_log_dir().map_err(|e| e.to_string())?; if !log_dir.exists() { return Ok(format!("Native log dir does not exist yet: {}", log_dir.display())); } diff --git a/src-tauri/src/commands/csv_export.rs b/src-tauri/src/commands/csv_export.rs index 3ed996b5b5..1381aab7e8 100644 --- a/src-tauri/src/commands/csv_export.rs +++ b/src-tauri/src/commands/csv_export.rs @@ -15,9 +15,13 @@ pub struct QueryResultCsvExportRequest { } #[tauri::command] -pub fn export_query_result_csv(request: QueryResultCsvExportRequest) -> Result<(), String> { - let csv = format_csv(&request.columns, &request.rows); - std::fs::write(&request.file_path, format!("\u{FEFF}{csv}")).map_err(|err| err.to_string()) +pub async fn export_query_result_csv(request: QueryResultCsvExportRequest) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || { + let csv = format_csv(&request.columns, &request.rows); + std::fs::write(&request.file_path, format!("\u{FEFF}{csv}")).map_err(|err| err.to_string()) + }) + .await + .map_err(|err| err.to_string())? } #[tauri::command] diff --git a/src-tauri/src/commands/external_sql.rs b/src-tauri/src/commands/external_sql.rs index a162d8f187..64b1f477e1 100644 --- a/src-tauri/src/commands/external_sql.rs +++ b/src-tauri/src/commands/external_sql.rs @@ -12,8 +12,8 @@ pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) -> } #[tauri::command] -pub fn read_external_sql_file(path: String) -> Result { - read_external_sql_file_content(Path::new(&path)) +pub async fn read_external_sql_file(path: String) -> Result { + read_external_sql_file_content_async(PathBuf::from(path)).await } #[derive(Default)] @@ -62,7 +62,8 @@ pub fn is_sql_file_path(path: &Path) -> bool { path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("sql")).unwrap_or(false) } -pub fn read_external_sql_file_content(path: &Path) -> Result { +#[cfg(test)] +fn read_external_sql_file_content(path: &Path) -> Result { if !is_sql_file_path(path) { return Err("Only .sql files can be opened this way".to_string()); } @@ -70,6 +71,14 @@ pub fn read_external_sql_file_content(path: &Path) -> Result { decode_sql_file_bytes(&bytes) } +async fn read_external_sql_file_content_async(path: PathBuf) -> Result { + if !is_sql_file_path(&path) { + return Err("Only .sql files can be opened this way".to_string()); + } + let bytes = tokio::fs::read(&path).await.map_err(|e| format!("Failed to read SQL file: {e}"))?; + decode_sql_file_bytes(&bytes) +} + fn dedupe_paths(paths: Vec) -> Vec { let mut unique = Vec::new(); for path in paths { diff --git a/src-tauri/src/commands/keychain.rs b/src-tauri/src/commands/keychain.rs index d196d5f920..020a311fb0 100644 --- a/src-tauri/src/commands/keychain.rs +++ b/src-tauri/src/commands/keychain.rs @@ -1,5 +1,3 @@ -use std::process::Command; - /// Read a macOS Keychain generic password by service name. /// Triggers a system authorization dialog (Touch ID / password) for each unique service. #[tauri::command] @@ -12,28 +10,34 @@ pub async fn read_keychain_password(service: String, account: Option) -> #[cfg(target_os = "macos")] { - let mut cmd = Command::new("security"); - cmd.args(["find-generic-password", "-s", &service, "-w"]); - if let Some(ref acct) = account { - cmd.args(["-a", acct]); - } + tauri::async_runtime::spawn_blocking(move || read_keychain_password_blocking(service, account)) + .await + .map_err(|err| err.to_string())? + } +} - let output = cmd.output().map_err(|e| format!("Failed to run security command: {e}"))?; +#[cfg(target_os = "macos")] +fn read_keychain_password_blocking(service: String, account: Option) -> Result { + let mut cmd = std::process::Command::new("security"); + cmd.args(["find-generic-password", "-s", &service, "-w"]); + if let Some(ref acct) = account { + cmd.args(["-a", acct]); + } - if output.status.success() { - let password = String::from_utf8_lossy(&output.stdout).trim().to_string(); - Ok(password) + let output = cmd.output().map_err(|e| format!("Failed to run security command: {e}"))?; + + if output.status.success() { + let password = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(password) + } else { + // Exit code 44 = user cancelled the authorization dialog + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + if output.status.code() == Some(44) || stderr.contains("User canceled") { + Ok(String::new()) + } else if stderr.contains("could not be found") || stderr.contains("The specified item could not be found") { + Ok(String::new()) } else { - // Exit code 44 = user cancelled the authorization dialog - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - if output.status.code() == Some(44) || stderr.contains("User canceled") { - Ok(String::new()) // Return empty instead of error for cancelled - } else if stderr.contains("could not be found") || stderr.contains("The specified item could not be found") - { - Ok(String::new()) // No entry found — not an error - } else { - Err(format!("Keychain read failed: {}", stderr.trim())) - } + Err(format!("Keychain read failed: {}", stderr.trim())) } } } diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index f6cd04bc50..50993b2870 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -31,11 +31,17 @@ struct NpmLatestPackage { #[tauri::command] pub async fn check_mcp_server_status() -> Result { - let npm_available = command_success("npm", &["--version"]); - let node_version = command_stdout("node", &["--version"]).ok().and_then(first_non_empty_line); - let current_version = if npm_available { installed_mcp_version() } else { None }; - let bin_path = locate_mcp_bin(); - let latest_version = fetch_latest_mcp_version().await.ok(); + let local_status = tauri::async_runtime::spawn_blocking(|| { + let npm_available = command_success("npm", &["--version"]); + let node_version = command_stdout("node", &["--version"]).ok().and_then(first_non_empty_line); + let current_version = if npm_available { installed_mcp_version() } else { None }; + let bin_path = locate_mcp_bin(); + (npm_available, node_version, current_version, bin_path) + }); + let latest_version = fetch_latest_mcp_version(); + let (local_status, latest_version) = tokio::join!(local_status, latest_version); + let (npm_available, node_version, current_version, bin_path) = local_status.map_err(|err| err.to_string())?; + let latest_version = latest_version.ok(); let update_available = current_version .as_deref() .zip(latest_version.as_deref()) @@ -58,7 +64,9 @@ pub async fn check_mcp_server_status() -> Result { async fn fetch_latest_mcp_version() -> Result { let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(10)).user_agent("dbx-mcp-status-checker"); - if let Some(proxy_url) = dbx_core::update::system_proxy_url() { + let proxy_url = + tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.map_err(|e| e.to_string())?; + if let Some(proxy_url) = proxy_url { let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Invalid system proxy URL: {e}"))?; builder = builder.proxy(proxy); } diff --git a/src-tauri/src/commands/plugins.rs b/src-tauri/src/commands/plugins.rs index a5644c683e..6488acd77d 100644 --- a/src-tauri/src/commands/plugins.rs +++ b/src-tauri/src/commands/plugins.rs @@ -8,7 +8,10 @@ use super::connection::AppState; #[tauri::command] pub async fn list_plugins(state: State<'_, Arc>) -> Result, String> { - state.plugins.list_installed() + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || dbx_core::plugins::PluginRegistry::new(root_dir).list_installed()) + .await + .map_err(|err| err.to_string())? } #[tauri::command] @@ -31,12 +34,18 @@ pub async fn install_jdbc_plugin_local( #[tauri::command] pub async fn uninstall_jdbc_plugin(state: State<'_, Arc>) -> Result { - jdbc::uninstall_jdbc_plugin(state.plugins.root_dir()) + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::uninstall_jdbc_plugin(&root_dir)) + .await + .map_err(|err| err.to_string())? } #[tauri::command] pub async fn list_jdbc_drivers(state: State<'_, Arc>) -> Result, String> { - jdbc::list_jdbc_drivers(state.plugins.root_dir()) + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::list_jdbc_drivers(&root_dir)) + .await + .map_err(|err| err.to_string())? } #[tauri::command] @@ -44,10 +53,16 @@ pub async fn import_jdbc_drivers( state: State<'_, Arc>, paths: Vec, ) -> Result, String> { - jdbc::import_jdbc_drivers(state.plugins.root_dir(), &paths) + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::import_jdbc_drivers(&root_dir, &paths)) + .await + .map_err(|err| err.to_string())? } #[tauri::command] pub async fn delete_jdbc_driver(state: State<'_, Arc>, path: String) -> Result, String> { - jdbc::delete_jdbc_driver(state.plugins.root_dir(), &path) + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::delete_jdbc_driver(&root_dir, &path)) + .await + .map_err(|err| err.to_string())? } diff --git a/src-tauri/src/commands/system_fonts.rs b/src-tauri/src/commands/system_fonts.rs index 3b09678831..051d2acd1e 100644 --- a/src-tauri/src/commands/system_fonts.rs +++ b/src-tauri/src/commands/system_fonts.rs @@ -1,4 +1,6 @@ #[tauri::command] -pub fn list_system_fonts() -> Result, String> { - Ok(dbx_core::jdbc::list_system_fonts()) +pub async fn list_system_fonts() -> Result, String> { + tauri::async_runtime::spawn_blocking(|| Ok(dbx_core::jdbc::list_system_fonts())) + .await + .map_err(|err| err.to_string())? } diff --git a/src-tauri/src/commands/text_export.rs b/src-tauri/src/commands/text_export.rs index 7077f0a348..7e3267ea33 100644 --- a/src-tauri/src/commands/text_export.rs +++ b/src-tauri/src/commands/text_export.rs @@ -11,19 +11,29 @@ pub struct QueryResultTextExportRequest { } impl QueryResultTextExportRequest { - fn data(&self) -> QueryResultTextExportData { - QueryResultTextExportData { columns: self.columns.clone(), rows: self.rows.clone() } + fn into_data(self) -> QueryResultTextExportData { + QueryResultTextExportData { columns: self.columns, rows: self.rows } } } #[tauri::command] -pub fn export_query_result_json(request: QueryResultTextExportRequest) -> Result<(), String> { - let content = format_json(&request.data())?; - std::fs::write(&request.file_path, format!("\u{FEFF}{content}")).map_err(|err| err.to_string()) +pub async fn export_query_result_json(request: QueryResultTextExportRequest) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || { + let file_path = request.file_path.clone(); + let content = format_json(&request.into_data())?; + std::fs::write(file_path, format!("\u{FEFF}{content}")).map_err(|err| err.to_string()) + }) + .await + .map_err(|err| err.to_string())? } #[tauri::command] -pub fn export_query_result_markdown(request: QueryResultTextExportRequest) -> Result<(), String> { - let content = format_markdown(&request.data()); - std::fs::write(&request.file_path, format!("\u{FEFF}{content}")).map_err(|err| err.to_string()) +pub async fn export_query_result_markdown(request: QueryResultTextExportRequest) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || { + let file_path = request.file_path.clone(); + let content = format_markdown(&request.into_data()); + std::fs::write(file_path, format!("\u{FEFF}{content}")).map_err(|err| err.to_string()) + }) + .await + .map_err(|err| err.to_string())? } diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index 768949a019..412099b655 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -10,6 +10,6 @@ pub async fn check_for_updates() -> Result { } #[tauri::command] -pub fn get_system_proxy_url() -> Option { - dbx_core::update::system_proxy_url() +pub async fn get_system_proxy_url() -> Option { + tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.ok().flatten() } diff --git a/src-tauri/src/commands/xlsx_export.rs b/src-tauri/src/commands/xlsx_export.rs index f6ca0cf997..78684f1b2b 100644 --- a/src-tauri/src/commands/xlsx_export.rs +++ b/src-tauri/src/commands/xlsx_export.rs @@ -12,11 +12,15 @@ pub struct QueryResultXlsxExportRequest { } #[tauri::command] -pub fn export_query_result_xlsx(request: QueryResultXlsxExportRequest) -> Result<(), String> { - let workbook = build_xlsx_workbook(&XlsxWorksheetData { - sheet_name: request.sheet_name, - columns: request.columns, - rows: request.rows, - })?; - std::fs::write(&request.file_path, workbook).map_err(|err| err.to_string()) +pub async fn export_query_result_xlsx(request: QueryResultXlsxExportRequest) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || { + let workbook = build_xlsx_workbook(&XlsxWorksheetData { + sheet_name: request.sheet_name, + columns: request.columns, + rows: request.rows, + })?; + std::fs::write(&request.file_path, workbook).map_err(|err| err.to_string()) + }) + .await + .map_err(|err| err.to_string())? }