diff --git a/crates/dbx-core/src/agent_service.rs b/crates/dbx-core/src/agent_service.rs index 7b36e10d18..26283adc83 100644 --- a/crates/dbx-core/src/agent_service.rs +++ b/crates/dbx-core/src/agent_service.rs @@ -1325,7 +1325,7 @@ fn db_type_for_native_offline_entry(registry: &AgentRegistry, platform: &str, na fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> { std::fs::create_dir_all(dest).map_err(|e| e.to_string())?; - let status = std::process::Command::new("tar") + let status = crate::process::new_std_command("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}"))?; diff --git a/crates/dbx-core/src/ai_cli_agent.rs b/crates/dbx-core/src/ai_cli_agent.rs index 9489ac9884..2f76d8617a 100644 --- a/crates/dbx-core/src/ai_cli_agent.rs +++ b/crates/dbx-core/src/ai_cli_agent.rs @@ -8,9 +8,6 @@ use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; use tokio::sync::Notify; -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x08000000; - #[derive(Debug, Clone)] pub struct CliAgentRunOptions { pub connection_id: String, @@ -103,19 +100,7 @@ pub fn model_infos(ids: &[&str]) -> Vec { } pub fn cli_command(program: impl AsRef) -> Command { - let command = Command::new(program); - configure_cli_command(command) -} - -#[cfg(windows)] -fn configure_cli_command(mut command: Command) -> Command { - command.creation_flags(CREATE_NO_WINDOW); - command -} - -#[cfg(not(windows))] -fn configure_cli_command(command: Command) -> Command { - command + crate::process::new_tokio_command(program) } pub async fn list_json_models_or_default( diff --git a/crates/dbx-core/src/ai_codex_cli.rs b/crates/dbx-core/src/ai_codex_cli.rs index fdfbf3ad64..d4a2f782dc 100644 --- a/crates/dbx-core/src/ai_codex_cli.rs +++ b/crates/dbx-core/src/ai_codex_cli.rs @@ -11,7 +11,6 @@ use std::env; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::time::Instant; -use tokio::process::Command; use tokio::sync::Notify; const DEFAULT_CODEX_MODELS: &[&str] = &["default", "gpt-5.5", "gpt-5.4-mini"]; @@ -186,7 +185,7 @@ fn program_path_candidates(dir: &Path, program: &str) -> Vec { #[cfg(not(windows))] async fn shell_program_path(program: &str) -> Option { let script = shell_resolve_script(program); - let mut command = Command::new(user_shell()); + let mut command = cli_command(user_shell()); command.args(user_shell_args(&script)); command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()); let output = command.output().await.ok()?; @@ -204,7 +203,7 @@ async fn shell_program_path(program: &str) -> Option { #[cfg(windows)] async fn shell_program_path(program: &str) -> Option { let script = format!("(Get-Command -All {} -ErrorAction SilentlyContinue).Source", windows_shell_quote(program)); - let mut command = Command::new("powershell.exe"); + let mut command = cli_command("powershell.exe"); command.args(["-NoProfile", "-Command", &script]); command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()); let output = command.output().await.ok()?; diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index cf1450a9dc..d65dc8e032 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -19,8 +19,6 @@ const AGENT_EXIT_DIAGNOSTIC_POLL_MS: u64 = 10; const AGENT_JAVA_OPTS_ENV: &str = "DBX_AGENT_JAVA_OPTS"; const AGENT_JAVA_TOO_OLD_MESSAGE: &str = "Agent requires Java 21, but DBX started it with an older Java runtime. Use DBX managed JRE 21 or select a Java 21 executable in Driver Manager."; -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x08000000; pub struct AgentDriverClient { child: Child, @@ -360,19 +358,13 @@ impl AgentDriverClient { /// they speak the DBX stdin/stdout JSON-RPC protocol. /// Blocks (async) until the agent writes `{"ready":true}` to stdout. pub async fn spawn(launch: AgentLaunchSpec) -> Result { - let mut command = Command::new(&launch.program); + let mut command = crate::process::new_std_command(&launch.program); command.args(&launch.args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()); if let Some(working_dir) = &launch.working_dir { command.current_dir(working_dir); } remove_agent_proxy_env(&mut command); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - command.creation_flags(CREATE_NO_WINDOW); - } - let mut child = command.spawn().map_err(|e| format!("Failed to spawn agent process {}: {e}", launch_display(&launch)))?; diff --git a/crates/dbx-core/src/db/duckdb_worker_process.rs b/crates/dbx-core/src/db/duckdb_worker_process.rs index ae29bd4b85..5a0db9c1db 100644 --- a/crates/dbx-core/src/db/duckdb_worker_process.rs +++ b/crates/dbx-core/src/db/duckdb_worker_process.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex as StdMutex, OnceLock}; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::process::{Child, ChildStdin, Command}; +use tokio::process::{Child, ChildStdin}; use tokio::sync::{oneshot, Mutex, OwnedSemaphorePermit, Semaphore}; use tokio_util::sync::CancellationToken; @@ -389,7 +389,7 @@ impl DuckDbWorkerClient { .map_err(|_| "DuckDB worker process limiter is closed".to_string())?; log::info!("[duckdb-worker:start] executable={}", self.inner.executable.display()); - let mut child = Command::new(&self.inner.executable) + let mut child = crate::process::new_tokio_command(&self.inner.executable) .arg("--duckdb-worker") .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/crates/dbx-core/src/jdbc.rs b/crates/dbx-core/src/jdbc.rs index bb6106a5e2..a63403de1d 100644 --- a/crates/dbx-core/src/jdbc.rs +++ b/crates/dbx-core/src/jdbc.rs @@ -5,7 +5,6 @@ use crate::update::{fetch_latest_release, is_newer_version, JdbcPluginLatest}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::path::{Path, PathBuf}; -use tokio::process::Command; const JDBC_PLUGIN_DOWNLOAD_URL: &str = "https://github.com/t8y2/dbx/releases/latest/download/dbx-jdbc-plugin-latest.zip"; @@ -153,7 +152,7 @@ pub async fn install_jdbc_driver_from_maven( let local_repo = plugin_dir.join("maven-cache"); std::fs::create_dir_all(&local_repo).map_err(|err| err.to_string())?; - let mut command = Command::new(&resolver); + let mut command = crate::process::new_tokio_command(&resolver); env.apply_to(&mut command); command.arg("resolve").arg("--coordinate").arg(&coordinate).arg("--local-repo").arg(&local_repo); for repo in &repositories { diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index 0b7a4d5a10..965ce2ee40 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -34,6 +34,7 @@ pub mod nacos; pub mod object_source_sql; pub mod path_utils; pub mod plugins; +pub mod process; pub mod query; pub mod query_cancel; pub mod query_execution_sql; diff --git a/crates/dbx-core/src/plugins.rs b/crates/dbx-core/src/plugins.rs index 881b521214..d9cef77b30 100644 --- a/crates/dbx-core/src/plugins.rs +++ b/crates/dbx-core/src/plugins.rs @@ -12,8 +12,6 @@ use tokio::sync::Mutex; use tokio::time::timeout; const PLUGIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x08000000; pub const SUPPORTED_PLUGIN_PROTOCOL_VERSION: u32 = 1; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -412,7 +410,7 @@ fn spawn_plugin_child(plugin: &InstalledPlugin, env: &PluginRuntimeEnv) -> Resul .ok_or_else(|| format!("Plugin '{}' does not declare an executable", plugin.manifest.id))?; let executable_path = resolve_plugin_executable(&plugin.path, executable); - let mut command = Command::new(&executable_path); + let mut command = crate::process::new_tokio_command(&executable_path); command .current_dir(&plugin.path) .stdin(Stdio::piped()) @@ -421,13 +419,6 @@ fn spawn_plugin_child(plugin: &InstalledPlugin, env: &PluginRuntimeEnv) -> Resul .kill_on_drop(true); env.apply_to(&mut command); - #[cfg(windows)] - { - #[allow(unused_imports)] - use std::os::windows::process::CommandExt; - command.creation_flags(CREATE_NO_WINDOW); - } - command.spawn().map_err(|err| format!("Failed to start plugin '{}': {err}", plugin.manifest.id)) } diff --git a/crates/dbx-core/src/process.rs b/crates/dbx-core/src/process.rs new file mode 100644 index 0000000000..6df6cf7df5 --- /dev/null +++ b/crates/dbx-core/src/process.rs @@ -0,0 +1,37 @@ +use std::ffi::OsStr; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + +pub fn new_std_command(program: impl AsRef) -> std::process::Command { + let mut command = std::process::Command::new(program); + hide_std_console_window(&mut command); + command +} + +pub fn hide_std_console_window(command: &mut std::process::Command) { + #[cfg(not(windows))] + let _ = command; + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + // DBX is a GUI app; console subprocesses should not flash a window. + command.creation_flags(CREATE_NO_WINDOW); + } +} + +pub fn new_tokio_command(program: impl AsRef) -> tokio::process::Command { + let mut command = tokio::process::Command::new(program); + hide_tokio_console_window(&mut command); + command +} + +pub fn hide_tokio_console_window(command: &mut tokio::process::Command) { + #[cfg(not(windows))] + let _ = command; + #[cfg(windows)] + { + // Keep async child processes consistent with std::process::Command. + command.creation_flags(CREATE_NO_WINDOW); + } +} diff --git a/crates/dbx-core/src/update.rs b/crates/dbx-core/src/update.rs index 9502c16757..7146985296 100644 --- a/crates/dbx-core/src/update.rs +++ b/crates/dbx-core/src/update.rs @@ -113,7 +113,7 @@ pub fn system_proxy_url() -> Option { #[cfg(target_os = "macos")] fn system_proxy_url_from_platform() -> Option { - let output = std::process::Command::new("scutil").arg("--proxy").output().ok()?; + let output = crate::process::new_std_command("scutil").arg("--proxy").output().ok()?; if !output.status.success() { return None; } @@ -123,20 +123,11 @@ fn system_proxy_url_from_platform() -> Option { #[cfg(target_os = "windows")] fn system_proxy_url_from_platform() -> Option { - use std::os::windows::process::CommandExt; - let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings"; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let proxy_enable = std::process::Command::new("reg") - .args(["query", key, "/v", "ProxyEnable"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .ok()?; - let proxy_server = std::process::Command::new("reg") - .args(["query", key, "/v", "ProxyServer"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .ok()?; + let proxy_enable = + crate::process::new_std_command("reg").args(["query", key, "/v", "ProxyEnable"]).output().ok()?; + let proxy_server = + crate::process::new_std_command("reg").args(["query", key, "/v", "ProxyServer"]).output().ok()?; if !proxy_enable.status.success() || !proxy_server.status.success() { return None; } diff --git a/src-tauri/src/commands/fs_open.rs b/src-tauri/src/commands/fs_open.rs index ef1721102d..e23c457b52 100644 --- a/src-tauri/src/commands/fs_open.rs +++ b/src-tauri/src/commands/fs_open.rs @@ -16,7 +16,7 @@ use dbx_core::path_utils::expand_tilde; pub fn reveal_in_file_manager(path: &Path) -> Result<(), String> { #[cfg(target_os = "macos")] { - std::process::Command::new("open") + dbx_core::process::new_std_command("open") .arg("-R") .arg(path) .spawn() @@ -30,7 +30,7 @@ pub fn reveal_in_file_manager(path: &Path) -> Result<(), String> { // after the comma. `Command::arg` does not invoke a shell, so the path // is forwarded as-is — spaces and non-ASCII characters survive. let arg = format!("/select,{}", path.display()); - std::process::Command::new("explorer") + dbx_core::process::new_std_command("explorer") .arg(arg) .spawn() .map(|_| ()) @@ -40,7 +40,7 @@ pub fn reveal_in_file_manager(path: &Path) -> Result<(), String> { #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] { let target: PathBuf = path.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| path.to_path_buf()); - std::process::Command::new("xdg-open") + dbx_core::process::new_std_command("xdg-open") .arg(&target) .spawn() .map(|_| ()) diff --git a/src-tauri/src/commands/keychain.rs b/src-tauri/src/commands/keychain.rs index 57a9baed06..6d99faa045 100644 --- a/src-tauri/src/commands/keychain.rs +++ b/src-tauri/src/commands/keychain.rs @@ -18,7 +18,7 @@ pub async fn read_keychain_password(service: String, account: Option) -> #[cfg(target_os = "macos")] fn read_keychain_password_blocking(service: String, account: Option) -> Result { - let mut cmd = std::process::Command::new("security"); + let mut cmd = dbx_core::process::new_std_command("security"); cmd.args(["find-generic-password", "-s", &service, "-w"]); if let Some(ref acct) = account { cmd.args(["-a", acct]); diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 18f3e15df3..38706d3b57 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -1,7 +1,6 @@ #[cfg(not(windows))] use std::env; use std::path::Path; -use std::process::Command; use std::time::Duration; use serde::{Deserialize, Serialize}; @@ -10,8 +9,6 @@ const MCP_PACKAGE_NAME: &str = "@dbx-app/mcp-server"; const MCP_LATEST_URL: &str = "https://registry.npmjs.org/@dbx-app%2fmcp-server/latest"; const MCP_INSTALL_COMMAND: &str = "npm install -g @dbx-app/mcp-server@latest --registry=https://registry.npmjs.org"; const SHELL_COMMAND_MARKER: &str = "__DBX_MCP_COMMAND_OUTPUT_START__"; -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x08000000; #[derive(Debug, Serialize)] pub struct McpServerStatus { @@ -238,13 +235,8 @@ fn command_output(command: &str, args: &[&str]) -> Result } fn run_command(command: &str, args: &[&str]) -> Result { - let mut cmd = Command::new(command); + let mut cmd = dbx_core::process::new_std_command(command); cmd.args(args); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - cmd.creation_flags(CREATE_NO_WINDOW); - } let output = cmd.output().map_err(|e| e.to_string())?; Ok(CommandOutput { success: output.status.success(), diff --git a/src-tauri/src/commands/saved_sql.rs b/src-tauri/src/commands/saved_sql.rs index 707873bf13..9d99385834 100644 --- a/src-tauri/src/commands/saved_sql.rs +++ b/src-tauri/src/commands/saved_sql.rs @@ -206,21 +206,21 @@ fn unique_file_path(dir: &Path, file_name: &str) -> PathBuf { fn open_path(path: &Path) -> Result<(), String> { #[cfg(target_os = "macos")] let mut command = { - let mut command = std::process::Command::new("open"); + let mut command = dbx_core::process::new_std_command("open"); command.arg(path); command }; #[cfg(target_os = "windows")] let mut command = { - let mut command = std::process::Command::new("explorer"); + let mut command = dbx_core::process::new_std_command("explorer"); command.arg(path); command }; #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] let mut command = { - let mut command = std::process::Command::new("xdg-open"); + let mut command = dbx_core::process::new_std_command("xdg-open"); command.arg(path); command }; diff --git a/src-tauri/src/commands/support_info.rs b/src-tauri/src/commands/support_info.rs index 3f79013a53..f052ee9603 100644 --- a/src-tauri/src/commands/support_info.rs +++ b/src-tauri/src/commands/support_info.rs @@ -127,7 +127,9 @@ fn platform_product_version() -> Option { #[cfg(any(target_os = "macos", target_os = "windows"))] fn command_first_line(command: &str, args: &[&str]) -> Option { - let output = std::process::Command::new(command).args(args).output().ok()?; + let mut process = dbx_core::process::new_std_command(command); + process.args(args); + let output = process.output().ok()?; if !output.status.success() { return None; }