ntoseye 0.32.0

WinDbg-like kernel debugger for Windows, from Linux and macOS
Documentation
use std::borrow::Cow;

use crate::expr::Expr;
use crate::repl::*;
use crate::target::pnp::{
    DevNodeDetail, DevNodeSummary, DeviceStackDetail, PnpTriageDetail, StateHistoryEntry,
};
use crate::ui;

const HISTORY_DISPLAY_LIMIT: usize = 5;

repl_command! {
    cmd_devnode;
    names: ["!devnode", "devnode"],
    usage: "!devnode [node|0] [-r]",
    summary: "Display a PnP device node and optionally its subtree.",
    details: "With no node (or 0), displays the root device node. -r and the trailing WinDbg-style 1 walk the node's subtree.",
    completion: Expression,
}

repl_command! {
    cmd_devstack;
    names: ["!devstack", "devstack"],
    usage: "!devstack <device-object|devnode>",
    summary: "Display the device stack for a DEVICE_OBJECT or device node.",
    details: "The argument may be a DEVICE_OBJECT or a DEVICE_NODE; the stack is shown from the top filter down to the PDO.",
    completion: Expression,
}

repl_command! {
    cmd_pnptriage;
    names: ["!pnptriage", "pnptriage"],
    usage: "!pnptriage",
    summary: "Report PnP device nodes with problems, pending IRPs, or incomplete starts.",
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct DevNodeRequest {
    node: Option<String>,
    recursive: bool,
}

fn parse_devnode_request(argv: &[Cow<'_, str>]) -> Option<DevNodeRequest> {
    let mut node = None;
    let mut root = false;
    let mut recursive = false;

    for argument in argv {
        match argument.as_ref() {
            "-r" | "1" => {
                if recursive {
                    return None;
                }
                recursive = true;
            }
            "0" => {
                if node.is_some() || root {
                    return None;
                }
                root = true;
            }
            value if value.starts_with('-') => return None,
            value if node.is_none() => node = Some(value.to_string()),
            _ => return None,
        }
    }

    Some(DevNodeRequest { node, recursive })
}

fn state_text(code: u32, name: &str) -> String {
    if name == format!("{code:#x}") {
        name.to_string()
    } else {
        format!("{name} ({code:#x})")
    }
}

fn problem_text(code: u32, name: Option<&str>) -> String {
    name.map_or_else(|| format!("{code:#x}"), str::to_string)
}

fn compact_devnode_line(node: &DevNodeSummary) -> String {
    let mut line = format!(
        "DevNode {}  PDO {}  {}  [{}]  {}",
        ui::addr(node.address.0),
        ui::addr(node.pdo.0),
        node.instance_path,
        node.service_name,
        node.state_name
    );
    if node.problem != 0 {
        line.push_str(&format!(
            "  problem={} status={:#x}",
            problem_text(node.problem, node.problem_name.as_deref()),
            node.problem_status
        ));
    }
    line
}

fn history_tail(history: &[StateHistoryEntry]) -> &[StateHistoryEntry] {
    let first = history.len().saturating_sub(HISTORY_DISPLAY_LIMIT);
    &history[first..]
}

fn print_devnode(node: &DevNodeDetail) {
    outln!(
        "DevNode {} for PDO {}",
        ui::addr(node.address.0),
        ui::addr(node.pdo.0)
    );
    outln!(
        "  Parent {} Sibling {} Child {}",
        ui::addr(node.parent.0),
        ui::addr(node.sibling.0),
        ui::addr(node.child.0)
    );
    outln!("  InstancePath is \"{}\"", node.instance_path);
    outln!("  ServiceName is \"{}\"", node.service_name);
    outln!("  State = {}", state_text(node.state, &node.state_name));
    outln!(
        "  Previous State = {}",
        state_text(node.previous_state, &node.previous_state_name)
    );
    for entry in history_tail(&node.state_history) {
        outln!(
            "  StateHistory[{}] = {}",
            entry.index,
            state_text(entry.state, &entry.state_name)
        );
    }
    outln!(
        "  Flags {:#x}  UserFlags {:#x}",
        node.flags,
        node.user_flags
    );
    if node.completion_status != 0 {
        outln!("  CompletionStatus {:#x}", node.completion_status);
    }
    if node.problem != 0 {
        outln!(
            "  Problem = {} ({})  status {:#x}",
            problem_text(node.problem, node.problem_name.as_deref()),
            node.problem,
            node.problem_status
        );
    }
    if !node.pending_irp.is_zero() {
        outln!("  PendingIrp {}", ui::addr(node.pending_irp.0));
    }
}

fn print_devnode_summary(node: &DevNodeSummary) {
    outln!("!DevNode {} :", ui::addr(node.address.0));
    outln!("  DeviceInst is \"{}\"", node.instance_path);
    outln!("  ServiceName is \"{}\"", node.service_name);
    outln!("  State = {}", state_text(node.state, &node.state_name));
}

fn print_devnode_tree(nodes: &[DevNodeSummary], truncated: bool) {
    let top = nodes.first().map_or(0, |node| node.depth);
    for node in nodes {
        let indentation = "  ".repeat((node.depth.saturating_sub(top) as usize).min(64));
        outln!("{}{}", indentation, compact_devnode_line(node));
    }
    if truncated {
        outln!("  ... devnode walk bound reached");
    }
}

fn print_device_stack(stack: &DeviceStackDetail) {
    outln!(
        "  {:<18} {:<18} {:<18}{}",
        "!DevObj",
        "!DrvObj",
        "!DevExt",
        "ObjectName"
    );
    for entry in &stack.entries {
        let marker = if entry.is_argument { ">" } else { " " };
        outln!(
            "{} {:<18} {:<18} {:<18}{}",
            marker,
            ui::addr(entry.device_object.0),
            entry.driver_name,
            ui::addr(entry.device_extension.0),
            entry.object_name
        );
    }
    if stack.truncated {
        outln!("  ... device stack bound reached");
    }
    if let Some(error) = &stack.pdo_devnode_error {
        error!("{error}");
    } else {
        match &stack.pdo_devnode {
            Some(node) => print_devnode_summary(node),
            None => outln!("!DevNode {}", ui::addr(0)),
        }
    }
}

fn print_pnp_triage(triage: &PnpTriageDetail) {
    outln!("Problem devnodes:");
    if triage.problems.is_empty() {
        outln!("  none");
    } else {
        for node in &triage.problems {
            outln!("  {}", compact_devnode_line(node));
        }
    }

    outln!("Devnodes not started:");
    if triage.not_started.is_empty() {
        outln!("  none");
    } else {
        for node in &triage.not_started {
            outln!("  {}", compact_devnode_line(node));
        }
    }

    outln!("Devnodes with pending PnP IRPs:");
    if triage.pending_irps.is_empty() {
        outln!("  none");
    } else {
        for node in &triage.pending_irps {
            outln!(
                "  {}  pending={}",
                compact_devnode_line(node),
                ui::addr(node.pending_irp.0)
            );
        }
    }

    outln!(
        "{} devnode(s): {} started, {} with problems, {} not started, {} with pending IRPs{}",
        triage.total,
        triage.started,
        triage.problems.len(),
        triage.not_started.len(),
        triage.pending_irps.len(),
        if triage.truncated {
            " (walk bound reached)"
        } else {
            ""
        }
    );
}

impl ReplState<'_> {
    fn cmd_devnode(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
        let Some(request) = parse_devnode_request(&invocation.argv) else {
            outln!("{}\n", command_help(invocation.name));
            return Ok(());
        };
        let node = match request.node {
            Some(text) => match Expr::eval_with_radix(&text, &self.ctx.target, self.radix) {
                Ok(address) => Some(address),
                Err(error) => {
                    error!("{error}");
                    return Ok(());
                }
            },
            None => None,
        };

        match self.ctx.target.inspect_devnode(node, request.recursive) {
            Ok(detail) if request.recursive => {
                print_devnode_tree(&detail.subtree, detail.subtree_truncated)
            }
            Ok(detail) => print_devnode(&detail),
            Err(error) => error!("{error}"),
        }
        Ok(())
    }

    fn cmd_devstack(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
        if invocation.argv.len() != 1 {
            outln!("{}\n", command_help(invocation.name));
            return Ok(());
        }
        let text = invocation.arg(0).unwrap_or_default();
        let address = match Expr::eval_with_radix(text, &self.ctx.target, self.radix) {
            Ok(address) => address,
            Err(error) => {
                error!("{error}");
                return Ok(());
            }
        };

        match self.ctx.target.inspect_device_stack(address) {
            Ok(stack) => print_device_stack(&stack),
            Err(error) => error!("{error}"),
        }
        Ok(())
    }

    fn cmd_pnptriage(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
        if !invocation.argv.is_empty() {
            outln!("{}\n", command_help(invocation.name));
            return Ok(());
        }
        match self.ctx.target.pnp_triage() {
            Ok(triage) => print_pnp_triage(&triage),
            Err(error) => error!("{error}"),
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse(args: &[&str]) -> Option<DevNodeRequest> {
        let argv: Vec<Cow<'_, str>> = args.iter().map(|arg| Cow::Borrowed(*arg)).collect();
        parse_devnode_request(&argv)
    }

    #[test]
    fn devnode_argument_forms_follow_windbg() {
        assert_eq!(
            parse(&[]),
            Some(DevNodeRequest {
                node: None,
                recursive: false
            })
        );
        assert_eq!(
            parse(&["0"]),
            Some(DevNodeRequest {
                node: None,
                recursive: false
            })
        );
        assert_eq!(
            parse(&["ffff808939817520"]),
            Some(DevNodeRequest {
                node: Some("ffff808939817520".to_string()),
                recursive: false
            })
        );
        assert_eq!(
            parse(&["0", "-r"]),
            Some(DevNodeRequest {
                node: None,
                recursive: true
            })
        );
        assert_eq!(
            parse(&["ffff808939817520", "1"]),
            Some(DevNodeRequest {
                node: Some("ffff808939817520".to_string()),
                recursive: true
            })
        );
        assert_eq!(
            parse(&["-r", "ffff808939817520"]),
            Some(DevNodeRequest {
                node: Some("ffff808939817520".to_string()),
                recursive: true
            })
        );
        for bad in [
            &["-x"][..],
            &["node", "other"][..],
            &["node", "-r", "1"][..],
            &["0", "0"][..],
        ] {
            assert!(parse(bad).is_none(), "{bad:?}");
        }
    }
}