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::() 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::() 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(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 { let mut pairs: Vec<(Vec, Vec)> = 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 { value.encode_wide().chain(std::iter::once(0)).collect() }