Skip to content

Commit 595651b

Browse files
awakecodingCopilot
andcommitted
Add DbgEng process dump smoke workflow
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e9c6fc7 commit 595651b

11 files changed

Lines changed: 467 additions & 15 deletions

File tree

.github/workflows/smoke-tests.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
name: Smoke tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
jobs:
9+
dump-smoke:
10+
name: Process dump smoke test
11+
runs-on: windows-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Select stable Rust
18+
shell: pwsh
19+
run: rustup default stable
20+
21+
- name: Restore debugger dependencies
22+
shell: pwsh
23+
run: cargo xtask deps
24+
25+
- name: Build windbg-tool
26+
shell: pwsh
27+
run: cargo build -p windbg-tool
28+
29+
- name: Create and inspect ping dump
30+
shell: pwsh
31+
run: |
32+
$ErrorActionPreference = 'Stop'
33+
34+
$tool = Join-Path $PWD 'target\debug\windbg-tool.exe'
35+
$dumpDir = Join-Path $env:RUNNER_TEMP 'windbg-tool-smoke'
36+
New-Item -ItemType Directory -Path $dumpDir -Force | Out-Null
37+
$dumpPath = Join-Path $dumpDir 'ping.dmp'
38+
Remove-Item -Path $dumpPath -Force -ErrorAction SilentlyContinue
39+
40+
$ping = Start-Process `
41+
-FilePath 'C:\Windows\System32\ping.exe' `
42+
-ArgumentList @('127.0.0.1', '-n', '10') `
43+
-PassThru
44+
45+
try {
46+
Start-Sleep -Seconds 1
47+
48+
& $tool dump create `
49+
--process-id $ping.Id `
50+
--output $dumpPath `
51+
--kind mini `
52+
--overwrite | ConvertFrom-Json | Out-Null
53+
54+
if (-not (Test-Path $dumpPath)) {
55+
throw "Expected dump file was not created: $dumpPath"
56+
}
57+
58+
$dumpFile = Get-Item $dumpPath
59+
if ($dumpFile.Length -le 0) {
60+
throw "Dump file is empty: $dumpPath"
61+
}
62+
}
63+
finally {
64+
if (-not $ping.HasExited) {
65+
Stop-Process -Id $ping.Id -Force
66+
$ping.WaitForExit()
67+
}
68+
}
69+
70+
& $tool daemon ensure | ConvertFrom-Json | Out-Null
71+
72+
$opened = & $tool dump open $dumpPath | ConvertFrom-Json
73+
if (-not $opened.target_id) {
74+
throw "dump open did not return a target_id"
75+
}
76+
$target = $opened.target_id
77+
78+
$status = & $tool target status --target $target | ConvertFrom-Json
79+
if ($status.target.kind -ne 'dump') {
80+
throw "Expected dump target kind, got '$($status.target.kind)'"
81+
}
82+
83+
$modules = & $tool target modules --target $target | ConvertFrom-Json
84+
$pingModule = $modules.modules | Where-Object {
85+
($_.module_name -like '*ping*') -or
86+
($_.image_name -like '*ping.exe*') -or
87+
($_.loaded_image_name -like '*ping.exe*')
88+
} | Select-Object -First 1
89+
if (-not $pingModule) {
90+
throw "Expected loaded modules to include ping.exe"
91+
}
92+
93+
$threads = & $tool target threads --target $target | ConvertFrom-Json
94+
if (-not $threads.threads -or $threads.threads.Count -lt 1) {
95+
throw "Expected at least one thread in the dump"
96+
}
97+
98+
$registers = & $tool target registers --target $target | ConvertFrom-Json
99+
if (
100+
-not $registers.registers.instruction_offset -and
101+
-not $registers.registers.stack_offset -and
102+
-not $registers.registers.frame_offset
103+
) {
104+
throw "Expected at least one current-thread register offset in the dump"
105+
}
106+
107+
$stack = & $tool target stack --target $target --max-frames 8 | ConvertFrom-Json
108+
if (-not $stack.frames -or $stack.frames.Count -lt 1) {
109+
throw "Expected at least one stack frame in the dump"
110+
}
111+
112+
- name: Upload dump on failure
113+
if: failure()
114+
uses: actions/upload-artifact@v4
115+
with:
116+
name: ping-dump-smoke
117+
path: ${{ runner.temp }}\windbg-tool-smoke\ping.dmp
118+
if-no-files-found: ignore
119+
retention-days: 3

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Representative command areas:
7070
- Discovery: `discover`, `recipes`, `tools`, `schema`
7171
- Session and replay: `open`, `load`, `sessions`, `info`, `position set`, `step`, `replay to`
7272
- Analysis: `symbols diagnose`, `disasm`, `memory dump`, `memory strings`, `memory chase`, `stack recover`, `stack backtrace`
73-
- Platform helpers: `remote explain`, `dbgeng server`, `live launch`, `windbg status`
73+
- Platform helpers: `remote explain`, `dbgeng server`, `live launch`, `dump create`, `windbg status`
7474

7575
For a fuller CLI walkthrough, output-shaping flags, and command map, see [the CLI guide](docs/cli.md).
7676

crates/windbg-dbgeng/src/lib.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::Context;
1+
use anyhow::{bail, Context};
22
use serde::Serialize;
33
use std::path::PathBuf;
44

@@ -37,6 +37,27 @@ pub struct DumpOpenOptions {
3737
pub path: PathBuf,
3838
}
3939

40+
#[derive(Debug, Clone)]
41+
pub struct DumpWriteOptions {
42+
pub path: PathBuf,
43+
pub kind: DumpKind,
44+
pub overwrite: bool,
45+
}
46+
47+
#[derive(Debug, Clone)]
48+
pub struct ProcessDumpOptions {
49+
pub process_id: u32,
50+
pub initial_break_timeout_ms: u32,
51+
pub write: DumpWriteOptions,
52+
}
53+
54+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
55+
#[serde(rename_all = "snake_case")]
56+
pub enum DumpKind {
57+
Mini,
58+
Full,
59+
}
60+
4061
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4162
#[serde(rename_all = "snake_case")]
4263
pub enum LiveLaunchEnd {
@@ -61,6 +82,18 @@ pub struct LiveLaunchResult {
6182
pub end: LiveLaunchEnd,
6283
}
6384

85+
#[derive(Debug, Clone, Serialize)]
86+
pub struct DumpWriteResult {
87+
pub path: PathBuf,
88+
pub kind: DumpKind,
89+
pub qualifier: u32,
90+
pub format_flags: u32,
91+
pub overwrite: bool,
92+
pub target: String,
93+
pub process_id: Option<u32>,
94+
pub detached: bool,
95+
}
96+
6497
#[derive(Debug, Clone, Serialize)]
6598
pub struct DebuggerExecutionStatus {
6699
pub raw: Option<u32>,
@@ -196,6 +229,10 @@ pub fn open_dump_session(options: DumpOpenOptions) -> anyhow::Result<DebuggerSes
196229
open_dump_session_impl(options)
197230
}
198231

232+
pub fn write_process_dump(options: ProcessDumpOptions) -> anyhow::Result<DumpWriteResult> {
233+
write_process_dump_impl(options)
234+
}
235+
199236
#[cfg(windows)]
200237
pub struct DebuggerSession {
201238
kind: DebuggerSessionKind,
@@ -281,6 +318,23 @@ impl DebuggerSession {
281318
Ok(())
282319
}
283320

321+
pub fn write_dump(&self, options: DumpWriteOptions) -> anyhow::Result<DumpWriteResult> {
322+
if self.kind != DebuggerSessionKind::Live {
323+
bail!("DbgEng dump writing requires a live target session");
324+
}
325+
write_dump_file(&self.client, &options)?;
326+
Ok(DumpWriteResult {
327+
path: options.path,
328+
kind: options.kind,
329+
qualifier: dump_kind_qualifier(options.kind),
330+
format_flags: dump_format_flags(options.overwrite),
331+
overwrite: options.overwrite,
332+
target: self.target.clone(),
333+
process_id: self.current_process_system_id().ok().or(self.process_id),
334+
detached: false,
335+
})
336+
}
337+
284338
pub fn core_registers(&self) -> anyhow::Result<CoreRegisterState> {
285339
let instruction_offset = unsafe { self.registers.GetInstructionOffset().ok() };
286340
let stack_offset = unsafe { self.registers.GetStackOffset().ok() };
@@ -699,6 +753,10 @@ impl DebuggerSession {
699753
anyhow::bail!("DbgEng sessions are only supported on Windows")
700754
}
701755

756+
pub fn write_dump(&self, _options: DumpWriteOptions) -> anyhow::Result<DumpWriteResult> {
757+
anyhow::bail!("DbgEng dump writing is only supported on Windows")
758+
}
759+
702760
pub fn core_registers(&self) -> anyhow::Result<CoreRegisterState> {
703761
anyhow::bail!("DbgEng sessions are only supported on Windows")
704762
}
@@ -952,6 +1010,27 @@ fn open_dump_session_impl(options: DumpOpenOptions) -> anyhow::Result<DebuggerSe
9521010
})
9531011
}
9541012

1013+
#[cfg(windows)]
1014+
fn write_process_dump_impl(options: ProcessDumpOptions) -> anyhow::Result<DumpWriteResult> {
1015+
let session = attach_live_session_impl(LiveAttachOptions {
1016+
process_id: options.process_id,
1017+
initial_break_timeout_ms: options.initial_break_timeout_ms,
1018+
})?;
1019+
let write_result = session.write_dump(options.write);
1020+
let detach_result = session.detach();
1021+
match (write_result, detach_result) {
1022+
(Ok(mut result), Ok(())) => {
1023+
result.detached = true;
1024+
Ok(result)
1025+
}
1026+
(Ok(_), Err(error)) => Err(error).context("dump was written, but DbgEng detach failed"),
1027+
(Err(error), Ok(())) => Err(error),
1028+
(Err(write_error), Err(detach_error)) => Err(write_error).with_context(|| {
1029+
format!("DbgEng detach also failed after dump write failed: {detach_error}")
1030+
}),
1031+
}
1032+
}
1033+
9551034
#[cfg(windows)]
9561035
fn read_wide_string<F>(mut reader: F) -> anyhow::Result<String>
9571036
where
@@ -999,6 +1078,26 @@ fn debug_value_type_name(value_type: u32) -> &'static str {
9991078
}
10001079
}
10011080

1081+
const DEBUG_DUMP_SMALL_VALUE: u32 = 1024;
1082+
const DEBUG_DUMP_FULL_VALUE: u32 = 1026;
1083+
const DEBUG_FORMAT_DEFAULT_VALUE: u32 = 0x0000_0000;
1084+
const DEBUG_FORMAT_NO_OVERWRITE_VALUE: u32 = 0x8000_0000;
1085+
1086+
fn dump_kind_qualifier(kind: DumpKind) -> u32 {
1087+
match kind {
1088+
DumpKind::Mini => DEBUG_DUMP_SMALL_VALUE,
1089+
DumpKind::Full => DEBUG_DUMP_FULL_VALUE,
1090+
}
1091+
}
1092+
1093+
fn dump_format_flags(overwrite: bool) -> u32 {
1094+
if overwrite {
1095+
DEBUG_FORMAT_DEFAULT_VALUE
1096+
} else {
1097+
DEBUG_FORMAT_NO_OVERWRITE_VALUE
1098+
}
1099+
}
1100+
10021101
fn encode_hex(bytes: &[u8]) -> String {
10031102
let mut result = String::with_capacity(bytes.len() * 2);
10041103
for byte in bytes {
@@ -1008,6 +1107,28 @@ fn encode_hex(bytes: &[u8]) -> String {
10081107
result
10091108
}
10101109

1110+
#[cfg(windows)]
1111+
fn write_dump_file(
1112+
client: &windows::Win32::System::Diagnostics::Debug::Extensions::IDebugClient5,
1113+
options: &DumpWriteOptions,
1114+
) -> anyhow::Result<()> {
1115+
use windows::core::PCWSTR;
1116+
1117+
let path_string = options.path.to_string_lossy().to_string();
1118+
let mut path = path_string.encode_utf16().collect::<Vec<_>>();
1119+
path.push(0);
1120+
unsafe {
1121+
client.WriteDumpFileWide(
1122+
PCWSTR(path.as_ptr()),
1123+
0,
1124+
dump_kind_qualifier(options.kind),
1125+
dump_format_flags(options.overwrite),
1126+
PCWSTR(std::ptr::null()),
1127+
)?;
1128+
}
1129+
Ok(())
1130+
}
1131+
10111132
#[cfg(not(windows))]
10121133
fn start_process_server_impl(options: ProcessServerOptions) -> anyhow::Result<ProcessServerResult> {
10131134
let _ = options;
@@ -1037,3 +1158,26 @@ fn open_dump_session_impl(options: DumpOpenOptions) -> anyhow::Result<DebuggerSe
10371158
let _ = options;
10381159
anyhow::bail!("DbgEng dump sessions are only supported on Windows")
10391160
}
1161+
1162+
#[cfg(not(windows))]
1163+
fn write_process_dump_impl(options: ProcessDumpOptions) -> anyhow::Result<DumpWriteResult> {
1164+
let _ = options;
1165+
anyhow::bail!("DbgEng dump writing is only supported on Windows")
1166+
}
1167+
1168+
#[cfg(test)]
1169+
mod tests {
1170+
use super::*;
1171+
1172+
#[test]
1173+
fn maps_dump_kinds_to_dbgeng_qualifiers() {
1174+
assert_eq!(dump_kind_qualifier(DumpKind::Mini), 1024);
1175+
assert_eq!(dump_kind_qualifier(DumpKind::Full), 1026);
1176+
}
1177+
1178+
#[test]
1179+
fn uses_no_overwrite_by_default() {
1180+
assert_eq!(dump_format_flags(false), 0x8000_0000);
1181+
assert_eq!(dump_format_flags(true), 0);
1182+
}
1183+
}

0 commit comments

Comments
 (0)