fix(windows): prevent remaining subprocess console flashes

refs #1468
This commit is contained in:
Ogulcan Celik 2026-07-17 16:03:47 +03:00
parent 8d2af4e046
commit a723dc8852
7 changed files with 34 additions and 14 deletions

View File

@ -8,7 +8,7 @@
### Fixed
- New panes, tabs, layouts, and workspaces using `new_cwd = "follow"` now inherit the foreground process-group leader's working directory instead of an unrelated helper process directory. (#1472)
- Background update checks, captured plugin commands, and workspace Git probes no longer flash console windows on Windows. (#1468)
- Noninteractive update, plugin, integration, sound, custom-command, and Git subprocesses no longer flash console windows on Windows. (#1468)
- Live handoff now preserves installed plugins and no longer lets the next plugin installation overwrite the existing registry. (#893)
- `herdr wait agent-status` now returns `pane_not_found` promptly when its target pane closes instead of waiting for the full timeout. (#1439)

View File

@ -849,7 +849,7 @@ fn git_checkout(
}
fn run_git<const N: usize>(cwd: Option<&Path>, args: [&str; N]) -> std::io::Result<()> {
let mut command = Command::new("git");
let mut command = crate::noninteractive_process::command("git");
command.args(args);
if let Some(cwd) = cwd {
command.current_dir(cwd);
@ -865,7 +865,7 @@ fn run_git<const N: usize>(cwd: Option<&Path>, args: [&str; N]) -> std::io::Resu
}
fn git_output<const N: usize>(cwd: &Path, args: [&str; N]) -> std::io::Result<String> {
let output = Command::new("git")
let output = crate::noninteractive_process::command("git")
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())

View File

@ -47,7 +47,7 @@ pub(crate) fn enforce_agent_version(
requirement: &AgentVersionRequirement,
) -> io::Result<Option<String>> {
let probe = format!("{} {}", requirement.binary, requirement.args.join(" "));
let output = match std::process::Command::new(requirement.binary)
let output = match crate::noninteractive_process::command(requirement.binary)
.args(requirement.args)
.output()
{

View File

@ -26,7 +26,9 @@ pub enum Signal {
}
pub(crate) fn detached_custom_command_process(command: &str) -> std::process::Command {
detached_custom_command_process_platform(command)
let mut process = detached_custom_command_process_platform(command);
configure_background_command(&mut process);
process
}
pub(crate) fn pane_custom_command_pty_builder(command: &str) -> portable_pty::CommandBuilder {

View File

@ -671,7 +671,7 @@ mod tests {
const CONSOLE_TEST_CHILD_ENV: &str = "HERDR_TEST_CONSOLE_CHILD_MODE";
#[test]
fn background_and_server_daemon_commands_do_not_have_consoles() {
fn windows_background_and_server_daemon_commands_do_not_have_consoles() {
if let Some(mode) = std::env::var_os(CONSOLE_TEST_CHILD_ENV) {
assert!(
unsafe { GetConsoleWindow() }.is_null(),
@ -696,7 +696,7 @@ mod tests {
for (mode, configure) in configurations {
let mut child = Command::new(&test_exe);
child
.arg("background_and_server_daemon_commands_do_not_have_consoles")
.arg("windows_background_and_server_daemon_commands_do_not_have_consoles")
.env(CONSOLE_TEST_CHILD_ENV, mode)
.stdin(Stdio::null())
.stdout(Stdio::null())
@ -710,6 +710,22 @@ mod tests {
);
}
let command = format!(
r#""{}" windows_background_and_server_daemon_commands_do_not_have_consoles"#,
test_exe.display()
);
let status = crate::platform::detached_custom_command_process(&command)
.env(CONSOLE_TEST_CHILD_ENV, "detached custom command descendant")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn detached custom command test child");
assert!(
status.success(),
"detached custom command descendant opened or inherited a console"
);
if allocated_console {
unsafe {
FreeConsole();

View File

@ -6,7 +6,9 @@
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
#[cfg(not(windows))]
use std::process::Command;
use std::process::Output;
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::warn;
@ -142,7 +144,7 @@ if (-not $script:done) { throw 'sound playback timed out' }
#[cfg(windows)]
fn run_windows_player(path: &Path) -> Result<Output, String> {
Command::new("powershell.exe")
crate::noninteractive_process::command("powershell.exe")
.args([
"-NoLogo",
"-NoProfile",

View File

@ -199,7 +199,7 @@ pub(crate) fn worktree_dirty_remove_message(path: &Path) -> String {
#[cfg(any(windows, test))]
pub(crate) fn checkout_has_dirty_files(path: &Path) -> Result<bool, String> {
let path_arg = path.display().to_string();
let output = std::process::Command::new("git")
let output = crate::noninteractive_process::command("git")
.args([
"-C",
&path_arg,
@ -265,7 +265,7 @@ pub(crate) fn build_worktree_add_existing_branch_command(
}
pub(crate) fn local_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, String> {
let output = std::process::Command::new("git")
let output = crate::noninteractive_process::command("git")
.arg("-C")
.arg(repo_root)
.args(["show-ref", "--verify", "--quiet"])
@ -306,7 +306,7 @@ pub(crate) fn run_worktree_add_command(
}
pub(crate) fn run_worktree_command(command: &WorktreeCommand) -> Result<(), String> {
let output = std::process::Command::new(&command.program)
let output = crate::noninteractive_process::command(&command.program)
.args(&command.args)
.output()
.map_err(|err| err.to_string())?;
@ -375,7 +375,7 @@ fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> boo
}
fn git_common_worktrees_dir(repo_root: &Path) -> Option<PathBuf> {
let output = std::process::Command::new("git")
let output = crate::noninteractive_process::command("git")
.arg("-C")
.arg(repo_root)
.args(["rev-parse", "--git-common-dir"])
@ -471,7 +471,7 @@ pub(crate) fn parse_worktree_list_porcelain(output: &str) -> Vec<ExistingWorktre
}
pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWorktree>, String> {
let output = std::process::Command::new("git")
let output = crate::noninteractive_process::command("git")
.arg("-C")
.arg(repo_root)
.args(["worktree", "list", "--porcelain"])