From 7df093484e2f13b71f87ec584011d9920a12a7e2 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Fri, 8 May 2026 18:53:10 +0800 Subject: [PATCH 1/3] fix: suppress windows helper command popups --- .../src-tauri/src/installer/commands.rs | 21 +++++++++++++-- .../desktop/src/api/remote_connect_api.rs | 6 +++-- .../tools/browser_control/browser_launcher.rs | 26 +++---------------- .../implementations/computer_use_actions.rs | 26 ++++++++++++------- .../tools/implementations/glob_tool.rs | 18 +++++++++---- .../core/src/service/remote_connect/ngrok.rs | 8 +++--- .../src/service/search/flashgrep/client.rs | 5 ++-- .../service/terminal/src/shell/detection.rs | 12 +++++++++ 8 files changed, 75 insertions(+), 47 deletions(-) diff --git a/BitFun-Installer/src-tauri/src/installer/commands.rs b/BitFun-Installer/src-tauri/src/installer/commands.rs index 0f91d0cd8..01a4ffaeb 100644 --- a/BitFun-Installer/src-tauri/src/installer/commands.rs +++ b/BitFun-Installer/src-tauri/src/installer/commands.rs @@ -30,6 +30,17 @@ const INSTALLER_STATE_FILE: &str = "installer-state.json"; const EMBEDDED_PAYLOAD_ZIP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/embedded_payload.zip")); +#[cfg(target_os = "windows")] +fn create_windows_silent_command>(program: S) -> std::process::Command { + use std::os::windows::process::CommandExt; + + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let mut command = std::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command +} + struct InstallerAppLanguage { code: &'static str, aliases: &'static [&'static str], @@ -566,14 +577,13 @@ exit /b 1 log_path.display() )); - let child = std::process::Command::new("cmd") + let child = create_windows_silent_command("cmd") .arg("/C") .arg("call") .arg(&script_path) .arg(uninstall_exe_path) .arg(&log_path) .current_dir(&temp_dir) - .creation_flags(CREATE_NO_WINDOW) .spawn() .map_err(|e| format!("Failed to schedule uninstall cleanup: {}", e))?; @@ -622,6 +632,13 @@ pub fn launch_application(install_path: String) -> Result<(), String> { PathBuf::from(&install_path).join("bitfun") }; + #[cfg(target_os = "windows")] + create_windows_silent_command(&exe) + .current_dir(&install_path) + .spawn() + .map_err(|e| format!("Failed to launch BitFun: {}", e))?; + + #[cfg(not(target_os = "windows"))] std::process::Command::new(&exe) .current_dir(&install_path) .spawn() diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 4634d11c4..437e7c792 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -8,7 +8,6 @@ use bitfun_core::service::remote_connect::{ use regex::Regex; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use std::process::Command; use std::sync::{Arc, OnceLock}; use tokio::sync::RwLock; @@ -270,7 +269,10 @@ fn detect_default_gateway_ip() -> Option { #[cfg(target_os = "windows")] { - let output = Command::new("route").args(["print", "-4"]).output().ok()?; + let output = bitfun_core::util::process_manager::create_command("route") + .args(["print", "-4"]) + .output() + .ok()?; if !output.status.success() { return None; } diff --git a/src/crates/core/src/agentic/tools/browser_control/browser_launcher.rs b/src/crates/core/src/agentic/tools/browser_control/browser_launcher.rs index 19f658c15..fadc3df73 100644 --- a/src/crates/core/src/agentic/tools/browser_control/browser_launcher.rs +++ b/src/crates/core/src/agentic/tools/browser_control/browser_launcher.rs @@ -1,6 +1,6 @@ //! Detect and launch the user's default browser with CDP debug port enabled. -use crate::util::errors::{BitFunError, BitFunResult}; +use crate::util::{errors::{BitFunError, BitFunResult}, process_manager}; #[allow(unused_imports)] use log::{debug, info}; use serde::{Deserialize, Serialize}; @@ -10,28 +10,10 @@ use std::time::Duration; /// Default CDP debug port. pub const DEFAULT_CDP_PORT: u16 = 9222; -/// Windows: suppress the brief console window that flashes when a GUI process -/// spawns a console subprocess (e.g. `reg.exe`, `tasklist.exe`). -/// Equivalent to passing `CREATE_NO_WINDOW` (0x08000000) to CreateProcess. -#[cfg(target_os = "windows")] -const CREATE_NO_WINDOW: u32 = 0x0800_0000; - -/// Build a `Command` that on Windows is configured with `CREATE_NO_WINDOW` -/// so no console window briefly pops up. On other platforms behaves like -/// `Command::new`. +/// Build a `Command` that suppresses transient Windows console windows while +/// preserving normal process behavior on other platforms. fn silent_command>(program: S) -> Command { - let cmd = Command::new(program); - #[cfg(target_os = "windows")] - { - use std::os::windows::process::CommandExt; - let mut cmd = cmd; - cmd.creation_flags(CREATE_NO_WINDOW); - cmd - } - #[cfg(not(target_os = "windows"))] - { - cmd - } + process_manager::create_command(program) } /// Known browser identifiers and their executable paths per platform. diff --git a/src/crates/core/src/agentic/tools/implementations/computer_use_actions.rs b/src/crates/core/src/agentic/tools/implementations/computer_use_actions.rs index 38bdf841a..ff734069c 100644 --- a/src/crates/core/src/agentic/tools/implementations/computer_use_actions.rs +++ b/src/crates/core/src/agentic/tools/implementations/computer_use_actions.rs @@ -12,6 +12,7 @@ use crate::agentic::tools::computer_use_host::{ use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; use crate::util::elapsed_ms_u64; use crate::util::errors::{BitFunError, BitFunResult}; +use crate::util::process_manager; use serde_json::{json, Value}; use super::control_hub::{err_response, ControlHubError, ErrorCode}; @@ -1643,7 +1644,7 @@ impl ComputerUseActions { // the still-running child, leaking a thread + process per // hung script. let started = std::time::Instant::now(); - let child = tokio::process::Command::new(&program) + let child = process_manager::create_tokio_command(&program) .args(&args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -1910,7 +1911,7 @@ impl ComputerUseActions { ), _ => ("xdg-open".to_string(), vec![url.to_string()]), }; - let status = std::process::Command::new(&program) + let status = process_manager::create_command(&program) .args(&args) .status() .map_err(|e| { @@ -1973,7 +1974,7 @@ impl ComputerUseActions { ), _ => ("xdg-open".to_string(), vec![path_str.to_string()]), }; - let status = std::process::Command::new(&program) + let status = process_manager::create_command(&program) .args(&args) .status() .map_err(|e| { @@ -2111,7 +2112,7 @@ fn hostname() -> std::io::Result { } } } - let out = std::process::Command::new("hostname").output()?; + let out = process_manager::create_command("hostname").output()?; Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) } @@ -2225,7 +2226,7 @@ pub(crate) fn linux_session_info() -> (Option, Option) { async fn clipboard_read() -> Result { #[cfg(target_os = "macos")] { - let out = tokio::process::Command::new("pbpaste") + let out = process_manager::create_tokio_command("pbpaste") .output() .await .map_err(|e| format!("spawn pbpaste: {}", e))?; @@ -2236,11 +2237,12 @@ async fn clipboard_read() -> Result { } #[cfg(target_os = "windows")] { - let out = tokio::process::Command::new("powershell") - .args(["-NoProfile", "-Command", "Get-Clipboard -Raw"]) + let (program, args) = powershell_invocation("Get-Clipboard -Raw"); + let out = process_manager::create_tokio_command(&program) + .args(&args) .output() .await - .map_err(|e| format!("spawn powershell: {}", e))?; + .map_err(|e| format!("spawn {}: {}", program, e))?; if !out.status.success() { return Err(format!("Get-Clipboard exit={:?}", out.status.code())); } @@ -2271,7 +2273,11 @@ async fn clipboard_read() -> Result { ] }; for (bin, args) in candidates { - if let Ok(out) = tokio::process::Command::new(bin).args(*args).output().await { + if let Ok(out) = process_manager::create_tokio_command(bin) + .args(*args) + .output() + .await + { if out.status.success() { return Ok(String::from_utf8_lossy(&out.stdout).to_string()); } @@ -2292,7 +2298,7 @@ async fn clipboard_write(text: &str) -> Result<(), String> { use tokio::io::AsyncWriteExt; async fn pipe(bin: &str, args: &[&str], text: &str) -> Result<(), String> { - let mut child = tokio::process::Command::new(bin) + let mut child = process_manager::create_tokio_command(bin) .args(args) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()) diff --git a/src/crates/core/src/agentic/tools/implementations/glob_tool.rs b/src/crates/core/src/agentic/tools/implementations/glob_tool.rs index f30f19202..51bc6e0a7 100644 --- a/src/crates/core/src/agentic/tools/implementations/glob_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/glob_tool.rs @@ -2,6 +2,7 @@ use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; use crate::service::search::{ get_global_workspace_search_service, workspace_search_runtime_available, GlobSearchRequest, }; +use crate::util::process_manager; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; use globset::{GlobBuilder, GlobMatcher}; @@ -11,7 +12,6 @@ use serde_json::{json, Value}; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::path::{Component, Path, PathBuf}; -use std::process::Command; fn extract_glob_base_directory(pattern: &str) -> (String, String) { let glob_start = pattern.find(['*', '?', '[', '{']); @@ -242,7 +242,7 @@ fn call_rg(search_path: &str, pattern: &str, limit: usize) -> Result } let args = build_rg_args(&relative_pattern, apply_gitignore, ignore_hidden_files); - let output = Command::new("rg") + let output = process_manager::create_command("rg") .current_dir(&walk_root) .args(&args) .arg(".") @@ -652,9 +652,9 @@ impl Tool for GlobTool { #[cfg(test)] mod tests { use super::{call_rg, derive_walk_root, extract_glob_base_directory}; + use crate::util::process_manager; use std::fs; use std::path::PathBuf; - use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; fn make_temp_dir(name: &str) -> PathBuf { @@ -694,7 +694,11 @@ mod tests { #[test] fn keeps_shallowest_matches_from_rg_results() { - if Command::new("rg").arg("--version").output().is_err() { + if process_manager::create_command("rg") + .arg("--version") + .output() + .is_err() + { return; } @@ -720,7 +724,11 @@ mod tests { #[test] fn wildcard_search_now_returns_files_only() { - if Command::new("rg").arg("--version").output().is_err() { + if process_manager::create_command("rg") + .arg("--version") + .output() + .is_err() + { return; } diff --git a/src/crates/core/src/service/remote_connect/ngrok.rs b/src/crates/core/src/service/remote_connect/ngrok.rs index 9966ac23c..0afe16df5 100644 --- a/src/crates/core/src/service/remote_connect/ngrok.rs +++ b/src/crates/core/src/service/remote_connect/ngrok.rs @@ -2,13 +2,13 @@ //! //! Supports macOS (pgrep) and Windows (tasklist) for process detection. +use crate::util::process_manager; use anyhow::{anyhow, Result}; use log::{info, warn}; use std::path::PathBuf; use std::process::Stdio; use std::sync::atomic::{AtomicU32, Ordering}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; /// Tracks the PID of the ngrok process we started, so it can be killed /// synchronously during application exit even if async cleanup didn't run. @@ -85,7 +85,7 @@ fn list_ngrok_pids() -> Vec { #[cfg(windows)] fn list_ngrok_pids() -> Vec { - std::process::Command::new("tasklist") + process_manager::create_command("tasklist") .args(["/FI", "IMAGENAME eq ngrok.exe", "/FO", "CSV", "/NH"]) .output() .ok() @@ -138,7 +138,7 @@ pub async fn start_ngrok_tunnel(local_port: u16) -> Result { info!("Using ngrok at: {}", ngrok_path.display()); - let mut child = Command::new(&ngrok_path) + let mut child = process_manager::create_tokio_command(&ngrok_path) .args([ "http", &local_port.to_string(), @@ -236,7 +236,7 @@ fn kill_process(pid: u32) { #[cfg(windows)] fn kill_process(pid: u32) { - let _ = std::process::Command::new("taskkill") + let _ = process_manager::create_command("taskkill") .args(["/F", "/PID", &pid.to_string()]) .output(); } diff --git a/src/crates/core/src/service/search/flashgrep/client.rs b/src/crates/core/src/service/search/flashgrep/client.rs index 724a43b08..6f11a7b84 100644 --- a/src/crates/core/src/service/search/flashgrep/client.rs +++ b/src/crates/core/src/service/search/flashgrep/client.rs @@ -9,10 +9,11 @@ use std::{ time::{Duration, Instant}, }; +use crate::util::process_manager; use serde::Serialize; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, - process::{Child, ChildStderr, ChildStdin, ChildStdout, Command}, + process::{Child, ChildStderr, ChildStdin, ChildStdout}, sync::{oneshot, Mutex}, time::{sleep, timeout}, }; @@ -395,7 +396,7 @@ impl AsyncDaemonClient { .or_else(|| std::env::var_os("FLASHGREP_DAEMON_BIN")) .unwrap_or_else(|| OsString::from("flashgrep")); - let mut command = Command::new(program); + let mut command = process_manager::create_tokio_command(program); command .arg("serve") .arg("--stdio") diff --git a/src/crates/core/src/service/terminal/src/shell/detection.rs b/src/crates/core/src/service/terminal/src/shell/detection.rs index 44125b5d4..c2fdb16bb 100644 --- a/src/crates/core/src/service/terminal/src/shell/detection.rs +++ b/src/crates/core/src/service/terminal/src/shell/detection.rs @@ -331,6 +331,18 @@ impl ShellDetector { #[allow(dead_code)] fn get_shell_version(path: &str) -> Option { + #[cfg(windows)] + let output = { + use std::os::windows::process::CommandExt; + + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + + let mut command = std::process::Command::new(path); + command.creation_flags(CREATE_NO_WINDOW); + command.arg("--version").output().ok()? + }; + + #[cfg(not(windows))] let output = std::process::Command::new(path) .arg("--version") .output() From 53420c2e1b93ab6f101a2fe70a246f3c4cf69d70 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Fri, 8 May 2026 19:12:24 +0800 Subject: [PATCH 2/3] fix: restore remote connect command import on unix --- src/apps/desktop/src/api/remote_connect_api.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 437e7c792..94921cf69 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -11,6 +11,9 @@ use std::path::PathBuf; use std::sync::{Arc, OnceLock}; use tokio::sync::RwLock; +#[cfg(any(target_os = "macos", target_os = "linux"))] +use std::process::Command; + static REMOTE_CONNECT_SERVICE: OnceLock>>> = OnceLock::new(); From ace5bd25e15e977319882d57d745f0bda929a249 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Fri, 8 May 2026 19:15:42 +0800 Subject: [PATCH 3/3] refactor: use process wrapper for remote connect gateway detection --- src/apps/desktop/src/api/remote_connect_api.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 94921cf69..5af571179 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -11,9 +11,6 @@ use std::path::PathBuf; use std::sync::{Arc, OnceLock}; use tokio::sync::RwLock; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use std::process::Command; - static REMOTE_CONNECT_SERVICE: OnceLock>>> = OnceLock::new(); @@ -240,7 +237,7 @@ pub struct LanNetworkInfo { fn detect_default_gateway_ip() -> Option { #[cfg(target_os = "macos")] { - let output = Command::new("route") + let output = bitfun_core::util::process_manager::create_command("route") .args(["-n", "get", "default"]) .output() .ok()?; @@ -256,7 +253,7 @@ fn detect_default_gateway_ip() -> Option { #[cfg(target_os = "linux")] { - let output = Command::new("ip") + let output = bitfun_core::util::process_manager::create_command("ip") .args(["route", "show", "default"]) .output() .ok()?;