新增 stdio MCP server 和 mcp 子命令,通过调试会话状态文件连接内置 TCP bridge,并提供 place_block 工具。 Bridge 将请求投递到服务端 System 的游戏线程执行,限制请求大小与队列长度,并按会话端口安全清理状态文件和连接。
276 lines
8.0 KiB
Rust
276 lines
8.0 KiB
Rust
use std::{io, path::Path, thread};
|
||
|
||
use crate::error::{CliError, Result};
|
||
|
||
use super::{
|
||
config::{DebugConfig, ResolvedModDir},
|
||
hotreload::{HotReloadOptions, 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],
|
||
mcp_state_path: Option<&Path>,
|
||
) -> 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 = if ipc.is_some() || mcp_state_path.is_some() || config.auto_hot_reload_ui {
|
||
Some(build_environment_block(
|
||
ipc.as_ref().map(|server| server.port()),
|
||
mcp_state_path,
|
||
))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
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 = HotReloadTask::start(
|
||
pid,
|
||
mod_dirs,
|
||
ipc.clone(),
|
||
HotReloadOptions {
|
||
python: config.auto_hot_reload_mods,
|
||
json_ui: config.auto_hot_reload_ui,
|
||
},
|
||
);
|
||
|
||
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],
|
||
_mcp_state_path: Option<&Path>,
|
||
) -> 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: Option<u16>, mcp_state_path: Option<&Path>) -> Vec<u16> {
|
||
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
||
.filter(|(key, _)| {
|
||
let key = key.to_string_lossy();
|
||
!key.eq_ignore_ascii_case("MCDEV_DEBUG_IPC_PORT")
|
||
&& !key.eq_ignore_ascii_case("MCDEV_MCP_BRIDGE_STATE")
|
||
})
|
||
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
||
.collect();
|
||
|
||
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(),
|
||
));
|
||
}
|
||
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(),
|
||
));
|
||
}
|
||
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()
|
||
}
|