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:
1 parent
bb8be87c8f
commit
40a296471d
17 files changed
+417
-39
No files matched your search
@@ -16,7 +16,6 @@
|
||||
//! 既不误杀真实装,也不假装校验通过。
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -31,6 +30,7 @@ use crate::download::verify;
|
||||
use crate::download::HashAlgo;
|
||||
use crate::game::java::{self, JavaRequirement, JavaRuntime};
|
||||
use crate::launch::redact::Redactor;
|
||||
use crate::platform::child::quiet;
|
||||
|
||||
/// 一步的结局。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -227,7 +227,7 @@ fn run_step(
|
||||
let classpath = step.classpath_join()?;
|
||||
|
||||
let started = Instant::now();
|
||||
let output = Command::new(&runtime.binary)
|
||||
let output = quiet(&runtime.binary)
|
||||
.arg("-cp")
|
||||
.arg(&classpath)
|
||||
.arg(&main_class)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! 两个纪律:①**不引依赖**——`tzutil` / `reg` 都是系统自带,`discover.rs` 早就有 `reg query`
|
||||
//! 的先例;②**不做网络探测**——探测(google 可达性)仍由调用方决定开不开。
|
||||
|
||||
use std::process::Command;
|
||||
use crate::platform::child::quiet;
|
||||
|
||||
use super::region::{Signals, GREATER_CHINA_TIMEZONES};
|
||||
|
||||
@@ -64,7 +64,7 @@ pub fn detect_signals() -> Signals {
|
||||
/// `tzutil /g` 的输出(失败为 `None`)。
|
||||
#[must_use]
|
||||
pub fn windows_timezone() -> Option<String> {
|
||||
let output = Command::new("tzutil").arg("/g").output().ok()?;
|
||||
let output = quiet("tzutil").arg("/g").output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn windows_timezone() -> Option<String> {
|
||||
/// 系统的 locale 名(`HKCU\Control Panel\International` 的 `LocaleName`)。
|
||||
#[must_use]
|
||||
pub fn windows_locale() -> Option<String> {
|
||||
let output = Command::new("reg")
|
||||
let output = quiet("reg")
|
||||
.args([
|
||||
"query",
|
||||
"HKCU\\Control Panel\\International",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
//! (别为了一次启动白跑 N 次 `java -version`)。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::platform::child::quiet;
|
||||
use crate::platform::paths::{EnvSource, ProcessEnv};
|
||||
use crate::platform::termux;
|
||||
|
||||
@@ -153,7 +153,7 @@ pub fn registry_homes() -> Vec<PathBuf> {
|
||||
}
|
||||
let mut homes = Vec::new();
|
||||
for view in ["64", "32"] {
|
||||
let Ok(output) = Command::new("reg")
|
||||
let Ok(output) = quiet("reg")
|
||||
.args(["query", "HKLM\\SOFTWARE\\JavaSoft\\JDK", "/s", "/reg:"])
|
||||
.arg(view)
|
||||
.output()
|
||||
|
||||
@@ -105,7 +105,7 @@ pub fn probe_with_timeout(
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
) -> (JavaRuntime, String) {
|
||||
let mut runtime = JavaRuntime::unknown(binary.to_path_buf(), origin);
|
||||
let mut command = std::process::Command::new(binary);
|
||||
let mut command = crate::platform::child::quiet(binary);
|
||||
command.arg("-version");
|
||||
let Some(output) = super::proc::run_with_timeout(command, timeout, cancelled) else {
|
||||
return (runtime, String::new());
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
//! 候选运行时由调用方注入,测试因此不需要真的装三个 JDK。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use super::extract::{java_binary_name, read_stamp};
|
||||
use super::vendor::JdkVendor;
|
||||
use crate::platform::child::quiet;
|
||||
|
||||
/// 缺省 Java 主版本(探测不到任何东西时的保守值,D13:JDK 17 基准)。
|
||||
pub const DEFAULT_MAJOR: u32 = 17;
|
||||
@@ -119,7 +119,7 @@ impl JavaRuntime {
|
||||
#[must_use]
|
||||
pub fn probe(binary: &Path, origin: JavaOrigin) -> JavaRuntime {
|
||||
let mut runtime = JavaRuntime::unknown(binary.to_path_buf(), origin);
|
||||
let Ok(output) = Command::new(binary).arg("-version").output() else {
|
||||
let Ok(output) = quiet(binary).arg("-version").output() else {
|
||||
return runtime;
|
||||
};
|
||||
let mut text = String::from_utf8_lossy(&output.stderr).into_owned();
|
||||
|
||||
@@ -236,7 +236,7 @@ mod tests {
|
||||
seconds: u32,
|
||||
) -> std::process::Command {
|
||||
let mut command = if cfg!(windows) {
|
||||
let mut command = std::process::Command::new("powershell");
|
||||
let mut command = crate::platform::child::quiet("powershell");
|
||||
command.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
@@ -248,7 +248,7 @@ mod tests {
|
||||
]);
|
||||
command
|
||||
} else {
|
||||
let mut command = std::process::Command::new("sh");
|
||||
let mut command = crate::platform::child::quiet("sh");
|
||||
command.args([
|
||||
"-c",
|
||||
&format!(
|
||||
@@ -267,11 +267,11 @@ mod tests {
|
||||
/// 一个必然超时的命令(两个平台都有,且在「等很久」这件事上行为一致)。
|
||||
fn sleeping_command(seconds: u32) -> std::process::Command {
|
||||
if cfg!(windows) {
|
||||
let mut command = std::process::Command::new("cmd");
|
||||
let mut command = crate::platform::child::quiet("cmd");
|
||||
command.args(["/C", &format!("ping -n {seconds} 127.0.0.1 > NUL")]);
|
||||
command
|
||||
} else {
|
||||
let mut command = std::process::Command::new("sleep");
|
||||
let mut command = crate::platform::child::quiet("sleep");
|
||||
command.arg(seconds.to_string());
|
||||
command
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
//! 镜像顺序是实测可用的国内源;全部失败才报错,错误里带每一跳的原因。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::platform::child::quiet;
|
||||
use crate::platform::paths::ProcessEnv;
|
||||
use crate::platform::termux as host;
|
||||
|
||||
@@ -183,7 +183,7 @@ fn apt_install(prefix: &Path, workspace: &Path, mirror: &str, package: &str) ->
|
||||
/// 执行一次 `apt-get`(`-o` 选项在前,子命令在后)。
|
||||
fn run_apt(prefix: &Path, options: &[String], args: &[&str]) -> Result<(), String> {
|
||||
let apt = prefix.join("bin").join("apt-get");
|
||||
let mut command = Command::new(&apt);
|
||||
let mut command = quiet(&apt);
|
||||
command.env("DEBIAN_FRONTEND", "noninteractive");
|
||||
for option in options {
|
||||
command.arg("-o").arg(option);
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
//! 的实际停止流程以“进程是否真的消失”为准(`try_wait` 轮询),本模块只提供发送动作。
|
||||
|
||||
use std::io;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::platform::child::quiet;
|
||||
|
||||
/// 温和关闭(Windows 无 `/F`;POSIX `SIGTERM`)。
|
||||
pub fn terminate(pid: u32) -> io::Result<()> {
|
||||
@@ -100,7 +101,7 @@ fn pgid_of(pid: u32) -> Option<u32> {
|
||||
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
fn pgid_of(pid: u32) -> Option<u32> {
|
||||
let output = Command::new("ps")
|
||||
let output = quiet("ps")
|
||||
.args(["-o", "pgid=", "-p", &pid.to_string()])
|
||||
.output()
|
||||
.ok()?;
|
||||
@@ -113,18 +114,14 @@ fn pgid_of(pid: u32) -> Option<u32> {
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
/// 不弹控制台窗口(否则 kill 子进程会闪一个黑框)。
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
pub(super) fn signal(pid: u32, force: bool) -> io::Result<()> {
|
||||
let mut command = Command::new("taskkill");
|
||||
// `CREATE_NO_WINDOW` 由 `platform::child::quiet` 统一带(否则 kill 子进程会闪一个黑框)。
|
||||
let mut command = quiet("taskkill");
|
||||
command.arg("/PID").arg(pid.to_string()).arg("/T");
|
||||
if force {
|
||||
command.arg("/F");
|
||||
}
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
let status = command
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
@@ -168,7 +165,7 @@ mod imp {
|
||||
/// `kill` 不存在时给**明确**错误而不是静默失败:否则调用方只会看到“进程仍在运行”,
|
||||
/// 排查方向会被带偏(M48 之前的红灯就是这么来的)。
|
||||
fn run_kill(flag: &str, args: &[&str]) -> io::Result<bool> {
|
||||
match Command::new("kill")
|
||||
match quiet("kill")
|
||||
.arg(flag)
|
||||
.args(args)
|
||||
.stdout(std::process::Stdio::null())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -216,8 +216,7 @@ pub fn format_version(version: (u32, u32)) -> String {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn run(program: &str, args: &[&str]) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let output = Command::new(program).args(args).output().ok()?;
|
||||
let output = super::child::quiet(program).args(args).output().ok()?;
|
||||
if output.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -91,8 +91,7 @@ pub fn parse_sysctl_bool(text: &str) -> bool {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn run(program: &str, args: &[&str]) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let output = Command::new(program).args(args).output().ok()?;
|
||||
let output = super::child::quiet(program).args(args).output().ok()?;
|
||||
if !output.status.success() || output.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,15 @@
|
||||
//! - [`wallpaper`]:桌面壁纸**路径探测**(D38 配色三级优先里第②级的输入)。本模块只做路径
|
||||
//! 探测——不解码图片、不取色、不生成调色板,莫奈取色与三级降级在 `crates/design` 的
|
||||
//! 后续 mission。
|
||||
//! - [`child`]:子进程的唯一构造器 [`child::quiet`](M213)。Windows 上给子进程带
|
||||
//! `CREATE_NO_WINDOW`,否则 GUI 父进程 spawn 控制台程序会给子进程新建控制台——桌面上
|
||||
//! 就是「黑框闪一下」。**别在别处直接构造 `Command`**,`tests/child_quiet.rs` 守门。
|
||||
//!
|
||||
//! 尚未落地(后续 mission):文件锁、进程工具、真实 GL/Vulkan 能力探测(钩子见
|
||||
//! [`linux_caps::probe_gl`] 与 [`vk_probe::probe_vk`],FFI 受限,实际探测归 app 层渲染后端选择)。
|
||||
|
||||
pub mod atomic;
|
||||
pub mod child;
|
||||
pub mod data_dir;
|
||||
pub mod dir_migration;
|
||||
pub mod error;
|
||||
|
||||
@@ -173,8 +173,7 @@ fn parse_dword(raw: &str) -> Option<u32> {
|
||||
|
||||
/// 跑一个外部命令,取 stdout(命令缺失 / 非零退出 / 空输出都返回 `None`)。
|
||||
fn run(program: &str, args: &[&str]) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let output = Command::new(program).args(args).output().ok()?;
|
||||
let output = super::child::quiet(program).args(args).output().ok()?;
|
||||
if output.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -296,8 +296,7 @@ pub fn ldconfig_has_libvulkan(text: &str) -> bool {
|
||||
|
||||
/// 跑一个只读命令取 stdout(`reg query` / `ldconfig -p`;失败返回 `None`)。
|
||||
fn run(program: &str, args: &[&str]) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let output = Command::new(program).args(args).output().ok()?;
|
||||
let output = super::child::quiet(program).args(args).output().ok()?;
|
||||
if output.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::platform::child::quiet;
|
||||
use crate::platform::paths::EnvSource;
|
||||
|
||||
/// 探测需要的外部世界:四个方法覆盖三平台的全部读法。
|
||||
@@ -62,10 +63,7 @@ impl ProbeIo for HostIo {
|
||||
}
|
||||
|
||||
fn run(&self, program: &str, args: &[&str]) -> Option<String> {
|
||||
let output = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()?;
|
||||
let output = quiet(program).args(args).output().ok()?;
|
||||
// `Command::output` 对非零退出同样返回 `Ok`,所以退出码要单独判:三条命令
|
||||
// (`reg.exe` / `gsettings` / `defaults`)出错时都走 stderr,只看 stdout 会把
|
||||
// 「查不到」和「命令失败」混成一种,调用方的原因文案就名不副实了。
|
||||
|
||||
@@ -206,8 +206,7 @@ fn parse_version_pair(value: &str) -> Option<(u32, u32)> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run(program: &str, args: &[&str]) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let output = Command::new(program).args(args).output().ok()?;
|
||||
let output = super::child::quiet(program).args(args).output().ok()?;
|
||||
if output.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user