2026-05-16 00:59:51 +08:00
|
|
|
|
use std::{io, path::Path, thread};
|
|
|
|
|
|
|
|
|
|
|
|
use crate::error::{CliError, Result};
|
|
|
|
|
|
|
|
|
|
|
|
use super::{
|
|
|
|
|
|
config::{DebugConfig, ResolvedModDir},
|
2026-07-21 20:44:13 +08:00
|
|
|
|
hotreload::{HotReloadOptions, HotReloadTask},
|
2026-05-16 00:59:51 +08:00
|
|
|
|
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],
|
2026-07-21 21:04:45 +08:00
|
|
|
|
mcp_state_path: Option<&Path>,
|
2026-05-16 00:59:51 +08:00
|
|
|
|
) -> 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));
|
2026-07-21 21:04:45 +08:00
|
|
|
|
let env_block = if ipc.is_some() || mcp_state_path.is_some() || config.auto_hot_reload_ui {
|
2026-07-21 20:44:13 +08:00
|
|
|
|
Some(build_environment_block(
|
|
|
|
|
|
ipc.as_ref().map(|server| server.port()),
|
2026-07-21 21:04:45 +08:00
|
|
|
|
mcp_state_path,
|
2026-07-21 20:44:13 +08:00
|
|
|
|
))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-05-16 00:59:51 +08:00
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
2026-07-21 20:44:13 +08:00
|
|
|
|
let mut hotreload = HotReloadTask::start(
|
|
|
|
|
|
pid,
|
|
|
|
|
|
mod_dirs,
|
|
|
|
|
|
ipc.clone(),
|
|
|
|
|
|
HotReloadOptions {
|
|
|
|
|
|
python: config.auto_hot_reload_mods,
|
|
|
|
|
|
json_ui: config.auto_hot_reload_ui,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-05-16 00:59:51 +08:00
|
|
|
|
|
|
|
|
|
|
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],
|
2026-07-21 21:04:45 +08:00
|
|
|
|
_mcp_state_path: Option<&Path>,
|
2026-05-16 00:59:51 +08:00
|
|
|
|
) -> 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)]
|
2026-07-21 21:04:45 +08:00
|
|
|
|
fn build_environment_block(ipc_port: Option<u16>, mcp_state_path: Option<&Path>) -> Vec<u16> {
|
2026-05-16 00:59:51 +08:00
|
|
|
|
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
2026-07-21 20:44:13 +08:00
|
|
|
|
.filter(|(key, _)| {
|
|
|
|
|
|
let key = key.to_string_lossy();
|
|
|
|
|
|
!key.eq_ignore_ascii_case("MCDEV_DEBUG_IPC_PORT")
|
2026-07-21 21:04:45 +08:00
|
|
|
|
&& !key.eq_ignore_ascii_case("MCDEV_MCP_BRIDGE_STATE")
|
2026-07-21 20:44:13 +08:00
|
|
|
|
})
|
2026-05-16 00:59:51 +08:00
|
|
|
|
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
|
|
|
|
|
.collect();
|
2026-07-21 20:44:13 +08:00
|
|
|
|
|
|
|
|
|
|
if let Some(port) = ipc_port {
|
|
|
|
|
|
pairs.push((
|
|
|
|
|
|
OsStr::new("MCDEV_DEBUG_IPC_PORT").encode_wide().collect(),
|
|
|
|
|
|
OsStr::new(&port.to_string()).encode_wide().collect(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-07-21 21:04:45 +08:00
|
|
|
|
if let Some(path) = mcp_state_path {
|
|
|
|
|
|
let state_path = path.to_string_lossy().replace('\\', "/");
|
|
|
|
|
|
pairs.push((
|
|
|
|
|
|
OsStr::new("MCDEV_MCP_BRIDGE_STATE").encode_wide().collect(),
|
|
|
|
|
|
OsStr::new(&state_path).encode_wide().collect(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-05-16 00:59:51 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|