use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use dbgscope::dbgeng::DebugEngine;
const LAUNCH: &str = "cmd.exe /c ping -n 30 127.0.0.1";
const WAIT_MS: u32 = 30_000;
const EXTRA_PUMPS: usize = 8;
fn a_process_to_attach_to() -> Child {
Command::new("ping")
.args(["-n", "30", "127.0.0.1"])
.stdout(Stdio::null())
.spawn()
.expect("could not start a process to attach to")
}
fn listed(e: &DebugEngine) -> (usize, String) {
let text = e.execute_command("|").unwrap_or_default();
(text.lines().filter(|l| l.contains("id:")).count(), text)
}
fn created_pid(listing: &str) -> Option<u64> {
let row = listing.lines().find(|l| l.contains("\tcreate\t"))?;
let after = row.split("id: ").nth(1)?;
u64::from_str_radix(after.split_whitespace().next()?, 16).ok()
}
fn last_event_pid(e: &DebugEngine) -> Option<u64> {
let text = e.execute_command(".lastevent").ok()?;
let line = text.lines().find(|l| l.contains("Last event:"))?;
let after = line.split("Last event: ").nth(1)?;
u64::from_str_radix(after.split('.').next()?, 16).ok()
}
fn pump_until(e: &DebugEngine, want: usize) -> (Option<usize>, usize) {
for pump in 1..=EXTRA_PUMPS {
let started = Instant::now();
let waited = e.wait_for_event(WAIT_MS);
let (count, _) = listed(e);
println!(
" pump {pump}: wait -> {:?} in {:?}, now {count} process(es)",
waited.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
if count >= want {
return (Some(pump), count);
}
}
let (count, _) = listed(e);
(None, count)
}
fn load(n: usize) -> Vec<Child> {
(0..n)
.map(|_| {
Command::new("cmd.exe")
.args(["/c", "for /l %i in (1,0,2) do @rem"])
.stdout(Stdio::null())
.spawn()
.expect("could not start a load generator")
})
.collect()
}
fn natural(rounds: usize, attach_first: bool) {
let order = if attach_first {
"attach_process then launch_process"
} else {
"launch_process then attach_process"
};
println!("=== A. {order} x{rounds} ===");
let mut short = 0usize;
let mut recovered = 0usize;
for round in 1..=rounds {
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let launched = if attach_first {
e.attach_process(theirs.id()).expect("attach failed");
e.launch_process(LAUNCH)
} else {
let launched = e.launch_process(LAUNCH);
e.attach_process(theirs.id()).expect("attach failed");
launched
};
let (count, text) = listed(&e);
if count < 2 {
short += 1;
println!(" round {round}: openers -> {launched:?}, {count} process(es):\n{text}");
println!(
" .lastevent => {}",
e.execute_command(".lastevent")
.unwrap_or_else(|err| err.to_string())
.trim()
);
let (pumps, after) = pump_until(&e, 2);
match pumps {
Some(n) => {
recovered += 1;
println!(" => the launched process arrived on pump {n}");
}
None => println!(" => still {after} after {EXTRA_PUMPS} pumps"),
}
}
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
println!(" {order}: short {short}/{rounds}, recovered by pumping {recovered}/{short}");
}
fn forced() {
println!("\n=== B. attach_process_begin + launch_process_begin, then one wait ===");
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let attach = e.attach_process_begin(theirs.id()).expect("attach failed");
let launch = e.launch_process_begin(LAUNCH).expect("launch failed");
drop(attach);
drop(launch);
let started = Instant::now();
let waited = e.wait_for_event(WAIT_MS);
let (count, text) = listed(&e);
println!(
" one wait -> {:?} in {:?}, {count} process(es):\n{text}",
waited.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
if count < 2 {
let (pumps, after) = pump_until(&e, 2);
match pumps {
Some(n) => println!(" => the second target arrived on pump {n}"),
None => println!(" => still {after} after {EXTRA_PUMPS} pumps"),
}
}
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
std::thread::sleep(Duration::from_millis(200));
}
fn membership_against_the_break(rounds: usize) {
println!("=== G. does the launch's own event end the wait? x{rounds} ===");
let mut asked = 0usize;
let mut elsewhere = 0usize;
for round in 1..=rounds {
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
e.attach_process(theirs.id()).expect("attach failed");
e.launch_process(LAUNCH).expect("launch failed");
let (count, text) = listed(&e);
match (count, created_pid(&text), last_event_pid(&e)) {
(2, Some(ours), Some(event)) => {
asked += 1;
if event != ours {
elsewhere += 1;
println!(
" round {round}: the launch returned on {event:#x}'s event, not its \
own ({ours:#x}):\n{text}"
);
}
}
other => println!(" round {round}: could not ask ({other:?}):\n{text}"),
}
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
println!(" ended on another target's event: {elsewhere}/{asked} asked, of {rounds} rounds");
}
fn listed_before_the_wait() {
println!("\n=== E. is a pending target listed before the wait that completes it? ===");
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
e.launch_process(LAUNCH).expect("launch failed");
let (before, _) = listed(&e);
let pending = e.attach_process_begin(theirs.id()).expect("attach failed");
let (after_begin, text) = listed(&e);
println!(" attach_process_begin: {before} process(es) -> {after_begin}:\n{text}");
println!(
" is the attached pid {} listed before the wait? {}",
theirs.id(),
text.contains(&format!("id: {:x}\t", theirs.id()))
);
pending.wait().expect("attach wait failed");
let (after_wait, text) = listed(&e);
println!(" after the wait: {after_wait} process(es):\n{text}");
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
fn a_target_that_arrived_before_its_wait() {
println!("\n=== F. wait() on a guard whose target is already in the session ===");
let e = DebugEngine::new();
let pending = e.launch_process_begin(LAUNCH).expect("launch failed");
e.wait_for_event(WAIT_MS).expect("the outside pump failed");
let (count, _) = listed(&e);
let started = Instant::now();
let waited = pending.wait();
println!(
" {count} process(es) before wait(); wait() -> {:?} in {:?}",
waited.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
let _ = e.end_session();
}
fn a_guard_whose_event_was_overwritten() {
println!("\n=== H. wait() on a guard whose stop was overwritten by another target ===");
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let launch = e.launch_process_begin(LAUNCH).expect("launch failed");
e.wait_for_event(WAIT_MS).expect("the launch pump failed");
let attach = e
.attach_process_begin(theirs.id())
.expect("attach begin failed");
e.wait_for_event(WAIT_MS).expect("the attach pump failed");
drop(attach);
let (count, text) = listed(&e);
println!(
" {count} process(es); last stopped on pid {:?}, the launch is pid {:?}",
last_event_pid(&e),
created_pid(&text)
);
let started = Instant::now();
let waited = launch.wait();
println!(
" launch.wait() -> {:?} in {:?}",
waited.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
fn what_a_wait_can_conclude() {
println!("=== I. what a wait can conclude ===");
fn wait_once(what: &str, e: &DebugEngine) {
let started = Instant::now();
let waited = e.wait_for_event(300);
println!(
" {what}: wait(300) -> {:?} in {:?}; has_target {:?}",
waited
.as_ref()
.map(|outcome| format!("{outcome:?}"))
.map_err(|err| err.to_string()),
started.elapsed(),
e.has_target()
);
}
let e = DebugEngine::new();
wait_once("fresh engine, never had a target", &e);
drop(e);
let e = DebugEngine::new();
e.launch_process(LAUNCH).expect("launch failed");
e.end_session().expect("end_session failed");
wait_once("after end_session", &e);
drop(e);
for (what, opened) in [("launch", true), ("attach", false)] {
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let begun = if opened {
e.launch_process_begin(LAUNCH).map(drop)
} else {
e.attach_process_begin(theirs.id()).map(drop)
};
println!(
" pending {what}: begin {:?}, has_target before the wait {:?}",
begun.as_ref().map(|()| "Ok").map_err(|err| err.to_string()),
e.has_target()
);
wait_once(&format!("pending {what}"), &e);
}
let _ = theirs.kill();
let _ = theirs.wait();
}
let e = DebugEngine::new();
println!(
" attach to a pid nothing owns: begin {:?}",
e.attach_process_begin(0x000f_4240)
.map(drop)
.map_err(|err| err.to_string())
);
drop(e);
let e = DebugEngine::new();
e.launch_process(LAUNCH).expect("launch failed");
wait_once("a stopped target, nothing left to report", &e);
}
fn a_launch_that_cannot_start() {
println!("\n=== C. launch_process on an image that does not exist ===");
let e = DebugEngine::new();
let started = Instant::now();
let launched = e.launch_process("no_such_program_xyzzy.exe");
println!(
" launch_process -> {launched:?} in {:?}",
started.elapsed()
);
println!(" has_target -> {:?}", e.has_target());
let (count, text) = listed(&e);
println!(" {count} process(es): {}", text.trim());
let started = Instant::now();
let again = e.wait_for_event(WAIT_MS);
println!(
" a further wait -> {:?} in {:?}",
again.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
let _ = e.end_session();
}
fn forced_by_interrupt(rounds: usize) {
println!("\n=== D. attach, launch_process_begin, SetInterrupt, then one wait x{rounds} ===");
for round in 1..=rounds {
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
e.attach_process(theirs.id()).expect("attach failed");
let pending = e.launch_process_begin(LAUNCH).expect("launch failed");
let _ = e.interrupt_handle().interrupt();
let started = Instant::now();
let waited = pending.wait();
let (count, text) = listed(&e);
println!(
" round {round}: wait -> {:?} in {:?}, {count} process(es)",
waited.as_ref().map(|_| "Ok").map_err(|err| err.to_string()),
started.elapsed()
);
if count < 2 {
println!("{text}");
println!(
" .lastevent => {}",
e.execute_command(".lastevent")
.unwrap_or_else(|err| err.to_string())
.trim()
);
let (pumps, after) = pump_until(&e, 2);
match pumps {
Some(n) => println!(" => the launched process arrived on pump {n}"),
None => println!(" => still {after} after {EXTRA_PUMPS} pumps"),
}
}
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
}
fn main() {
let mut args = std::env::args().skip(1);
let rounds: usize = args.next().and_then(|a| a.parse().ok()).unwrap_or(10);
let spinners: usize = args.next().and_then(|a| a.parse().ok()).unwrap_or(0);
let mut load = load(spinners);
if spinners > 0 {
println!("(running under {spinners} CPU spinners)");
}
membership_against_the_break(rounds.max(1));
a_guard_whose_event_was_overwritten();
a_target_that_arrived_before_its_wait();
listed_before_the_wait();
what_a_wait_can_conclude();
a_launch_that_cannot_start();
forced_by_interrupt(rounds.max(1));
forced();
natural(rounds, true);
natural(rounds, false);
for mut child in load.drain(..) {
let _ = child.kill();
let _ = child.wait();
}
}