feat(debug): 支持网易 Minecraft 调试启动
新增 debug 子命令,自动准备开发世界、注册内置调试 MOD,并将项目行为包和资源包链接到网易运行目录,方便启动游戏后直接进入调试世界。 调试 MOD 资源随仓库一起嵌入,避免依赖本机绝对路径;Windows junction 写入剥离 verbatim 前缀后的 DOS 路径,保证 Minecraft 能正确读取链接包。
This commit is contained in:
250
src/debug/process.rs
Normal file
250
src/debug/process.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::{io, path::Path, thread};
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
use super::{
|
||||
config::{DebugConfig, ResolvedModDir},
|
||||
hotreload::HotReloadTask,
|
||||
ipc::DebugIpcServer,
|
||||
log::{self, LineBuffer},
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{
|
||||
CloseHandle, ERROR_BROKEN_PIPE, GetLastError, HANDLE, HANDLE_FLAG_INHERIT,
|
||||
SetHandleInformation,
|
||||
},
|
||||
Storage::FileSystem::ReadFile,
|
||||
System::{
|
||||
Console::{GetStdHandle, STD_INPUT_HANDLE},
|
||||
Pipes::CreatePipe,
|
||||
Threading::{
|
||||
CREATE_UNICODE_ENVIRONMENT, CreateProcessW, INFINITE, PROCESS_INFORMATION,
|
||||
STARTF_USESTDHANDLES, STARTUPINFOW, WaitForSingleObject,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
|
||||
|
||||
#[cfg(windows)]
|
||||
use super::win::WinHandle;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn launch_game(
|
||||
config: &DebugConfig,
|
||||
config_arg: Option<&Path>,
|
||||
mod_dirs: &[ResolvedModDir],
|
||||
) -> Result<()> {
|
||||
let enable_ipc = config.auto_hot_reload_mods;
|
||||
let ipc = if enable_ipc {
|
||||
let server = DebugIpcServer::start()?;
|
||||
println!("[MCDK] IPC调试服务器已启动,端口:{}", server.port());
|
||||
Some(server)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command = build_command(config, config_arg);
|
||||
let mut command_w = wide_null(OsStr::new(&command));
|
||||
let env_block = ipc
|
||||
.as_ref()
|
||||
.map(|server| build_environment_block(server.port()));
|
||||
|
||||
let mut security = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: ptr::null_mut(),
|
||||
bInheritHandle: 1,
|
||||
};
|
||||
|
||||
let (out_read, out_write) = create_pipe_pair(&mut security)?;
|
||||
let (err_read, err_write) = create_pipe_pair(&mut security)?;
|
||||
|
||||
let mut startup: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||
startup.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdOutput = out_write.raw();
|
||||
startup.hStdError = err_write.raw();
|
||||
startup.hStdInput = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
|
||||
|
||||
let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let env_ptr = env_block
|
||||
.as_ref()
|
||||
.map(|block| block.as_ptr().cast_mut().cast())
|
||||
.unwrap_or(ptr::null_mut());
|
||||
let creation_flags = if env_block.is_some() {
|
||||
CREATE_UNICODE_ENVIRONMENT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let ok = unsafe {
|
||||
CreateProcessW(
|
||||
ptr::null(),
|
||||
command_w.as_mut_ptr(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
creation_flags,
|
||||
env_ptr,
|
||||
ptr::null(),
|
||||
&startup,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
return Err(CliError::Io(io::Error::new(
|
||||
err.kind(),
|
||||
format!("CreateProcessW failed for command {command:?}: {err}"),
|
||||
)));
|
||||
}
|
||||
|
||||
drop(out_write);
|
||||
drop(err_write);
|
||||
|
||||
let pid = process_info.dwProcessId;
|
||||
let stdout_filter = config.include_debug_mod;
|
||||
let stderr_filter = config.include_debug_mod;
|
||||
let stdout_thread =
|
||||
thread::spawn(move || read_pipe(out_read, stdout_filter, log::process_stdout_line));
|
||||
let stderr_thread =
|
||||
thread::spawn(move || read_pipe(err_read, stderr_filter, log::process_stderr_line));
|
||||
|
||||
let mut hotreload = if config.auto_hot_reload_mods {
|
||||
ipc.clone()
|
||||
.and_then(|server| HotReloadTask::start(pid, mod_dirs, server))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
unsafe { WaitForSingleObject(process_info.hProcess, INFINITE) };
|
||||
|
||||
if let Some(task) = &mut hotreload {
|
||||
task.safe_exit();
|
||||
}
|
||||
if let Some(server) = &ipc {
|
||||
server.safe_exit();
|
||||
}
|
||||
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
unsafe {
|
||||
CloseHandle(process_info.hProcess);
|
||||
CloseHandle(process_info.hThread);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn launch_game(
|
||||
_config: &DebugConfig,
|
||||
_config_arg: Option<&Path>,
|
||||
_mod_dirs: &[ResolvedModDir],
|
||||
) -> Result<()> {
|
||||
Err(CliError::InvalidInput(
|
||||
"debug launch is only supported on Windows".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn build_command(config: &DebugConfig, config_arg: Option<&Path>) -> String {
|
||||
let mut command = String::new();
|
||||
command.push('"');
|
||||
command.push_str(&config.game_executable_path);
|
||||
command.push('"');
|
||||
if !config.netease_config.chat_extension {
|
||||
command.push_str(" chatExtension=false");
|
||||
}
|
||||
if let Some(path) = config_arg {
|
||||
command.push_str(" config=\"");
|
||||
command.push_str(&path.to_string_lossy().replace('\\', "/"));
|
||||
command.push('"');
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn create_pipe_pair(security: &mut SECURITY_ATTRIBUTES) -> Result<(WinHandle, WinHandle)> {
|
||||
let mut read: HANDLE = ptr::null_mut();
|
||||
let mut write: HANDLE = ptr::null_mut();
|
||||
let ok = unsafe { CreatePipe(&mut read, &mut write, security, 0) };
|
||||
if ok == 0 {
|
||||
return Err(CliError::Io(io::Error::last_os_error()));
|
||||
}
|
||||
let read = WinHandle::new(read)?;
|
||||
let write = WinHandle::new(write)?;
|
||||
let ok = unsafe { SetHandleInformation(read.raw(), HANDLE_FLAG_INHERIT, 0) };
|
||||
if ok == 0 {
|
||||
return Err(CliError::Io(io::Error::last_os_error()));
|
||||
}
|
||||
Ok((read, write))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn read_pipe<F>(pipe: WinHandle, filter_python: bool, process_line: F)
|
||||
where
|
||||
F: Fn(&str) + Send + 'static,
|
||||
{
|
||||
const BUFSZ: usize = 4096;
|
||||
let mut line_buffer = LineBuffer::new(filter_python);
|
||||
let mut buffer = [0u8; BUFSZ];
|
||||
|
||||
loop {
|
||||
let mut bytes_read = 0u32;
|
||||
let ok = unsafe {
|
||||
ReadFile(
|
||||
pipe.raw(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len() as u32,
|
||||
&mut bytes_read,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = unsafe { GetLastError() };
|
||||
if err == ERROR_BROKEN_PIPE {
|
||||
line_buffer.flush(|line| process_line(&line));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if bytes_read == 0 {
|
||||
line_buffer.flush(|line| process_line(&line));
|
||||
break;
|
||||
}
|
||||
line_buffer.append(&buffer[..bytes_read as usize], |line| process_line(&line));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn build_environment_block(ipc_port: u16) -> Vec<u16> {
|
||||
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
||||
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
||||
.collect();
|
||||
pairs.push((
|
||||
OsStr::new("MCDEV_DEBUG_IPC_PORT").encode_wide().collect(),
|
||||
OsStr::new(&ipc_port.to_string()).encode_wide().collect(),
|
||||
));
|
||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut block = Vec::new();
|
||||
for (key, value) in pairs {
|
||||
block.extend(key);
|
||||
block.push('=' as u16);
|
||||
block.extend(value);
|
||||
block.push(0);
|
||||
}
|
||||
block.push(0);
|
||||
block
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn wide_null(value: &OsStr) -> Vec<u16> {
|
||||
value.encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
Reference in New Issue
Block a user