Skip to content

Commit b677936

Browse files
quark-zjumeta-codesync[bot]
authored andcommitted
backtrace-python: support libpython filter on Windows
Summary: Implemented by claude. I'm not familiar with the win32 APIs. But it seems to do the job well. Another approach would be filtering out by name. Since we already have the memory segments filtering on other platforms, it seems consistent to implement the Windows filtering using memory segments too. Reviewed By: muirdm Differential Revision: D91844715 fbshipit-source-id: 5b73d0d78643ef1a18e41e315da8a0b76c6a486b
1 parent 2949827 commit b677936

3 files changed

Lines changed: 135 additions & 2 deletions

File tree

eden/scm/lib/backtrace-python/BUCK

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ rust_library(
3737
}},
3838
crate_root = "src/lib.rs",
3939
mapped_srcs = {":gen_offsets.rs": "src/offsets.rs"},
40+
os_deps = [
41+
(
42+
"windows",
43+
[
44+
"fbsource//third-party/rust:winapi",
45+
],
46+
),
47+
],
4048
deps = [
4149
"fbsource//third-party/rust:libc",
4250
"//eden/scm/lib/backtrace-ext:backtrace-ext",

eden/scm/lib/backtrace-python/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ sapling-evalframe-sys = { version = "0.1.0", path = "evalframe-sys" }
2020

2121
[build-dependencies]
2222
sapling-backtrace-python-offset-probe = { version = "0.1.0", path = "offset-probe" }
23+
24+
[target.'cfg(target_os = "windows")'.dependencies]
25+
winapi = { version = "0.3", features = ["everything", "std"] }

eden/scm/lib/backtrace-python/src/libpython_filter.rs

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ fn get_python_ranges() -> Vec<Range> {
5858
{
5959
parse_dyld_images()
6060
}
61-
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
61+
#[cfg(target_os = "windows")]
62+
{
63+
parse_windows_modules()
64+
}
65+
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
6266
{
6367
Default::default()
6468
}
@@ -178,6 +182,98 @@ fn parse_dyld_images() -> Vec<Range> {
178182
ranges
179183
}
180184

185+
/// Parse loaded modules to find libpython and .pyd/.dll ranges
186+
#[cfg(target_os = "windows")]
187+
fn parse_windows_modules() -> Vec<Range> {
188+
use std::mem::MaybeUninit;
189+
use std::ptr;
190+
191+
use winapi::shared::minwindef::DWORD;
192+
use winapi::shared::minwindef::FALSE;
193+
use winapi::shared::minwindef::HMODULE;
194+
use winapi::shared::minwindef::MAX_PATH;
195+
use winapi::um::processthreadsapi::GetCurrentProcess;
196+
use winapi::um::psapi::EnumProcessModules;
197+
use winapi::um::psapi::GetModuleFileNameExW;
198+
use winapi::um::psapi::GetModuleInformation;
199+
use winapi::um::psapi::MODULEINFO;
200+
201+
let mut ranges = Vec::new();
202+
203+
let process = unsafe { GetCurrentProcess() };
204+
205+
// First, get the number of modules
206+
let mut bytes_needed: DWORD = 0;
207+
if unsafe { EnumProcessModules(process, ptr::null_mut(), 0, &mut bytes_needed) } == FALSE {
208+
return ranges;
209+
}
210+
211+
let module_count = bytes_needed as usize / std::mem::size_of::<HMODULE>();
212+
if module_count == 0 {
213+
return ranges;
214+
}
215+
216+
// Allocate buffer for module handles
217+
let mut modules: Vec<HMODULE> = vec![ptr::null_mut(); module_count];
218+
219+
if unsafe {
220+
EnumProcessModules(
221+
process,
222+
modules.as_mut_ptr(),
223+
bytes_needed,
224+
&mut bytes_needed,
225+
)
226+
} == FALSE
227+
{
228+
return ranges;
229+
}
230+
231+
// Iterate through modules
232+
for &module in &modules {
233+
if module.is_null() {
234+
continue;
235+
}
236+
237+
// Get module filename
238+
let mut filename: [u16; MAX_PATH] = [0; MAX_PATH];
239+
let len = unsafe {
240+
GetModuleFileNameExW(process, module, filename.as_mut_ptr(), MAX_PATH as DWORD)
241+
};
242+
243+
if len == 0 {
244+
continue;
245+
}
246+
247+
let path = String::from_utf16_lossy(&filename[..len as usize]);
248+
249+
if !is_python_library_path(&path) {
250+
continue;
251+
}
252+
253+
// Get module information (base address and size)
254+
let mut mod_info: MaybeUninit<MODULEINFO> = MaybeUninit::uninit();
255+
if unsafe {
256+
GetModuleInformation(
257+
process,
258+
module,
259+
mod_info.as_mut_ptr(),
260+
std::mem::size_of::<MODULEINFO>() as DWORD,
261+
)
262+
} == FALSE
263+
{
264+
continue;
265+
}
266+
267+
let mod_info = unsafe { mod_info.assume_init() };
268+
let start = mod_info.lpBaseOfDll as usize;
269+
let end = start + mod_info.SizeOfImage as usize;
270+
ranges.push(Range { start, end });
271+
}
272+
273+
ranges.sort();
274+
ranges
275+
}
276+
181277
/// Check if a pathname is a Python library or extension
182278
fn is_python_library_path(path: &str) -> bool {
183279
// Linux examples:
@@ -189,5 +285,31 @@ fn is_python_library_path(path: &str) -> bool {
189285
// /usr/local/Cellar/python@3.11/3.11.4/Frameworks/Python.framework/Versions/3.11/Python
190286
// /Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload/_json.cpython-311-darwin.so
191287
// /opt/homebrew/lib/python3.11/site-packages/numpy/.dylibs/libopenblas64_.0.dylib
192-
path.contains("/libpython") || path.contains("/python") || path.contains("/Python.framework/")
288+
//
289+
// Windows examples:
290+
// C:\Python311\python311.dll
291+
// C:\Python311\python3.dll
292+
// C:\Python311\DLLs\_socket.pyd
293+
// C:\Users\...\AppData\Local\Programs\Python\Python311\python311.dll
294+
// C:\Users\...\site-packages\numpy\core\_multiarray_umath.cp311-win_amd64.pyd
295+
296+
// Unix-style paths
297+
if path.contains("/libpython")
298+
|| path.contains("/python")
299+
|| path.contains("/Python.framework/")
300+
{
301+
return true;
302+
}
303+
304+
// Windows-style paths (use backslash)
305+
if path.contains("\\python") || path.contains("\\Python") {
306+
return true;
307+
}
308+
309+
// .pyd files are Python extension modules on Windows
310+
if path.ends_with(".pyd") {
311+
return true;
312+
}
313+
314+
false
193315
}

0 commit comments

Comments
 (0)