Skip to content

Commit 3e27816

Browse files
committed
fix(windows): hide remaining child consoles
Route non-interactive CLI, dispatch, service, adapter, and test processes through the shared process factories so Windows release and headless hosts do not flash console windows. Apply explicit CREATE_NO_WINDOW where lower-level test helpers cannot depend on process-runtime, and set windowsHide for the built-in Graphviz renderer. Document the repository-wide rule for future call sites.
1 parent 514f488 commit 3e27816

36 files changed

Lines changed: 280 additions & 215 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ await api.invoke('your_command', { request: { ... } });
152152
- Desktop-only host adapters belong in `src/apps/desktop`, then flow through typed capability interfaces and, when event delivery is needed, the production transport adapter.
153153
- In shared core, avoid host-specific APIs such as `tauri::AppHandle`; use shared abstractions such as `bitfun_events::EventEmitter`.
154154

155-
#### Child processes in GUI hosts
155+
#### Non-interactive child processes
156156

157-
- GUI hosts such as Desktop and Installer must not spawn child processes with bare
158-
`std::process::Command` or `tokio::process::Command`. Prefer
157+
- Any non-interactive child process that may run under a GUI, headless,
158+
background, or redirected host must not use bare `std::process::Command` or
159+
`tokio::process::Command`, including CLI modes that another host can invoke.
160+
Prefer
159161
`bitfun_services_core::process_manager::{create_command, create_tokio_command}`
160162
or the existing facade for that layer. If a direct command is unavoidable,
161163
Windows code must explicitly apply `CREATE_NO_WINDOW`; Node child processes

src/apps/cli/src/dispatch/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ mod worker;
66
mod workspace;
77

88
use std::path::{Path, PathBuf};
9-
use std::process::Command;
109

1110
use anyhow::{anyhow, bail, Context, Result};
1211
use bitfun_core::infrastructure::ai::AIClientFactory;
@@ -853,7 +852,7 @@ fn classify_repository_probe(result: Result<GitProbeOutput, GitProbeOutput>) ->
853852
/// below matches Git's English prose, and a localized host would otherwise make
854853
/// an ownership rejection unrecognizable.
855854
fn git_probe(workspace: &Path, args: &[&str]) -> Result<GitProbeOutput, GitProbeOutput> {
856-
let output = Command::new("git")
855+
let output = bitfun_services_core::process_manager::create_command("git")
857856
.env("LC_ALL", "C")
858857
.arg("-C")
859858
.arg(workspace)

src/apps/cli/src/dispatch/store.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2916,14 +2916,14 @@ mod tests {
29162916
let executable = std::env::current_exe().expect("test executable");
29172917
let test_name =
29182918
"dispatch::store::tests::cross_process_reader_and_writer_remain_consistent_during_rotation";
2919-
let mut reader = std::process::Command::new(&executable)
2919+
let mut reader = bitfun_services_core::process_manager::create_command(&executable)
29202920
.args(["--exact", test_name, "--nocapture"])
29212921
.env(MODE_ENV, "reader")
29222922
.env(ROOT_ENV, &root)
29232923
.env(DONE_ENV, &done)
29242924
.spawn()
29252925
.expect("spawn stress reader");
2926-
let writer = std::process::Command::new(&executable)
2926+
let writer = bitfun_services_core::process_manager::create_command(&executable)
29272927
.args(["--exact", test_name, "--nocapture"])
29282928
.env(MODE_ENV, "writer")
29292929
.env(ROOT_ENV, &root)
@@ -3076,20 +3076,22 @@ mod tests {
30763076
let dir = tempfile::tempdir().expect("tempdir");
30773077
let bitfun_home = dir.path().join("bitfun-home");
30783078
let user_root = dir.path().join("user-root");
3079-
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
3080-
.args([
3081-
"--exact",
3082-
"dispatch::store::tests::default_store_honors_path_manager_storage_overrides",
3083-
"--nocapture",
3084-
])
3085-
.env(CHILD_ENV, &bitfun_home)
3086-
.env("BITFUN_HOME", &bitfun_home)
3087-
.env("BITFUN_USER_ROOT", &user_root)
3088-
.env("BITFUN_E2E_STORAGE_GUARD", "1")
3089-
.env_remove("BITFUN_E2E_HOME")
3090-
.env_remove("BITFUN_E2E_USER_ROOT")
3091-
.output()
3092-
.expect("run isolated path test");
3079+
let output = bitfun_services_core::process_manager::create_command(
3080+
std::env::current_exe().expect("test executable"),
3081+
)
3082+
.args([
3083+
"--exact",
3084+
"dispatch::store::tests::default_store_honors_path_manager_storage_overrides",
3085+
"--nocapture",
3086+
])
3087+
.env(CHILD_ENV, &bitfun_home)
3088+
.env("BITFUN_HOME", &bitfun_home)
3089+
.env("BITFUN_USER_ROOT", &user_root)
3090+
.env("BITFUN_E2E_STORAGE_GUARD", "1")
3091+
.env_remove("BITFUN_E2E_HOME")
3092+
.env_remove("BITFUN_E2E_USER_ROOT")
3093+
.output()
3094+
.expect("run isolated path test");
30933095
assert!(
30943096
output.status.success(),
30953097
"isolated child failed:\nstdout:\n{}\nstderr:\n{}",

src/apps/cli/src/dispatch/workspace.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,7 +1573,7 @@ fn commit_exists(repo: &Path, commit: &str) -> Result<bool> {
15731573
}
15741574

15751575
fn git_command(dir: &Path) -> Command {
1576-
let mut command = Command::new("git");
1576+
let mut command = bitfun_services_core::process_manager::create_command("git");
15771577
command
15781578
.current_dir(dir)
15791579
// A detached dispatch worker has nobody to answer a credential or
@@ -2419,20 +2419,22 @@ mod tests {
24192419
let dir = tempfile::tempdir().expect("tempdir");
24202420
let bitfun_home = dir.path().join("bitfun-home");
24212421
let user_root = dir.path().join("user-root");
2422-
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
2423-
.args([
2424-
"--exact",
2425-
"dispatch::workspace::tests::completed_clean_sync_poll_returns_the_durable_result",
2426-
"--nocapture",
2427-
])
2428-
.env(CHILD_ENV, &bitfun_home)
2429-
.env("BITFUN_HOME", &bitfun_home)
2430-
.env("BITFUN_USER_ROOT", &user_root)
2431-
.env("BITFUN_E2E_STORAGE_GUARD", "1")
2432-
.env_remove("BITFUN_E2E_HOME")
2433-
.env_remove("BITFUN_E2E_USER_ROOT")
2434-
.output()
2435-
.expect("run isolated clean-sync poll test");
2422+
let output = bitfun_services_core::process_manager::create_command(
2423+
std::env::current_exe().expect("test executable"),
2424+
)
2425+
.args([
2426+
"--exact",
2427+
"dispatch::workspace::tests::completed_clean_sync_poll_returns_the_durable_result",
2428+
"--nocapture",
2429+
])
2430+
.env(CHILD_ENV, &bitfun_home)
2431+
.env("BITFUN_HOME", &bitfun_home)
2432+
.env("BITFUN_USER_ROOT", &user_root)
2433+
.env("BITFUN_E2E_STORAGE_GUARD", "1")
2434+
.env_remove("BITFUN_E2E_HOME")
2435+
.env_remove("BITFUN_E2E_USER_ROOT")
2436+
.output()
2437+
.expect("run isolated clean-sync poll test");
24362438
assert!(
24372439
output.status.success(),
24382440
"isolated child failed:\nstdout:\n{}\nstderr:\n{}",

src/apps/cli/src/modes/exec/tests.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::process::Command;
2-
31
use super::lifecycle::{
42
completed_turn_failure, drain_interrupted_turn_events, effective_event_invocation,
53
event_belongs_to_exec_turn, event_turn_id, is_exec_terminal,
@@ -103,7 +101,7 @@ fn git_patch_includes_staged_and_untracked_files_from_a_repo_subdirectory() {
103101
let temp = tempfile::tempdir().expect("tempdir");
104102
let repo = temp.path();
105103
let run_git = |args: &[&str]| {
106-
let output = Command::new("git")
104+
let output = bitfun_core::util::process_manager::create_command("git")
107105
.args(args)
108106
.current_dir(repo)
109107
.output()
@@ -141,7 +139,7 @@ fn git_patch_excludes_a_preexisting_output_artifact_inside_the_repository() {
141139
let temp = tempfile::tempdir().expect("tempdir");
142140
let repo = temp.path();
143141
let run_git = |args: &[&str]| {
144-
let output = Command::new("git")
142+
let output = bitfun_core::util::process_manager::create_command("git")
145143
.args(args)
146144
.current_dir(repo)
147145
.output()
@@ -181,7 +179,7 @@ fn git_patch_excludes_a_tracked_output_artifact_inside_the_repository() {
181179
let temp = tempfile::tempdir().expect("tempdir");
182180
let repo = temp.path();
183181
let run_git = |args: &[&str]| {
184-
let output = Command::new("git")
182+
let output = bitfun_core::util::process_manager::create_command("git")
185183
.args(args)
186184
.current_dir(repo)
187185
.output()
@@ -702,7 +700,7 @@ fn deferred_exec_event_projects_effective_name_and_input() {
702700
}
703701

704702
fn run_git(workspace: &std::path::Path, args: &[&str]) {
705-
let output = Command::new("git")
703+
let output = bitfun_core::util::process_manager::create_command("git")
706704
.args(args)
707705
.current_dir(workspace)
708706
.output()

src/apps/cli/src/modes/exec/verification.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ pub(super) async fn run_verifier(
6363
retries_used: u32,
6464
) -> VerifyOutcome {
6565
let mut process = if cfg!(windows) {
66-
let mut process = tokio::process::Command::new("cmd");
66+
let mut process = bitfun_core::util::process_manager::create_tokio_command("cmd");
6767
process.arg("/C").arg(command);
6868
process
6969
} else {
70-
let mut process = tokio::process::Command::new("sh");
70+
let mut process = bitfun_core::util::process_manager::create_tokio_command("sh");
7171
process.arg("-c").arg(command);
7272
process
7373
};

src/apps/cli/tests/acp_stdio_cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ struct AcpProcess {
1616

1717
impl AcpProcess {
1818
async fn spawn(environment: &CliTestEnvironment) -> Self {
19-
let mut command = tokio::process::Command::new(env!("CARGO_BIN_EXE_bitfun"));
19+
let mut command = bitfun_services_core::process_manager::create_tokio_command(env!(
20+
"CARGO_BIN_EXE_bitfun"
21+
));
2022
command
2123
.arg("acp")
2224
.stdin(Stdio::piped())

src/apps/cli/tests/cli_command_contracts/compat_entrypoint.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ fn run_freshly_written(command: &mut Command) -> std::io::Result<Output> {
2727

2828
#[test]
2929
fn legacy_version_matches_primary_and_warns_only_on_stderr() {
30-
let primary = Command::new(env!("CARGO_BIN_EXE_bitfun"))
31-
.arg("--version")
32-
.output()
33-
.expect("run bitfun --version");
34-
let legacy = Command::new(env!("CARGO_BIN_EXE_bitfun-cli"))
35-
.arg("--version")
36-
.output()
37-
.expect("run deprecated bitfun-cli --version");
30+
let primary =
31+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
32+
.arg("--version")
33+
.output()
34+
.expect("run bitfun --version");
35+
let legacy =
36+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun-cli"))
37+
.arg("--version")
38+
.output()
39+
.expect("run deprecated bitfun-cli --version");
3840

3941
assert!(primary.status.success());
4042
assert!(legacy.status.success());
@@ -45,14 +47,16 @@ fn legacy_version_matches_primary_and_warns_only_on_stderr() {
4547

4648
#[test]
4749
fn legacy_forwards_clap_failure_exit_code() {
48-
let primary = Command::new(env!("CARGO_BIN_EXE_bitfun"))
49-
.arg("--not-a-real-option")
50-
.output()
51-
.expect("run invalid primary command");
52-
let legacy = Command::new(env!("CARGO_BIN_EXE_bitfun-cli"))
53-
.arg("--not-a-real-option")
54-
.output()
55-
.expect("run invalid legacy command");
50+
let primary =
51+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
52+
.arg("--not-a-real-option")
53+
.output()
54+
.expect("run invalid primary command");
55+
let legacy =
56+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun-cli"))
57+
.arg("--not-a-real-option")
58+
.output()
59+
.expect("run invalid legacy command");
5660

5761
assert_eq!(legacy.status.code(), primary.status.code());
5862
assert!(String::from_utf8_lossy(&legacy.stderr).starts_with(DEPRECATION));
@@ -69,8 +73,10 @@ fn legacy_reports_a_missing_primary_without_recursing() {
6973
let copied = temp.path().join(file_name);
7074
std::fs::copy(env!("CARGO_BIN_EXE_bitfun-cli"), &copied)
7175
.expect("copy deprecated launcher without primary sibling");
72-
let output = run_freshly_written(Command::new(copied).arg("--version"))
73-
.expect("run isolated deprecated launcher");
76+
let output = run_freshly_written(
77+
bitfun_services_core::process_manager::create_command(copied).arg("--version"),
78+
)
79+
.expect("run isolated deprecated launcher");
7480
let stderr = String::from_utf8_lossy(&output.stderr);
7581

7682
assert!(!output.status.success());

src/apps/cli/tests/cli_command_contracts/exec_cli_contracts.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#[path = "../support/mod.rs"]
22
mod support;
33

4-
use std::process::{Command, Output};
4+
use std::process::Output;
55
use support::{
66
command_output_with_timeout, CliTestEnvironment, MockOpenAiServer, STREAM_COMPLETED_MARKER,
77
STREAM_START_MARKER,
88
};
99

1010
fn run_cli(args: &[&str]) -> Output {
11-
Command::new(env!("CARGO_BIN_EXE_bitfun"))
11+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
1212
.args(args)
1313
.output()
1414
.expect("run bitfun")
@@ -43,13 +43,13 @@ fn is_terminal_event(value: &serde_json::Value) -> bool {
4343
fn command_timeout_helper_returns_within_its_failure_budget() {
4444
#[cfg(windows)]
4545
let mut command = {
46-
let mut command = Command::new("powershell.exe");
46+
let mut command = bitfun_services_core::process_manager::create_command("powershell.exe");
4747
command.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]);
4848
command
4949
};
5050
#[cfg(not(windows))]
5151
let mut command = {
52-
let mut command = Command::new("sh");
52+
let mut command = bitfun_services_core::process_manager::create_command("sh");
5353
command.args(["-c", "exec sleep 30"]);
5454
command
5555
};

src/apps/cli/tests/cli_command_contracts/mcp_add_cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::path::Path;
2-
use std::process::{Command, Output};
2+
use std::process::Output;
33

44
fn run_cli(workspace: &Path, user_root: &Path, home_root: &Path, args: &[&str]) -> Output {
55
let config_root = user_root.join("host-config");
6-
Command::new(env!("CARGO_BIN_EXE_bitfun"))
6+
bitfun_services_core::process_manager::create_command(env!("CARGO_BIN_EXE_bitfun"))
77
.args(args)
88
.current_dir(workspace)
99
.env_remove("BITFUN_USER_ROOT")

0 commit comments

Comments
 (0)