|
| 1 | +use std::ffi::c_void; |
| 2 | +use std::fs::{self, File}; |
| 3 | +use std::io::Write; |
| 4 | +use std::mem::{size_of, zeroed}; |
| 5 | +use std::ptr::{null, null_mut}; |
| 6 | + |
| 7 | +use windows_sys::Win32::Foundation::{BOOL, HANDLE, MAX_PATH}; |
| 8 | +use windows_sys::Win32::Storage::FileSystem::CREATE_ALWAYS; |
| 9 | +use windows_sys::Win32::System::Diagnostics::Debug::{ |
| 10 | + AddrModeFlat, MiniDumpNormal, MiniDumpWriteDump, StackWalk, SymCleanup, SymFromAddr, |
| 11 | + SymFunctionTableAccess, SymGetModuleBase, SymInitialize, EXCEPTION_POINTERS, |
| 12 | + MINIDUMP_EXCEPTION_INFORMATION, STACKFRAME, SYMBOL_INFO, |
| 13 | +}; |
| 14 | +use windows_sys::Win32::System::ProcessStatus::{GetModuleBaseNameA, GetModuleInformation, MODULEINFO}; |
| 15 | +use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentProcessId, GetCurrentThread, GetCurrentThreadId}; |
| 16 | + |
| 17 | +use super::log::{log_error, log_info}; |
| 18 | +use super::pe::get_self_base_dir; |
| 19 | + |
| 20 | +const EXCEPTION_EXECUTE_HANDLER: i32 = 1; |
| 21 | +const FILE_ATTRIBUTE_NORMAL: u32 = 0x80; |
| 22 | +const GENERIC_WRITE: u32 = 0x40000000; |
| 23 | +const IMAGE_FILE_MACHINE_I386: u32 = 0x014c; |
| 24 | + |
| 25 | +extern "system" { |
| 26 | + fn CreateDirectoryA(path: *const u8, security: *const c_void) -> i32; |
| 27 | + fn CreateFileA( |
| 28 | + name: *const u8, |
| 29 | + access: u32, |
| 30 | + share: u32, |
| 31 | + security: *const c_void, |
| 32 | + disposition: u32, |
| 33 | + flags: u32, |
| 34 | + template: *mut c_void, |
| 35 | + ) -> HANDLE; |
| 36 | + fn CloseHandle(handle: HANDLE) -> BOOL; |
| 37 | + fn GetLocalTime(st: *mut SYSTEMTIME); |
| 38 | + fn SetUnhandledExceptionFilter( |
| 39 | + filter: Option<unsafe extern "system" fn(*mut EXCEPTION_POINTERS) -> i32>, |
| 40 | + ) -> Option<unsafe extern "system" fn(*mut EXCEPTION_POINTERS) -> i32>; |
| 41 | +} |
| 42 | + |
| 43 | +#[repr(C)] |
| 44 | +struct SYSTEMTIME { |
| 45 | + w_year: u16, |
| 46 | + w_month: u16, |
| 47 | + w_day_of_week: u16, |
| 48 | + w_day: u16, |
| 49 | + w_hour: u16, |
| 50 | + w_minute: u16, |
| 51 | + w_second: u16, |
| 52 | + w_milliseconds: u16, |
| 53 | +} |
| 54 | + |
| 55 | +#[cfg(target_arch = "x86")] |
| 56 | +type NativeContext = windows_sys::Win32::System::Diagnostics::Debug::CONTEXT; |
| 57 | + |
| 58 | +pub unsafe fn install() { |
| 59 | + SetUnhandledExceptionFilter(Some(unhandled_exception_filter)); |
| 60 | + log_info("crash dump handler installed"); |
| 61 | +} |
| 62 | + |
| 63 | +unsafe extern "system" fn unhandled_exception_filter(exception: *mut EXCEPTION_POINTERS) -> i32 { |
| 64 | + if let Err(err) = write_crash_report(exception) { |
| 65 | + log_error(&format!("failed to write crash dump: {}", err)); |
| 66 | + } |
| 67 | + EXCEPTION_EXECUTE_HANDLER |
| 68 | +} |
| 69 | + |
| 70 | +unsafe fn write_crash_report(exception: *mut EXCEPTION_POINTERS) -> Result<(), String> { |
| 71 | + let base_dir = get_self_base_dir().ok_or_else(|| "cannot resolve base dir".to_string())?; |
| 72 | + let crash_dir = format!("{}\\mods\\crash", base_dir); |
| 73 | + CreateDirectoryA(format!("{}\0", format!("{}\\mods", base_dir)).as_ptr(), null()); |
| 74 | + CreateDirectoryA(format!("{}\0", crash_dir).as_ptr(), null()); |
| 75 | + |
| 76 | + let stamp = timestamp(); |
| 77 | + let dump_path = format!("{}\\crash_{}.dmp", crash_dir, stamp); |
| 78 | + let log_path = format!("{}\\crash_{}.log", crash_dir, stamp); |
| 79 | + |
| 80 | + write_minidump(&dump_path, exception)?; |
| 81 | + write_text_log(&log_path, exception, &dump_path)?; |
| 82 | + log_error(&format!("crash dump written: {}", dump_path)); |
| 83 | + Ok(()) |
| 84 | +} |
| 85 | + |
| 86 | +unsafe fn write_minidump(path: &str, exception: *mut EXCEPTION_POINTERS) -> Result<(), String> { |
| 87 | + let path_c = format!("{}\0", path); |
| 88 | + let file = CreateFileA( |
| 89 | + path_c.as_ptr(), |
| 90 | + GENERIC_WRITE, |
| 91 | + 0, |
| 92 | + null(), |
| 93 | + CREATE_ALWAYS, |
| 94 | + FILE_ATTRIBUTE_NORMAL, |
| 95 | + null_mut(), |
| 96 | + ); |
| 97 | + if file == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { |
| 98 | + return Err(format!("CreateFileA failed for {}", path)); |
| 99 | + } |
| 100 | + |
| 101 | + let mut exception_info = MINIDUMP_EXCEPTION_INFORMATION { |
| 102 | + ThreadId: GetCurrentThreadId(), |
| 103 | + ExceptionPointers: exception, |
| 104 | + ClientPointers: 0, |
| 105 | + }; |
| 106 | + let ok = MiniDumpWriteDump( |
| 107 | + GetCurrentProcess(), |
| 108 | + GetCurrentProcessId(), |
| 109 | + file, |
| 110 | + MiniDumpNormal, |
| 111 | + &mut exception_info, |
| 112 | + null_mut(), |
| 113 | + null_mut(), |
| 114 | + ); |
| 115 | + CloseHandle(file); |
| 116 | + if ok == 0 { |
| 117 | + return Err("MiniDumpWriteDump failed".to_string()); |
| 118 | + } |
| 119 | + Ok(()) |
| 120 | +} |
| 121 | + |
| 122 | +unsafe fn write_text_log(path: &str, exception: *mut EXCEPTION_POINTERS, dump_path: &str) -> Result<(), String> { |
| 123 | + let mut file = File::create(path).map_err(|err| err.to_string())?; |
| 124 | + writeln!(file, "ChuModLoader crash report").map_err(|err| err.to_string())?; |
| 125 | + writeln!(file, "dump: {}", dump_path).map_err(|err| err.to_string())?; |
| 126 | + |
| 127 | + if exception.is_null() || (*exception).ExceptionRecord.is_null() { |
| 128 | + writeln!(file, "exception: <null>").map_err(|err| err.to_string())?; |
| 129 | + return Ok(()); |
| 130 | + } |
| 131 | + |
| 132 | + let record = &*(*exception).ExceptionRecord; |
| 133 | + writeln!(file, "exception_code: 0x{:08X}", record.ExceptionCode).map_err(|err| err.to_string())?; |
| 134 | + writeln!(file, "exception_address: 0x{:08X}", record.ExceptionAddress as usize).map_err(|err| err.to_string())?; |
| 135 | + if let Some(module) = module_offset(record.ExceptionAddress as usize) { |
| 136 | + writeln!(file, "exception_module: {}+0x{:X}", module.0, module.1).map_err(|err| err.to_string())?; |
| 137 | + } |
| 138 | + |
| 139 | + #[cfg(target_arch = "x86")] |
| 140 | + if !(*exception).ContextRecord.is_null() { |
| 141 | + write_registers(&mut file, &*((*exception).ContextRecord as *const NativeContext))?; |
| 142 | + write_stack_trace(&mut file, &mut *((*exception).ContextRecord as *mut NativeContext))?; |
| 143 | + } |
| 144 | + |
| 145 | + #[cfg(not(target_arch = "x86"))] |
| 146 | + writeln!(file, "registers/stack: unsupported target arch").map_err(|err| err.to_string())?; |
| 147 | + |
| 148 | + Ok(()) |
| 149 | +} |
| 150 | + |
| 151 | +#[cfg(target_arch = "x86")] |
| 152 | +fn write_registers(file: &mut File, ctx: &NativeContext) -> Result<(), String> { |
| 153 | + writeln!(file, "registers:").map_err(|err| err.to_string())?; |
| 154 | + writeln!(file, " EAX=0x{:08X} EBX=0x{:08X} ECX=0x{:08X} EDX=0x{:08X}", ctx.Eax, ctx.Ebx, ctx.Ecx, ctx.Edx).map_err(|err| err.to_string())?; |
| 155 | + writeln!(file, " ESI=0x{:08X} EDI=0x{:08X} EBP=0x{:08X} ESP=0x{:08X}", ctx.Esi, ctx.Edi, ctx.Ebp, ctx.Esp).map_err(|err| err.to_string())?; |
| 156 | + writeln!(file, " EIP=0x{:08X} EFLAGS=0x{:08X}", ctx.Eip, ctx.EFlags).map_err(|err| err.to_string())?; |
| 157 | + Ok(()) |
| 158 | +} |
| 159 | + |
| 160 | +#[cfg(target_arch = "x86")] |
| 161 | +unsafe fn write_stack_trace(file: &mut File, ctx: &mut NativeContext) -> Result<(), String> { |
| 162 | + writeln!(file, "stack_trace:").map_err(|err| err.to_string())?; |
| 163 | + let process = GetCurrentProcess(); |
| 164 | + let thread = GetCurrentThread(); |
| 165 | + SymInitialize(process, null(), 1); |
| 166 | + |
| 167 | + let mut frame: STACKFRAME = zeroed(); |
| 168 | + frame.AddrPC.Offset = ctx.Eip; |
| 169 | + frame.AddrPC.Mode = AddrModeFlat; |
| 170 | + frame.AddrFrame.Offset = ctx.Ebp; |
| 171 | + frame.AddrFrame.Mode = AddrModeFlat; |
| 172 | + frame.AddrStack.Offset = ctx.Esp; |
| 173 | + frame.AddrStack.Mode = AddrModeFlat; |
| 174 | + |
| 175 | + for index in 0..64 { |
| 176 | + let ok = StackWalk( |
| 177 | + IMAGE_FILE_MACHINE_I386, |
| 178 | + process, |
| 179 | + thread, |
| 180 | + &mut frame, |
| 181 | + ctx as *mut _ as *mut c_void, |
| 182 | + None, |
| 183 | + Some(SymFunctionTableAccess), |
| 184 | + Some(SymGetModuleBase), |
| 185 | + None, |
| 186 | + ); |
| 187 | + if ok == 0 || frame.AddrPC.Offset == 0 { |
| 188 | + break; |
| 189 | + } |
| 190 | + let addr = frame.AddrPC.Offset; |
| 191 | + let symbol = symbol_from_addr(process, addr as u64).unwrap_or_else(|| module_offset(addr as usize).map(|(m, o)| format!("{}+0x{:X}", m, o)).unwrap_or_else(|| "<unknown>".to_string())); |
| 192 | + writeln!(file, " #{:02} 0x{:08X} {}", index, addr as u32, symbol).map_err(|err| err.to_string())?; |
| 193 | + } |
| 194 | + |
| 195 | + SymCleanup(process); |
| 196 | + Ok(()) |
| 197 | +} |
| 198 | + |
| 199 | +#[cfg(target_arch = "x86")] |
| 200 | +unsafe fn symbol_from_addr(process: HANDLE, addr: u64) -> Option<String> { |
| 201 | + let mut storage = [0u8; size_of::<SYMBOL_INFO>() + 512]; |
| 202 | + let symbol = storage.as_mut_ptr() as *mut SYMBOL_INFO; |
| 203 | + (*symbol).SizeOfStruct = size_of::<SYMBOL_INFO>() as u32; |
| 204 | + (*symbol).MaxNameLen = 511; |
| 205 | + let mut displacement = 0u64; |
| 206 | + if SymFromAddr(process, addr, &mut displacement, symbol) == 0 { |
| 207 | + return None; |
| 208 | + } |
| 209 | + let name_ptr = (*symbol).Name.as_ptr() as *const u8; |
| 210 | + let len = (0..511).position(|i| *name_ptr.add(i) == 0).unwrap_or(511); |
| 211 | + let name = String::from_utf8_lossy(std::slice::from_raw_parts(name_ptr, len)).into_owned(); |
| 212 | + Some(format!("{}+0x{:X}", name, displacement)) |
| 213 | +} |
| 214 | + |
| 215 | +unsafe fn module_offset(addr: usize) -> Option<(String, usize)> { |
| 216 | + let module = module_from_address(addr)?; |
| 217 | + let mut name = [0u8; MAX_PATH as usize]; |
| 218 | + let len = GetModuleBaseNameA( |
| 219 | + GetCurrentProcess(), |
| 220 | + module, |
| 221 | + name.as_mut_ptr(), |
| 222 | + name.len() as u32, |
| 223 | + ); |
| 224 | + let module_name = if len == 0 { |
| 225 | + "<module>".to_string() |
| 226 | + } else { |
| 227 | + String::from_utf8_lossy(&name[..len as usize]).into_owned() |
| 228 | + }; |
| 229 | + Some((module_name, addr.saturating_sub(module as usize))) |
| 230 | +} |
| 231 | + |
| 232 | +unsafe fn module_from_address(addr: usize) -> Option<*mut c_void> { |
| 233 | + let mut module = null_mut(); |
| 234 | + let flags = 0x00000004u32 | 0x00000002u32; |
| 235 | + let ok = windows_sys::Win32::System::LibraryLoader::GetModuleHandleExA( |
| 236 | + flags, |
| 237 | + addr as *const u8, |
| 238 | + &mut module, |
| 239 | + ); |
| 240 | + if ok == 0 || module.is_null() { |
| 241 | + return None; |
| 242 | + } |
| 243 | + let mut info: MODULEINFO = zeroed(); |
| 244 | + if GetModuleInformation(GetCurrentProcess(), module, &mut info, size_of::<MODULEINFO>() as u32) == 0 { |
| 245 | + return None; |
| 246 | + } |
| 247 | + Some(module) |
| 248 | +} |
| 249 | + |
| 250 | +unsafe fn timestamp() -> String { |
| 251 | + let mut st: SYSTEMTIME = zeroed(); |
| 252 | + GetLocalTime(&mut st); |
| 253 | + format!( |
| 254 | + "{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}", |
| 255 | + st.w_year, st.w_month, st.w_day, st.w_hour, st.w_minute, st.w_second, st.w_milliseconds |
| 256 | + ) |
| 257 | +} |
| 258 | + |
| 259 | +pub fn log_panic_context(scope: &str, name: &str) { |
| 260 | + let base_dir = get_self_base_dir().unwrap_or_default(); |
| 261 | + if base_dir.is_empty() { |
| 262 | + log_error(&format!("panic caught in {}: {}", scope, name)); |
| 263 | + return; |
| 264 | + } |
| 265 | + let crash_dir = format!("{}\\mods\\crash", base_dir); |
| 266 | + let _ = fs::create_dir_all(&crash_dir); |
| 267 | + let path = format!("{}\\panic_{}.log", crash_dir, unsafe { timestamp() }); |
| 268 | + if let Ok(mut file) = File::create(&path) { |
| 269 | + let _ = writeln!(file, "ChuModLoader panic context"); |
| 270 | + let _ = writeln!(file, "scope: {}", scope); |
| 271 | + let _ = writeln!(file, "mod: {}", name); |
| 272 | + } |
| 273 | + log_error(&format!("panic caught in {}: {} (context={})", scope, name, path)); |
| 274 | +} |
0 commit comments