fix(core): 老版本 Java 按版本距离选 + 启动看门狗有界等待 + 结束归属(M214 D4/D6/D10)
D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个 (托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`: 不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` / 实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。 `JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。 D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑): ①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死); ②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。 到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`, 返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配 (默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`, 生效值每次启动写进 launcher.log 与人读输出)。 D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记** `kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径, `finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」 判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是 `TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。 顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、 `NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。 用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、 exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
This commit is contained in:
1 parent
5959a27add
commit
ff78fe2cae
23 files changed
+1708
-250
No files matched your search
@@ -19,8 +19,20 @@ use crate::launch::watchdog::{self, PumpVerdict, WatchdogConfig};
|
||||
|
||||
/// 强制结束后等进程消失的预算。
|
||||
const FORCE_KILL_BUDGET: Duration = Duration::from_secs(5);
|
||||
/// 轮询进程状态的间隔。
|
||||
const POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
/// 轮询进程状态的间隔(有界等待也用这个)。
|
||||
pub const POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
|
||||
/// 默认启动预算:spawn 后**连续这么久既没有日志输出、也没见到游戏窗口**就停止等待(D6)。
|
||||
///
|
||||
/// 判据是「静默」不是「等了多久」:还在打日志说明它还在加载(大整合包首次启动三分钟以上
|
||||
/// 是常态,那期间日志一直在刷),所以这里按「一声不响」计量,宁松不紧。
|
||||
pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
/// 默认关闭宽限:日志里出现关闭标记后这么久还没退出,就认为卡在关闭(D6)。
|
||||
pub const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_secs(60);
|
||||
/// 覆盖启动预算的环境变量(单位秒,`0` = 不限)。
|
||||
pub const STARTUP_TIMEOUT_ENV: &str = "ULL_LAUNCH_STARTUP_TIMEOUT_SECS";
|
||||
/// 覆盖关闭宽限的环境变量(单位秒,`0` = 不限)。
|
||||
pub const SHUTDOWN_GRACE_ENV: &str = "ULL_LAUNCH_SHUTDOWN_GRACE_SECS";
|
||||
|
||||
/// 进程管理配置。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -31,6 +43,13 @@ pub struct ProcessConfig {
|
||||
pub graceful_stop: Duration,
|
||||
/// 环形缓冲配置。
|
||||
pub buffer: RingBufferConfig,
|
||||
/// 未见游戏窗口的**静默**预算(D6;`0` = 不判)。
|
||||
///
|
||||
/// 判据是「连续这么久没有任何日志输出、也没见到窗口」——还在打日志就不算卡死,
|
||||
/// 大整合包慢慢加载不会被判进来。
|
||||
pub startup_timeout: Duration,
|
||||
/// 出现关闭标记后仍不退出的宽限(D6;`0` = 不判)。
|
||||
pub shutdown_grace: Duration,
|
||||
}
|
||||
|
||||
impl Default for ProcessConfig {
|
||||
@@ -39,10 +58,43 @@ impl Default for ProcessConfig {
|
||||
watchdog: WatchdogConfig::default(),
|
||||
graceful_stop: Duration::from_secs(3),
|
||||
buffer: RingBufferConfig::default(),
|
||||
startup_timeout: DEFAULT_STARTUP_TIMEOUT,
|
||||
shutdown_grace: DEFAULT_SHUTDOWN_GRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessConfig {
|
||||
/// 按环境变量覆盖两个看门狗时长(D6 的「可配」出口;不设就等于默认值)。
|
||||
///
|
||||
/// 放在这里而不是 `default()`:默认值必须是纯的(测试与嵌入方都吃它),
|
||||
/// 环境变量只在宿主明确要求时生效(CLI 的 `LauncherConfig::with_watchdog_env`)。
|
||||
#[must_use]
|
||||
pub fn with_env_overrides(mut self) -> Self {
|
||||
if let Some(value) = env_seconds(STARTUP_TIMEOUT_ENV) {
|
||||
self.startup_timeout = value;
|
||||
}
|
||||
if let Some(value) = env_seconds(SHUTDOWN_GRACE_ENV) {
|
||||
self.shutdown_grace = value;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 读一个「秒数」环境变量(空值/非数字/溢出都当没设)。
|
||||
fn env_seconds(name: &str) -> Option<Duration> {
|
||||
parse_seconds(&std::env::var(name).ok()?)
|
||||
}
|
||||
|
||||
/// 解析秒数(`"120"` → 120s,`"0"` → 0s = 关闭该判据;其余一律 `None`)。
|
||||
fn parse_seconds(raw: &str) -> Option<Duration> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
trimmed.parse::<u64>().ok().map(Duration::from_secs)
|
||||
}
|
||||
|
||||
/// 已启动的游戏进程。
|
||||
#[derive(Debug)]
|
||||
pub struct RunningProcess {
|
||||
@@ -147,6 +199,15 @@ impl RunningProcess {
|
||||
})
|
||||
}
|
||||
|
||||
/// 非阻塞取退出状态(`None` = 还在跑)。有界等待(D6)靠它轮询。
|
||||
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
|
||||
self.child.try_wait().map_err(|source| LaunchError::Io {
|
||||
op: "try_wait_child",
|
||||
path: PathBuf::from(self.pid.to_string()),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
/// 进程退出后,按预算收尾日志泵。
|
||||
pub fn supervise(&mut self) -> PumpVerdict {
|
||||
watchdog::wait_for_pumps(&mut self.pump, &self.config.watchdog)
|
||||
@@ -229,3 +290,28 @@ pub fn exit_code(status: &ExitStatus) -> i32 {
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_seconds, ProcessConfig, DEFAULT_SHUTDOWN_GRACE, DEFAULT_STARTUP_TIMEOUT};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn defaults_are_real_values_not_disabled() {
|
||||
let config = ProcessConfig::default();
|
||||
assert_eq!(config.startup_timeout, DEFAULT_STARTUP_TIMEOUT);
|
||||
assert_eq!(config.shutdown_grace, DEFAULT_SHUTDOWN_GRACE);
|
||||
assert!(!config.startup_timeout.is_zero(), "默认值不能等于关掉");
|
||||
assert!(!config.shutdown_grace.is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seconds_parser_accepts_plain_numbers_and_zero() {
|
||||
assert_eq!(parse_seconds("120"), Some(Duration::from_secs(120)));
|
||||
assert_eq!(parse_seconds(" 45 "), Some(Duration::from_secs(45)));
|
||||
assert_eq!(parse_seconds("0"), Some(Duration::ZERO));
|
||||
assert_eq!(parse_seconds(""), None);
|
||||
assert_eq!(parse_seconds("abc"), None);
|
||||
assert_eq!(parse_seconds("-5"), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user