diff --git a/crates/ide-core/src/lib.rs b/crates/ide-core/src/lib.rs index 7113b25e..574f2f89 100644 --- a/crates/ide-core/src/lib.rs +++ b/crates/ide-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod event; pub mod ids; pub mod ignored; pub mod ipc_path; +pub mod process; pub mod project_dir; pub mod settings; pub mod text; diff --git a/crates/ide-core/src/process.rs b/crates/ide-core/src/process.rs new file mode 100644 index 00000000..bcfbadb8 --- /dev/null +++ b/crates/ide-core/src/process.rs @@ -0,0 +1,17 @@ +//! Child-process helpers shared by every crate that spawns tools. + +use std::process::Command; + +/// Keeps a console child (java, node, curl, a language server, ...) from +/// opening its own console window on Windows. Release builds use the GUI +/// subsystem, so without this every spawn flashes a `cmd` window. A no-op +/// elsewhere. For a `tokio::process::Command`, pass `cmd.as_std_mut()`. +pub fn no_window(cmd: &mut Command) -> &mut Command { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} diff --git a/crates/ide-coverage/src/lib.rs b/crates/ide-coverage/src/lib.rs index 48972d60..ccb1c308 100644 --- a/crates/ide-coverage/src/lib.rs +++ b/crates/ide-coverage/src/lib.rs @@ -204,7 +204,7 @@ pub fn collect(spec: &CoverageReportSpec) -> IdeResult { CoverageReportSpec::Exported { runner, argv, cwd, env, lcov } => { let (program, args) = argv.split_first().ok_or_else(|| IdeError::invalid("empty coverage export command"))?; - let out = Command::new(program) + let out = ide_core::process::no_window(&mut Command::new(program)) .args(args) .current_dir(cwd) .envs(env) diff --git a/crates/ide-dap/src/adapter.rs b/crates/ide-dap/src/adapter.rs index 1e75a833..1b3ce2d2 100644 --- a/crates/ide-dap/src/adapter.rs +++ b/crates/ide-dap/src/adapter.rs @@ -233,7 +233,9 @@ pub async fn prepare_debugpy(spec: &mut AdapterSpec) -> IdeResult<()> { let Some(python) = tokio::task::spawn_blocking(move || resolve_binary(&command, &cwd)).await.ok().flatten() else { return Ok(()); // Reported as a missing adapter when it is started. }; - let probe = Command::new(&python) + let mut probe = Command::new(&python); + ide_core::process::no_window(probe.as_std_mut()); + let probe = probe .args(["-c", "import debugpy.adapter"]) .current_dir(&spec.cwd) .stdin(Stdio::null()) @@ -335,6 +337,7 @@ impl AdapterLauncher for ProcessLauncher { None => spec.args.clone(), }; let mut cmd = Command::new(&binary); + ide_core::process::no_window(cmd.as_std_mut()); cmd.args(&args) .envs(spec.env.iter().map(|(k, v)| (k, v))) .current_dir(&spec.cwd) diff --git a/crates/ide-dap/src/cargo.rs b/crates/ide-dap/src/cargo.rs index 6fb6cb8a..a96ca00a 100644 --- a/crates/ide-dap/src/cargo.rs +++ b/crates/ide-dap/src/cargo.rs @@ -55,6 +55,7 @@ pub async fn build( ) -> IdeResult { let cargo = find_binary("cargo", cwd).ok_or_else(|| IdeError::unavailable("`cargo` not found on PATH"))?; let mut cmd = tokio::process::Command::new(cargo); + ide_core::process::no_window(cmd.as_std_mut()); cmd.args(args) .arg("--message-format=json-render-diagnostics") .current_dir(cwd) diff --git a/crates/ide-dap/src/session.rs b/crates/ide-dap/src/session.rs index 80534b96..7fefb1e0 100644 --- a/crates/ide-dap/src/session.rs +++ b/crates/ide-dap/src/session.rs @@ -390,6 +390,7 @@ pub(crate) fn run_in_terminal(s: &Arc, args: &Value) -> Result std::io::Result { use std::process::Stdio; let mut cmd = tokio::process::Command::new(command); + ide_core::process::no_window(cmd.as_std_mut()); cmd.args(args) .current_dir(cwd) .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) diff --git a/crates/ide-lsp/src/launch.rs b/crates/ide-lsp/src/launch.rs index 52ab31b7..728e1083 100644 --- a/crates/ide-lsp/src/launch.rs +++ b/crates/ide-lsp/src/launch.rs @@ -72,7 +72,7 @@ fn resolve_rustup_proxy(command: &str, binary: PathBuf) -> PathBuf { if !is_proxy || command.contains('/') { return binary; } - let works = std::process::Command::new(&binary) + let works = ide_core::process::no_window(&mut std::process::Command::new(&binary)) .arg("--version") .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) diff --git a/crates/ide-profiler/src/lib.rs b/crates/ide-profiler/src/lib.rs index bdee90b4..ae4f7872 100644 --- a/crates/ide-profiler/src/lib.rs +++ b/crates/ide-profiler/src/lib.rs @@ -161,7 +161,7 @@ pub fn collect(spec: &ProfileSpec) -> IdeResult { if !data.is_file() { return Err(nothing_recorded("perf wrote no data")); } - let out = Command::new(perf) + let out = ide_core::process::no_window(&mut Command::new(perf)) .args(["script", "-i"]) .arg(data.as_ref() as &Path) .current_dir(cwd) diff --git a/crates/ide-run/src/wrap.rs b/crates/ide-run/src/wrap.rs index 6a4fea77..d3fe25b9 100644 --- a/crates/ide-run/src/wrap.rs +++ b/crates/ide-run/src/wrap.rs @@ -93,7 +93,7 @@ pub fn find_tool(name: &str, config: &RunConfig, dir: &Path, root: &Path) -> Opt pub fn probe(argv: &[String], dir: &Path, config: &RunConfig) -> bool { let Some((program, args)) = argv.split_first() else { return false }; let program = resolve(program, config); - Command::new(program) + ide_core::process::no_window(&mut Command::new(program)) .args(args) .current_dir(dir) .envs(&config.env) diff --git a/crates/ide-sdk/src/exec.rs b/crates/ide-sdk/src/exec.rs index 50b53e8b..267aa91d 100644 --- a/crates/ide-sdk/src/exec.rs +++ b/crates/ide-sdk/src/exec.rs @@ -44,7 +44,7 @@ pub(crate) fn which(name: &str) -> Option { /// Runs `program args` and returns trimmed stdout (stderr when stdout is empty) /// on success; `None` on failure or after `timeout`. pub(crate) fn query(program: &Path, args: &[&str], timeout: Duration) -> Option { - let mut child = Command::new(program) + let mut child = ide_core::process::no_window(&mut Command::new(program)) .args(args) .env("PATH", search_path()) .stdin(Stdio::null()) @@ -96,6 +96,7 @@ pub(crate) fn stream( on_line: &dyn Fn(String), ) -> IdeResult { let mut cmd = Command::new(program); + ide_core::process::no_window(&mut cmd); cmd.args(args).env("PATH", search_path()).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); cmd.envs(env.iter().map(|(k, v)| (k, v))); if let Some(cwd) = cwd { diff --git a/crates/ide-tools/src/archive.rs b/crates/ide-tools/src/archive.rs index 539bb29f..99dfc57e 100644 --- a/crates/ide-tools/src/archive.rs +++ b/crates/ide-tools/src/archive.rs @@ -113,7 +113,7 @@ fn extract_zip(archive: &Path, dest: &Path) -> IdeResult<()> { /// `.tar.xz` goes through the system `tar` (bsdtar on macOS/Windows, GNU tar + xz on Linux). fn extract_tar_xz(archive: &Path, dest: &Path) -> IdeResult<()> { let tar = which::which("tar").map_err(|_| IdeError::unavailable("`tar` is required to unpack .tar.xz archives"))?; - let out = std::process::Command::new(tar) + let out = ide_core::process::no_window(&mut std::process::Command::new(tar)) .arg("-xJf") .arg(archive) .arg("-C") diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index d09ed65b..1b1ec4a5 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -311,7 +311,7 @@ impl Tools { fn command(&self, program: impl AsRef) -> Command { let mut cmd = Command::new(program); - cmd.env("PATH", &self.child_path); + ide_core::process::no_window(&mut cmd).env("PATH", &self.child_path); cmd } diff --git a/crates/ide-tools/src/process.rs b/crates/ide-tools/src/process.rs index 0ad77520..42a1a9d1 100644 --- a/crates/ide-tools/src/process.rs +++ b/crates/ide-tools/src/process.rs @@ -48,7 +48,7 @@ pub fn run_streamed(cmd: Command, cancel: &CancellationToken, emit: Emit) -> Ide /// Like [`run_streamed`] but returns `(success, output, status text)` instead of failing on a non-zero exit. pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> IdeResult<(bool, String, String)> { emit(InstallEvent::Line { text: format!("$ {}", describe(&cmd)) }); - cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); + ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd.spawn().map_err(|e| IdeError::unavailable(format!("cannot run {}: {e}", describe(&cmd))))?; let (tx, rx) = mpsc::channel::(); let pipes: [Option>; 2] = [ @@ -94,7 +94,7 @@ pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> /// Runs `cmd` to completion within `timeout`; returns (success, stdout + stderr). pub fn run_quiet(mut cmd: Command, timeout: Duration) -> Option<(bool, String)> { - cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); + ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd.spawn().ok()?; let mut stdout = child.stdout.take()?; let mut stderr = child.stderr.take()?; @@ -161,6 +161,7 @@ impl Http { fn base(&self, url: &str) -> Command { let mut cmd = Command::new(&self.curl); + ide_core::process::no_window(&mut cmd); cmd.args(["-fsSL", "--retry", "2", "--connect-timeout", "20", "-A", "openidea"]); if url.starts_with("https://api.github.com/") && let Ok(token) = std::env::var("GITHUB_TOKEN").or_else(|_| std::env::var("GH_TOKEN"))