fix(core): 所有子进程统一走 platform::child::quiet + 角色守卫(M213)

GUI 父进程(PE Subsystem = 2)spawn 控制台子进程时,Windows 会为子进程新建一块
控制台——桌面上就是「黑框闪一下」。改前 crates/core/src 有 21 处 Command::new,
只有 tree_kill 那一处带了 CREATE_NO_WINDOW,于是探 N 个 JDK 就闪 N 次 java -version。

- 新增 crates/core/src/platform/child.rs:唯一构造器 quiet(program),Windows 上统一
  .creation_flags(CREATE_NO_WINDOW),其它平台原样返回。tree_kill 里那份就地合并进来,
  不再保留第二份常量。
- 20 处 spawn 全部改走它(region_system / java{discover,manual,probe,proc,termux} /
  addon exec / platform{linux_caps,macos_version,theme_probe,vk_probe,windows_version,
  wallpaper/io})。
- launch/process.rs 零改动:游戏进程要 CREATE_NEW_PROCESS_GROUP 撑 taskkill /T,
  带不带控制台窗口是产品决定。
- crates/core/tests/child_quiet.rs:按角色判「有没有绕开构造器」的守卫,外加构造器
  本身的 4 条行为用例。三条变异(裸 Command::new / 别名 Spawn::new / 删掉 creation_flags)
  都实测变红并已还原。
This commit is contained in:
AskaEth committed 2026-11-09 15:07:56 +08:00
1 parent bb8be87c8f
commit 40a296471d
17 files changed
+417 -39

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
//! 子进程构造器:本 crate 里**唯一**允许直接 `std::process::Command::new` 的地方(M213)。
//!
//! ## 为什么需要它(这不是风格问题,是窗口问题)
//!
//! Windows 上「某个子进程要不要一块控制台」由**创建者的可执行文件类型**决定:一个**自己没有
//! 控制台**的进程(GUI 程序的 PE Subsystem = 2;本仓库的 `unlucky-launcher.exe` 就是,M196 已改)
//! 去 spawn 一个**控制台程序**(`java.exe` / `reg.exe` / `tzutil.exe` / `cmd.exe` /
//! `powershell.exe` …)时,Windows 会为这个子进程**新建一块控制台**——桌面上看到的就是
//! 「黑框闪一下」。所以探 N 个 JDK 就闪 N 次 `java -version`,用户的原话是「疯狂闪」。
//!
//! 抑制手段只有一个:创建时带 `CREATE_NO_WINDOW`(`0x0800_0000`)。
//! **`Stdio::null()` 不管这件事**——它只把子进程的 stdout/stderr 指到空设备,控制台窗口照样会被
//! 创建。所以到处补 `.stdout(Stdio::null())` 是无效修法。
//!
//! ## 谁**不许**走这里
//!
//! **游戏进程**([`crate::launch::process::RunningProcess::spawn`])不走这里:它要
//! `CREATE_NEW_PROCESS_GROUP` 才能用 `taskkill /T` 回收整棵进程树,两者是同一枚
//! `creation_flags` 参数上的不同取值,不能简单叠加。「游戏要不要带自己的控制台窗口」是
//! **产品决定**(玩家要看游戏自己的输出窗口),不是顺手抑制的事——所以 M213 一个字没动它。
//!
//! 那边是唯一另一处合法的直接构造点,两处都由 `crates/core/tests/child_quiet.rs` 的守卫钉住。
use std::ffi::OsStr;
use std::process::Command;
/// `CREATE_NO_WINDOW`:子进程不获得控制台窗口。
///
/// 见 <https://learn.microsoft.com/windows/win32/procthread/process-creation-flags>。
/// 别和 `CREATE_NEW_CONSOLE`(`0x0000_0010`,意思正好相反)或
/// `DETACHED_PROCESS`(`0x0000_0008`,连标准句柄都不给)搞混。
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// 起一个**不该在桌面上留下窗口**的子进程。
///
/// - Windows:`.creation_flags(CREATE_NO_WINDOW)`;
/// - 其它平台:原样返回,一个字不改(POSIX 没有「spawn 就新开一个终端」这回事,子进程的
/// 窗口从来不由我们造)。
///
/// 返回值就是普通 [`std::process::Command`],调用方照旧 `.arg(..)` / `.env(..)` /
/// `.output()` / `.status()`,或者整个交给 `game::java::proc::run_with_timeout` 去带超时跑。
/// 本 crate 是 `#![forbid(unsafe_code)]`,而 `CommandExt::creation_flags` 是 std 的安全 API,
/// 所以这里不需要任何 `unsafe`。
#[must_use]
pub fn quiet<S: AsRef<OsStr>>(program: S) -> Command {
#[allow(unused_mut)]
let mut command = Command::new(program);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
command.creation_flags(CREATE_NO_WINDOW);
}
command
}