fix(app): Stop console tools from opening cmd windows on Windows (#20)

Release builds use the GUI subsystem, so a console child spawned without
CREATE_NO_WINDOW gets its own console window. git, gh, docker, builds and
run configurations already set the flag; language servers, SDK and tool
probes, debug adapters, tool downloads, coverage export and perf did not.
Add ide_core::process::no_window and use it at every remaining spawn.

Fixes #16
This commit is contained in:
l0ng-ai
2026-10-24 16:31:50 +08:00
committed by GitHub
parent 5c0c3a009c
commit bb9486c758
14 changed files with 36 additions and 10 deletions
+1
View File
@@ -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;
+17
View File
@@ -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
}
+1 -1
View File
@@ -204,7 +204,7 @@ pub fn collect(spec: &CoverageReportSpec) -> IdeResult<CoverageSuite> {
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)
+4 -1
View File
@@ -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)
+1
View File
@@ -55,6 +55,7 @@ pub async fn build(
) -> IdeResult<PathBuf> {
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)
+1
View File
@@ -390,6 +390,7 @@ pub(crate) fn run_in_terminal(s: &Arc<Session>, args: &Value) -> Result<Value, S
return Err("runInTerminal without args".into());
};
let mut cmd = tokio::process::Command::new(program);
ide_core::process::no_window(cmd.as_std_mut());
cmd.args(rest).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
if let Some(cwd) = args.get("cwd").and_then(Value::as_str).filter(|c| !c.is_empty()) {
cmd.current_dir(cwd);
+1
View File
@@ -241,6 +241,7 @@ impl Peer {
) -> std::io::Result<Self> {
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())))
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -161,7 +161,7 @@ pub fn collect(spec: &ProfileSpec) -> IdeResult<Profile> {
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)
+1 -1
View File
@@ -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)
+2 -1
View File
@@ -44,7 +44,7 @@ pub(crate) fn which(name: &str) -> Option<PathBuf> {
/// 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<String> {
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<String> {
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 {
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -311,7 +311,7 @@ impl Tools {
fn command(&self, program: impl AsRef<std::ffi::OsStr>) -> 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
}
+3 -2
View File
@@ -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::<String>();
let pipes: [Option<Box<dyn Read + Send>>; 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"))