Files
emod-cli/src/debug/process.rs
Blank038 1a7bc6aca1 feat(mcp): 增加游戏内方块调试桥接
新增 stdio MCP server 和 mcp 子命令,通过调试会话状态文件连接内置 TCP bridge,并提供 place_block 工具。

Bridge 将请求投递到服务端 System 的游戏线程执行,限制请求大小与队列长度,并按会话端口安全清理状态文件和连接。
2026-07-21 21:04:45 +08:00

276 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}