Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,222 @@
|
||||
//! 进程管理:spawn + stdout/stderr 泵 + 进程树回收。
|
||||
//!
|
||||
//! 对应 `analyse/02` §6 的四条线程模型(`stdout-pump` / `stderr-pump` / `exit-waiter`),
|
||||
//! 但线程生命周期的收尾由 [`crate::launch::watchdog`] 兜底,不无条件 `join`。
|
||||
//!
|
||||
//! 本模块只管“把命令行跑起来并收集输出”,不做组件检查、不做诊断——
|
||||
//! 那些是 `launch` 上层流水线(阶段B)与 M15 的职责。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, ExitStatus, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::launch::args_table::AssembledCommand;
|
||||
use crate::launch::error::{LaunchError, Result};
|
||||
use crate::launch::pump::{LineSink, LogLine, PumpGroup, RingBuffer, RingBufferConfig};
|
||||
use crate::launch::tree_kill;
|
||||
use crate::launch::watchdog::{self, PumpVerdict, WatchdogConfig};
|
||||
|
||||
/// 强制结束后等进程消失的预算。
|
||||
const FORCE_KILL_BUDGET: Duration = Duration::from_secs(5);
|
||||
/// 轮询进程状态的间隔。
|
||||
const POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
|
||||
/// 进程管理配置。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ProcessConfig {
|
||||
/// 日志泵等待预算。
|
||||
pub watchdog: WatchdogConfig,
|
||||
/// 停止时温和关闭的宽限期。
|
||||
pub graceful_stop: Duration,
|
||||
/// 环形缓冲配置。
|
||||
pub buffer: RingBufferConfig,
|
||||
}
|
||||
|
||||
impl Default for ProcessConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
watchdog: WatchdogConfig::default(),
|
||||
graceful_stop: Duration::from_secs(3),
|
||||
buffer: RingBufferConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 已启动的游戏进程。
|
||||
#[derive(Debug)]
|
||||
pub struct RunningProcess {
|
||||
child: Child,
|
||||
pump: PumpGroup,
|
||||
buffer: Arc<Mutex<RingBuffer>>,
|
||||
started_at: Instant,
|
||||
pid: u32,
|
||||
config: ProcessConfig,
|
||||
}
|
||||
|
||||
impl RunningProcess {
|
||||
/// 启动进程并接管两条输出流。
|
||||
pub fn spawn(
|
||||
command: &AssembledCommand,
|
||||
working_dir: Option<&Path>,
|
||||
sink: Arc<dyn LineSink>,
|
||||
config: ProcessConfig,
|
||||
) -> Result<Self> {
|
||||
let mut process = Command::new(&command.program);
|
||||
process.args(&command.args);
|
||||
for (key, value) in &command.env {
|
||||
process.env(key, value);
|
||||
}
|
||||
if let Some(dir) = working_dir {
|
||||
process.current_dir(dir);
|
||||
}
|
||||
process
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
/// 独立进程组:便于 `taskkill /T` 递归回收。
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
|
||||
process.creation_flags(CREATE_NEW_PROCESS_GROUP);
|
||||
}
|
||||
|
||||
let mut child = process.spawn().map_err(|source| LaunchError::Spawn {
|
||||
program: command.program.clone(),
|
||||
source,
|
||||
})?;
|
||||
let pid = child.id();
|
||||
let buffer = Arc::new(Mutex::new(RingBuffer::new(config.buffer)));
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
let pump = PumpGroup::spawn(stdout, stderr, buffer.clone(), sink);
|
||||
Ok(Self {
|
||||
child,
|
||||
pump,
|
||||
buffer,
|
||||
started_at: Instant::now(),
|
||||
pid,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// 进程号。
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// 已运行毫秒数。
|
||||
pub fn uptime_ms(&self) -> u64 {
|
||||
u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
/// 启动时刻(窗口检测计时用)。
|
||||
pub fn started_at(&self) -> Instant {
|
||||
self.started_at
|
||||
}
|
||||
|
||||
/// 共享的日志环形缓冲。
|
||||
pub fn buffer(&self) -> Arc<Mutex<RingBuffer>> {
|
||||
self.buffer.clone()
|
||||
}
|
||||
|
||||
/// 当前可读日志快照。
|
||||
pub fn lines(&self) -> Vec<LogLine> {
|
||||
match self.buffer.lock() {
|
||||
Ok(guard) => guard.lines().cloned().collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 等待进程退出(不等待日志泵)。
|
||||
pub fn wait(&mut self) -> Result<ExitStatus> {
|
||||
self.child.wait().map_err(|source| LaunchError::Io {
|
||||
op: "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)
|
||||
}
|
||||
|
||||
/// 进程是否仍在运行。
|
||||
pub fn is_running(&mut self) -> bool {
|
||||
matches!(self.child.try_wait(), Ok(None))
|
||||
}
|
||||
|
||||
/// 三段式停止整棵进程树(温和 → 宽限 → 强制)。
|
||||
///
|
||||
/// 判定标准是**进程是否真的没了**,不是 `taskkill`/`kill` 的退出码:
|
||||
/// 外部命令对“已经退出的进程”会返回非 0(Windows 上 `taskkill` 报 128),
|
||||
/// 把它当失败会误报——尤其是温和阶段已经把进程关掉的情况。
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
if !self.is_running() {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = tree_kill::terminate(self.pid);
|
||||
if self.wait_until_exit(self.config.graceful_stop) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = tree_kill::force_kill(self.pid);
|
||||
if self.wait_until_exit(FORCE_KILL_BUDGET) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(LaunchError::Kill {
|
||||
pid: self.pid,
|
||||
source: std::io::Error::other("强制结束后进程仍在运行"),
|
||||
})
|
||||
}
|
||||
|
||||
/// 直接强杀(看门狗/崩溃兜底路径)。
|
||||
pub fn force_kill(&mut self) -> Result<()> {
|
||||
if !self.is_running() {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = tree_kill::force_kill(self.pid);
|
||||
if self.wait_until_exit(FORCE_KILL_BUDGET) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LaunchError::Kill {
|
||||
pid: self.pid,
|
||||
source: std::io::Error::other("强制结束后进程仍在运行"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_until_exit(&mut self, budget: Duration) -> bool {
|
||||
let deadline = Instant::now() + budget;
|
||||
loop {
|
||||
match self.child.try_wait() {
|
||||
Ok(Some(_)) => return true,
|
||||
Ok(None) => {}
|
||||
Err(_) => return false,
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
std::thread::sleep(POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从退出状态取整数退出码。
|
||||
///
|
||||
/// POSIX 上被子进程信号杀死没有 `code()`,按 shell 惯例补 `128 + signal`
|
||||
/// (`SIGKILL` → 137、`SIGTERM` → 143,与 `analyse/02` 附录 C 一致)。
|
||||
pub fn exit_code(status: &ExitStatus) -> i32 {
|
||||
if let Some(code) = status.code() {
|
||||
return code;
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
if let Some(signal) = status.signal() {
|
||||
return 128 + signal;
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
Reference in new issue
Block a user