Preserve ordered SSH and proxy connection layers (#802)

* Preserve ordered SSH and proxy connection layers

Replace separate SSH/proxy connection fields with ordered transport_layers while keeping legacy migration and secret fallback paths intact. The UI now edits SSH tunnel/proxy layers in configured order without a global SSH enable gate.

Constraint: Existing saved SSH tunnels, proxy settings, and secret-store keys must continue to load through migration.
Rejected: Folding proxy fields into SshTunnelConfig | mixes proxy semantics into an SSH-specific structure.
Confidence: high
Scope-risk: moderate
Directive: Keep SSH/proxy structs provider-specific; put cross-layer chaining in transport_layer_tunnel orchestration.
Tested: git diff --check; cargo fmt --check; cargo check --workspace; cargo test -p dbx-core --lib; vue-tsc --noEmit --project apps/desktop/tsconfig.json; tsc -p packages/node-core/tsconfig.json --noEmit; oxlint --vue-plugin apps/desktop/src; tsx --tsconfig apps/desktop/tsconfig.json --test packages/app-tests/*.test.ts; tsx --test packages/node-core/tests/*.test.ts
Not-tested: Live external SSH/proxy/database endpoint integration.

* Keep ordered transport changes CI-format clean

Constraint: CI pnpm check failed only on oxfmt formatting for two desktop TypeScript files
Confidence: high
Scope-risk: narrow
Directive: Keep generated/editor config changes out of this PR fix commit
Tested: PATH="/Volumes/data/code/rust/dbx/node_modules/.bin:/Users/hekx/.codex/tmp/arg0/codex-arg0nuL34n:/Users/hekx/.cargo/bin:/Users/hekx/.local/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/libpq/bin:/Volumes/data/Users/hekx/.opencode/bin:/Users/hekx/.bun/bin:/Volumes/data/sdks/flutter/bin:/Volumes/data/Users/hekx/.cargo/bin:/Users/hekx/.local/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/libpq/bin:/Volumes/data/Users/hekx/.opencode/bin:/Users/hekx/.bun/bin:/Volumes/data/Users/hekx/Library/pnpm:/Volumes/data/sdks/flutter/bin:/Volumes/data/Users/hekx/.cargo/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/opt/X11/bin:/Library/Apple/usr/bin:/Applications/Wireshark.app/Contents/MacOS:/usr/local/go/bin:/opt/homebrew/bin:/opt/podman/bin:/Applications/Ghostty.app/Contents/MacOS" rtk node scripts/run-check.mjs
Not-tested: GitHub Actions rerun not observed locally

* Keep Rust checks warning-clean

Constraint: cargo clippy --workspace --all-targets --all-features -- -D warnings failed across core, web, and tauri crates
Rejected: Broad workspace-level clippy suppression | kept allows local to long-argument command/API boundaries
Confidence: high
Scope-risk: moderate
Directive: Preserve src-tauri/tauri.conf.json as an unrelated local change outside this commit
Tested: rtk cargo clippy --workspace --all-targets --all-features -- -D warnings
Not-tested: Full GitHub Actions rerun not observed locally

* Reuse existing proxy tunnels on retry

Proxy tunnel startup now mirrors SSH tunnel behavior by returning the existing local port for an active connection id instead of replacing the managed handle. A second map check aborts a just-spawned duplicate handle if a concurrent retry won the race before insertion, preventing orphaned listeners while keeping the change narrow.

Constraint: Reviewer requested proxy tunnel behavior align with SSH local-port reuse

Rejected: Always overwrite and abort the previous handle | less consistent with SSH behavior and churns listeners during retries

Confidence: high

Scope-risk: narrow

Tested: cargo fmt --check --all

Tested: cargo test -p dbx-core db::proxy_tunnel::tests::start_tunnel_reuses_existing_local_port

Tested: cargo clippy -p dbx-core --all-targets -- -D warnings

---------

Co-authored-by: hekx <hekx@momandeMac-mini.local>
Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
This commit is contained in:
authored and GitHub committed 2026-06-06 23:22:29 +08:00
1 parent 0c3572d779
commit e9ee982843
54 files changed
+1869 -1188

No files matched your search

+7 -25
View File
@@ -152,7 +152,7 @@ async fn connect_agent_pool(
#[cfg(test)]
mod tests {
use super::mongo_legacy_connect_params;
use dbx_core::models::connection::{ConnectionConfig, DatabaseType, ProxyType};
use dbx_core::models::connection::{ConnectionConfig, DatabaseType};
fn mongodb_config() -> ConnectionConfig {
ConnectionConfig {
@@ -170,24 +170,9 @@ mod tests {
visible_databases: None,
attached_databases: Vec::new(),
color: None,
ssh_enabled: false,
ssh_host: String::new(),
ssh_port: 22,
ssh_user: String::new(),
ssh_password: String::new(),
ssh_key_path: String::new(),
ssh_key_passphrase: String::new(),
ssh_expose_lan: false,
ssh_connect_timeout_secs: dbx_core::models::connection::default_ssh_connect_timeout_secs(),
ssh_tunnels: Vec::new(),
transport_layers: Vec::new(),
connect_timeout_secs: dbx_core::models::connection::default_connect_timeout_secs(),
query_timeout_secs: dbx_core::models::connection::default_query_timeout_secs(),
proxy_enabled: false,
proxy_type: ProxyType::Socks5,
proxy_host: String::new(),
proxy_port: 1080,
proxy_username: String::new(),
proxy_password: String::new(),
ssl: false,
ca_cert_path: String::new(),
sysdba: false,
@@ -252,8 +237,8 @@ pub async fn load_sidebar_layout(state: State<'_, Arc<AppState>>) -> Result<Opti
#[tauri::command]
pub async fn test_connection(state: State<'_, Arc<AppState>>, config: ConnectionConfig) -> Result<String, String> {
let tunnel_id = format!("{}:test", config.id);
let connection_id =
if config.ssh_enabled && !config.ssh_host.is_empty() { tunnel_id.as_str() } else { config.id.as_str() };
let has_transport_layers = config.has_effective_transport_layers();
let connection_id = if has_transport_layers { tunnel_id.as_str() } else { config.id.as_str() };
let (host, port) = state.connection_host_port(connection_id, &config).await?;
let probe_result = probe_connection_endpoint(&config, &host, port).await;
let url = connection_url_for_endpoint(&config, &host, port);
@@ -426,11 +411,8 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
},
};
if config.ssh_enabled && !config.ssh_host.is_empty() {
state.tunnels.stop_tunnel(&tunnel_id).await;
}
if config.proxy_enabled && !config.proxy_host.is_empty() {
state.proxy_tunnels.stop_tunnel(&tunnel_id).await;
if has_transport_layers {
state.reset_connection_transport_for_config(&tunnel_id, &config).await;
}
result
@@ -443,7 +425,7 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
let db_config = metadata_connection_config(&config);
state.remove_connection_pools(&id).await;
state.reset_connection_transport(&id).await;
state.reset_connection_transport_for_config(&id, &db_config).await;
let (host, port) = state.connection_host_port(&id, &db_config).await?;
probe_connection_endpoint(&db_config, &host, port).await?;
+1
View File
@@ -22,6 +22,7 @@ pub async fn mongo_list_collections(
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn mongo_find_documents(
state: State<'_, Arc<AppState>>,
connection_id: String,
+7 -7
View File
@@ -13,12 +13,12 @@ pub async fn list_plugins(state: State<'_, Arc<AppState>>) -> Result<Vec<Install
#[tauri::command]
pub async fn jdbc_plugin_status(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
jdbc::get_jdbc_plugin_status(&state.plugins.root_dir()).await
jdbc::get_jdbc_plugin_status(state.plugins.root_dir()).await
}
#[tauri::command]
pub async fn install_jdbc_plugin(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
jdbc::install_jdbc_plugin(&state.plugins.root_dir()).await
jdbc::install_jdbc_plugin(state.plugins.root_dir()).await
}
#[tauri::command]
@@ -26,17 +26,17 @@ pub async fn install_jdbc_plugin_local(
state: State<'_, Arc<AppState>>,
path: String,
) -> Result<JdbcPluginStatus, String> {
jdbc::install_jdbc_plugin_from_file(&state.plugins.root_dir(), &path).await
jdbc::install_jdbc_plugin_from_file(state.plugins.root_dir(), &path).await
}
#[tauri::command]
pub async fn uninstall_jdbc_plugin(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
jdbc::uninstall_jdbc_plugin(&state.plugins.root_dir())
jdbc::uninstall_jdbc_plugin(state.plugins.root_dir())
}
#[tauri::command]
pub async fn list_jdbc_drivers(state: State<'_, Arc<AppState>>) -> Result<Vec<JdbcDriverInfo>, String> {
jdbc::list_jdbc_drivers(&state.plugins.root_dir())
jdbc::list_jdbc_drivers(state.plugins.root_dir())
}
#[tauri::command]
@@ -44,10 +44,10 @@ pub async fn import_jdbc_drivers(
state: State<'_, Arc<AppState>>,
paths: Vec<String>,
) -> Result<Vec<JdbcDriverInfo>, String> {
jdbc::import_jdbc_drivers(&state.plugins.root_dir(), &paths)
jdbc::import_jdbc_drivers(state.plugins.root_dir(), &paths)
}
#[tauri::command]
pub async fn delete_jdbc_driver(state: State<'_, Arc<AppState>>, path: String) -> Result<Vec<JdbcDriverInfo>, String> {
jdbc::delete_jdbc_driver(&state.plugins.root_dir(), &path)
jdbc::delete_jdbc_driver(state.plugins.root_dir(), &path)
}
+2
View File
@@ -8,6 +8,7 @@ use dbx_core::models::connection::DatabaseType;
use dbx_core::sql::split_sql_statements;
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn execute_query(
state: State<'_, Arc<AppState>>,
connection_id: String,
@@ -47,6 +48,7 @@ pub async fn execute_query(
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn execute_multi(
state: State<'_, Arc<AppState>>,
connection_id: String,
+10 -6
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Arc, OnceLock};
use std::time::Instant;
use tauri::{AppHandle, Emitter, State};
@@ -12,8 +12,11 @@ use dbx_core::sql_file_import::{execute_sql_file_content, sql_file_error_progres
pub use dbx_core::sql::{decode_sql_file_bytes, SqlFilePreview, SqlFileRequest, SqlFileStatus};
static SQL_FILE_EXECUTIONS: std::sync::LazyLock<RwLock<HashMap<String, CancellationToken>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static SQL_FILE_EXECUTIONS: OnceLock<RwLock<HashMap<String, CancellationToken>>> = OnceLock::new();
fn sql_file_executions() -> &'static RwLock<HashMap<String, CancellationToken>> {
SQL_FILE_EXECUTIONS.get_or_init(|| RwLock::new(HashMap::new()))
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -47,7 +50,7 @@ pub async fn execute_sql_file(
) -> Result<(), String> {
let token = CancellationToken::new();
{
let mut executions = SQL_FILE_EXECUTIONS.write().await;
let mut executions = sql_file_executions().write().await;
register_sql_file_execution(&mut executions, request.execution_id.clone(), token.clone())?;
}
@@ -56,7 +59,7 @@ pub async fn execute_sql_file(
let result = execute_sql_file_inner(&app, &state, &request, token, started_at).await;
{
let mut executions = SQL_FILE_EXECUTIONS.write().await;
let mut executions = sql_file_executions().write().await;
remove_sql_file_execution(&mut executions, &request.execution_id);
}
result
@@ -64,7 +67,7 @@ pub async fn execute_sql_file(
#[tauri::command]
pub async fn cancel_sql_file_execution(execution_id: String) -> Result<bool, String> {
let executions = SQL_FILE_EXECUTIONS.read().await;
let executions = sql_file_executions().read().await;
if let Some(token) = executions.get(&execution_id) {
token.cancel();
Ok(true)
@@ -119,6 +122,7 @@ fn remove_sql_file_execution(executions: &mut HashMap<String, CancellationToken>
executions.remove(execution_id);
}
#[allow(clippy::too_many_arguments)]
fn emit_progress(
app: &AppHandle,
execution_id: &str,
+9 -6
View File
@@ -1,5 +1,5 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::{Arc, OnceLock};
use tauri::{AppHandle, Emitter, State};
use tokio::sync::RwLock;
@@ -10,19 +10,22 @@ use crate::commands::transfer::get_db_type;
// Re-export types for backward compatibility
pub use dbx_core::table_import::{TableImportPreview, TableImportProgress, TableImportRequest, TableImportSummary};
static CANCELLED_IMPORTS: std::sync::LazyLock<RwLock<HashSet<String>>> =
std::sync::LazyLock::new(|| RwLock::new(HashSet::new()));
static CANCELLED_IMPORTS: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
fn cancelled_imports() -> &'static RwLock<HashSet<String>> {
CANCELLED_IMPORTS.get_or_init(|| RwLock::new(HashSet::new()))
}
fn emit_progress(app: &AppHandle, progress: TableImportProgress) {
let _ = app.emit("table-import-progress", progress);
}
async fn is_cancelled(import_id: &str) -> bool {
CANCELLED_IMPORTS.read().await.contains(import_id)
cancelled_imports().read().await.contains(import_id)
}
async fn clear_cancelled(import_id: &str) {
CANCELLED_IMPORTS.write().await.remove(import_id);
cancelled_imports().write().await.remove(import_id);
}
#[tauri::command]
@@ -60,6 +63,6 @@ pub async fn import_table_file(
#[tauri::command]
pub async fn cancel_table_import(import_id: String) -> Result<bool, String> {
CANCELLED_IMPORTS.write().await.insert(import_id);
cancelled_imports().write().await.insert(import_id);
Ok(true)
}