From 87a4a5c92222ef49c86414ab8d8976ab7423c2af Mon Sep 17 00:00:00 2001 From: godzie44 Date: Fri, 31 Jan 2025 21:03:26 +0300 Subject: [PATCH] feat(debugger): add `trigger` command (close #39) --- README.md | 1 + src/debugger/breakpoint.rs | 2 +- src/ui/command/mod.rs | 2 + src/ui/command/parser/mod.rs | 95 ++++++++++++++- src/ui/command/trigger.rs | 25 ++++ src/ui/console/editor.rs | 18 ++- src/ui/console/help.rs | 42 ++++--- src/ui/console/hook.rs | 10 ++ src/ui/console/mod.rs | 191 +++++++++++++++++++++++++----- src/ui/console/print.rs | 1 + src/ui/console/trigger.rs | 56 +++++++++ tests/integration/test_command.py | 43 +++++++ 12 files changed, 436 insertions(+), 50 deletions(-) create mode 100644 src/ui/command/trigger.rs create mode 100644 src/ui/console/trigger.rs diff --git a/README.md b/README.md index c6836951..c4fc6f6c 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,7 @@ Of course, the debugger provides many more commands: alias: `reg write`) - `register info` - print list of registers with it values (alias: `reg info`) - `sharedlib info` - show list of shared libraries +- `trigger` - define a list of commands that will be executed when a certain event is triggered (at a breakpoint or watchpoint hit), see `help trigger` for more info - `quit` - exit the BugStalker (alias: `q`) ## Tui interface diff --git a/src/debugger/breakpoint.rs b/src/debugger/breakpoint.rs index ae5353f5..7ab90cb5 100644 --- a/src/debugger/breakpoint.rs +++ b/src/debugger/breakpoint.rs @@ -841,7 +841,7 @@ impl UninitBreakpoint { brkpt.number, brkpt.place, BrkptType::UserDefined, - Some(brkpt.debug_info_file).map(|path| path.into()), + Some(brkpt.debug_info_file), ) } diff --git a/src/ui/command/mod.rs b/src/ui/command/mod.rs index 5c7105ab..cb9e6edf 100644 --- a/src/ui/command/mod.rs +++ b/src/ui/command/mod.rs @@ -22,6 +22,7 @@ pub mod step_out; pub mod step_over; pub mod symbol; pub mod thread; +pub mod trigger; pub mod variables; pub mod watch; @@ -64,6 +65,7 @@ pub enum Command { SkipInput, Oracle(String, Option), Async(r#async::Command), + Trigger(trigger::Command), Help { command: Option, reason: Option, diff --git a/src/ui/command/parser/mod.rs b/src/ui/command/parser/mod.rs index 1c5218dd..97689b60 100644 --- a/src/ui/command/parser/mod.rs +++ b/src/ui/command/parser/mod.rs @@ -1,7 +1,9 @@ pub mod expression; use super::r#break::BreakpointIdentity; -use super::{frame, memory, r#async, register, source_code, thread, watch, Command, CommandError}; +use super::{ + frame, memory, r#async, register, source_code, thread, trigger, watch, Command, CommandError, +}; use super::{r#break, CommandResult}; use crate::debugger::register::debug::BreakCondition; use crate::debugger::variable::dqe::Dqe; @@ -76,6 +78,11 @@ pub const ASYNC_COMMAND_STEP_OVER_SUBCOMMAND: &str = "stepover"; pub const ASYNC_COMMAND_STEP_OVER_SUBCOMMAND_SHORT: &str = "next"; pub const ASYNC_COMMAND_STEP_OUT_SUBCOMMAND: &str = "stepout"; pub const ASYNC_COMMAND_STEP_OUT_SUBCOMMAND_SHORT: &str = "finish"; +pub const TRIGGER_COMMAND: &str = "trigger"; +pub const TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND: &str = "any"; +pub const TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND: &str = "b"; +pub const TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND: &str = "w"; +pub const TRIGGER_COMMAND_INFO_SUBCOMMAND: &str = "info"; pub const HELP_COMMAND: &str = "help"; pub const HELP_COMMAND_SHORT: &str = "h"; @@ -468,6 +475,40 @@ impl Command { ))) .boxed(); + let trigger = op(TRIGGER_COMMAND) + .ignore_then(choice(( + choice(( + sub_op(TRIGGER_COMMAND_INFO_SUBCOMMAND).to(trigger::Command::Info), + sub_op(TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND).to( + trigger::Command::AttachToDefined(trigger::TriggerEvent::Any), + ), + sub_op(TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND) + .ignore_then(text::int(10)) + .from_str() + .unwrapped() + .map(|num| { + trigger::Command::AttachToDefined(trigger::TriggerEvent::Breakpoint( + num, + )) + }), + sub_op(TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND) + .ignore_then(text::int(10)) + .from_str() + .unwrapped() + .map(|num| { + trigger::Command::AttachToDefined(trigger::TriggerEvent::Watchpoint( + num, + )) + }), + )) + .padded(), + end() + .to(trigger::Command::AttachToPreviouslyCreated) + .padded(), + ))) + .map(Command::Trigger) + .boxed(); + let oracle = op_w_arg(ORACLE_COMMAND) .ignore_then(text::ident().padded().then(text::ident().or_not())) .map(|(name, subcmd)| { @@ -498,6 +539,7 @@ impl Command { command(ORACLE_COMMAND, oracle), command(WATCH_COMMAND, watchpoint), command(ASYNC_COMMAND, r#async), + command(TRIGGER_COMMAND, trigger), )) } @@ -1078,6 +1120,57 @@ fn test_parser() { )); }, }, + TestCase { + inputs: vec!["trigger", " trigger "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Trigger(trigger::Command::AttachToPreviouslyCreated) + )); + }, + }, + TestCase { + inputs: vec!["trigger any", " trigger any "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Trigger(trigger::Command::AttachToDefined( + trigger::TriggerEvent::Any + )) + )); + }, + }, + TestCase { + inputs: vec!["trigger info", " trigger info "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Trigger(trigger::Command::Info) + )); + }, + }, + TestCase { + inputs: vec!["trigger b 1", " trigger b 1 "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Trigger(trigger::Command::AttachToDefined( + trigger::TriggerEvent::Breakpoint(num) + )) if num == 1 + )); + }, + }, + TestCase { + inputs: vec!["trigger w 2", " trigger w 2 "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Trigger(trigger::Command::AttachToDefined( + trigger::TriggerEvent::Watchpoint(num) + )) if num == 2 + )); + }, + }, TestCase { inputs: vec!["oracle tokio", " oracle tokio "], command_matcher: |result| { diff --git a/src/ui/command/trigger.rs b/src/ui/command/trigger.rs new file mode 100644 index 00000000..b3e4c93d --- /dev/null +++ b/src/ui/command/trigger.rs @@ -0,0 +1,25 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum TriggerEvent { + Breakpoint(u32), + Watchpoint(u32), + Any, +} + +impl Display for TriggerEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TriggerEvent::Breakpoint(num) => write!(f, "Breakpoint {num}"), + TriggerEvent::Watchpoint(num) => write!(f, "Watchpoint {num}"), + TriggerEvent::Any => write!(f, "Any breakpoint or watchpoint"), + } + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Command { + AttachToPreviouslyCreated, + AttachToDefined(TriggerEvent), + Info, +} diff --git a/src/ui/console/editor.rs b/src/ui/console/editor.rs index a63edcce..a43e6d8d 100644 --- a/src/ui/console/editor.rs +++ b/src/ui/console/editor.rs @@ -14,9 +14,11 @@ use crate::ui::command::parser::{ SOURCE_COMMAND_FUNCTION_SUBCOMMAND, STEP_INSTRUCTION_COMMAND, STEP_INTO_COMMAND, STEP_INTO_COMMAND_SHORT, STEP_OUT_COMMAND, STEP_OUT_COMMAND_SHORT, STEP_OVER_COMMAND, STEP_OVER_COMMAND_SHORT, SYMBOL_COMMAND, THREAD_COMMAND, THREAD_COMMAND_CURRENT_SUBCOMMAND, - THREAD_COMMAND_INFO_SUBCOMMAND, THREAD_COMMAND_SWITCH_SUBCOMMAND, VAR_COMMAND, VAR_LOCAL_KEY, - WATCH_COMMAND, WATCH_COMMAND_SHORT, WATCH_INFO_SUBCOMMAND, WATCH_REMOVE_SUBCOMMAND, - WATCH_REMOVE_SUBCOMMAND_SHORT, + THREAD_COMMAND_INFO_SUBCOMMAND, THREAD_COMMAND_SWITCH_SUBCOMMAND, TRIGGER_COMMAND, + TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND, TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND, + TRIGGER_COMMAND_INFO_SUBCOMMAND, TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND, VAR_COMMAND, + VAR_LOCAL_KEY, WATCH_COMMAND, WATCH_COMMAND_SHORT, WATCH_INFO_SUBCOMMAND, + WATCH_REMOVE_SUBCOMMAND, WATCH_REMOVE_SUBCOMMAND_SHORT, }; use chumsky::prelude::{any, choice, just}; use chumsky::text::whitespace; @@ -417,6 +419,16 @@ pub fn create_editor( SOURCE_COMMAND_FUNCTION_SUBCOMMAND.to_string(), ], }, + CommandHint { + short: None, + long: TRIGGER_COMMAND.to_string(), + subcommands: vec![ + TRIGGER_COMMAND_INFO_SUBCOMMAND.to_string(), + TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND.to_string(), + TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND.to_string(), + TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND.to_string(), + ], + }, CommandHint { short: None, long: ASYNC_COMMAND.to_string(), diff --git a/src/ui/console/help.rs b/src/ui/console/help.rs index 7669d7b5..00998da3 100644 --- a/src/ui/console/help.rs +++ b/src/ui/console/help.rs @@ -24,6 +24,7 @@ thread info|current|switch -- show list of threads or current ( sharedlib info -- show list of shared libraries source asm|fn| -- show source code or assembly instructions for current (in focus) function async backtrace|backtrace all|task -- commands for async rust +trigger info|any|<>|b |w -- define a list of commands that will be executed when a certain event is triggered oracle <>| -- execute a specific oracle h, help <>| -- show help tui -- change ui mode to tui @@ -94,7 +95,7 @@ pub const HELP_VAR: &str = "\ \x1b[32;1mvar\x1b[0m Show local and global variables, supports data queries expressions over variables (see `help dqe`). -Available subcomands: +Available subcommands: var locals - print current stack frame local variables var - print local and global variables with selected name @@ -112,7 +113,7 @@ pub const HELP_ARG: &str = "\ \x1b[32;1marg\x1b[0m Show current stack frame arguments, supports data queries expressions over arguments (see `help dqe`). -Available subcomands: +Available subcommands: arg all - print all arguments arg - print argument with selected name @@ -126,7 +127,7 @@ pub const HELP_BACKTRACE: &str = "\ \x1b[32;1mbt, backtrace\x1b[0m Show backtrace of all stack frames in current thread or from all threads. -Available subcomands: +Available subcommands: backtrace all - show backtrace for all running threads backtrace - show backtrace of current thread @@ -141,7 +142,7 @@ pub const HELP_FRAME: &str = "\ \x1b[32;1mf, frame\x1b[0m Show current stack frame info or set frame to focus. -Available subcomands: +Available subcommands: frame info - show current stack frame information (see output explanation) frame switch - set frame to focus @@ -184,12 +185,12 @@ pub const HELP_BREAK: &str = "\ \x1b[32;1mb, break\x1b[0m Manage breakpoints. -Available subcomands: +Available subcommands: break - set breakpoint to location break remove | - deactivate and delete selected breakpoint break info - show all breakpoints -Posible location format: +Possible location format: - at instruction. Example: break 0x55555555BD30 - at function start. A function can be defined by its full name (with namespace) or by function name (in case of possible collisions, breakpoints will be set in @@ -207,7 +208,7 @@ or raw memory region have a different lifetimes. Watchpoints for global variable are lives until BugStalker session is alive. On the contrary, watchpoints for local variables are lives until debugee is not restarted, and will be removed automatically. -Available subcomands: +Available subcommands: watch +rw|+w| - set write or read-write watchpoint (write by default) to memory location [addr; addr+size], size must be one of [1,2,4,8] bytes watch +rw|+w| - set write or read-write watchpoint (write by default) to DQE result (see `help dqe`), expression result must one of [1,2,4,8] bytes watch remove || - deactivate and delete selected watchpoint @@ -225,7 +226,7 @@ pub const HELP_SYMBOL: &str = "\ \x1b[32;1msymbol\x1b[0m Print symbols matched by regular expression. -Available subcomands: +Available subcommands: symbol "; @@ -233,7 +234,7 @@ pub const HELP_MEMORY: &str = "\ \x1b[32;1mmem, memory\x1b[0m Read or write into debugged program memory. -Available subcomands: +Available subcommands: memory read
- print 8-byte block at address in debugee memory memory write
- writes 8-byte value to address in debugee memory "; @@ -242,7 +243,7 @@ pub const HELP_REGISTER: &str = "\ \x1b[32;1mreg, register\x1b[0m Read, write, or view debugged program registers (x86_64 registers support). -Available subcomands: +Available subcommands: register read - print value of register by name (x86_64 register name in lowercase) register write - set new value to register by name register info - print list of registers with it values @@ -252,7 +253,7 @@ pub const HELP_THREAD: &str = "\ \x1b[32;1mthread\x1b[0m Show threads information or set thread to focus. -Available subcomands: +Available subcommands: thread info - print list of thread information thread current - prints thread that has focus thread switch - set thread to focus @@ -262,7 +263,7 @@ pub const HELP_SHARED_LIB: &str = "\ \x1b[32;1msharedlib\x1b[0m Show shared libraries information. -Available subcomands: +Available subcommands: sharedlib info - print list of loaded shared libraries and their mapping addresses "; @@ -270,7 +271,7 @@ pub const HELP_SOURCE: &str = "\ \x1b[32;1msource\x1b[0m Show source code or assembly instructions for current (in focus) function. -Available subcomands: +Available subcommands: source fn - show code of function in focus source asm - show assembly of function in focus source - show line in focus with lines up and down of this line @@ -280,7 +281,7 @@ pub const HELP_ASYNC: &str = "\ \x1b[32;1masync\x1b[0m Commands for async rust (currently for tokio runtime only). -Available subcomands: +Available subcommands: async backtrace - show state of async workers and blocking threads async backtrace all - show state of async workers and blocking threads, show info about all running tasks async task - show active task (active task means a task that is running on the thread that is currently in focus) if `async_fn_regex` parameter is empty, @@ -289,6 +290,18 @@ async next, async stepover - perform a stepover within the context of the curren async finish, async stepout - execute the program until the current task moves into the completed state "; +pub const HELP_TRIGGER: &str = "\ +\x1b[32;1mtrigger\x1b[0m +Define a list of commands that will be executed when a certain event is triggered (at a breakpoint or watchpoint hit). + +Available subcommands: +trigger any - define a list of commands that will be executed when any breakpoint or watchpoint is hit +trigger - define a list of commands that will be executed when a previously defined breakpoint or watchpoint is hit +trigger b - define a list of commands that will be executed when the given breakpoint is hit +trigger w - define a list of commands that will be executed when the given watchpoint is hit +trigger info - show the list of triggers +"; + pub const HELP_TUI: &str = "\ \x1b[32;1mtui\x1b[0m Change ui mode to terminal ui. @@ -342,6 +355,7 @@ impl Helper { Some(parser::SHARED_LIB_COMMAND) => HELP_SHARED_LIB, Some(parser::SOURCE_COMMAND) => HELP_SOURCE, Some(parser::ASYNC_COMMAND) => HELP_ASYNC, + Some(parser::TRIGGER_COMMAND) => HELP_TRIGGER, Some(parser::ORACLE_COMMAND) => self.oracle_help.get_or_insert_with(|| { let mut help = HELP_ORACLE.to_string(); let oracles = debugger.all_oracles(); diff --git a/src/ui/console/hook.rs b/src/ui/console/hook.rs index 05b79479..433dbe45 100644 --- a/src/ui/console/hook.rs +++ b/src/ui/console/hook.rs @@ -1,9 +1,11 @@ use super::print::style::AsyncTaskView; +use super::trigger::TriggerRegistry; use crate::debugger::address::RelocatedAddress; use crate::debugger::register::debug::BreakCondition; use crate::debugger::variable::value::Value; use crate::debugger::PlaceDescriptor; use crate::debugger::{EventHook, FunctionDie}; +use crate::ui::command; use crate::ui::console::file::FileView; use crate::ui::console::print::style::{AddressView, FilePathView, FunctionNameView, KeywordView}; use crate::ui::console::print::ExternalPrinter; @@ -27,6 +29,7 @@ pub struct TerminalHook { on_install_proc: Box, printer: ExternalPrinter, context: RefCell, + trigger_reg: Rc, } impl TerminalHook { @@ -34,12 +37,14 @@ impl TerminalHook { printer: ExternalPrinter, fv: Rc, on_install_proc: impl Fn(Pid) + 'static, + trigger_reg: Rc, ) -> Self { Self { file_view: fv, on_install_proc: Box::new(on_install_proc), printer, context: RefCell::new(Context::default()), + trigger_reg, } } } @@ -65,6 +70,8 @@ impl EventHook for TerminalHook { } self.context.borrow_mut().prev_func = mb_func.cloned(); + self.trigger_reg + .fire_event(command::trigger::TriggerEvent::Breakpoint(num)); Ok(()) } @@ -121,6 +128,9 @@ impl EventHook for TerminalHook { } } + self.trigger_reg + .fire_event(command::trigger::TriggerEvent::Watchpoint(num)); + Ok(()) } diff --git a/src/ui/console/mod.rs b/src/ui/console/mod.rs index c6cd007c..07fa9747 100644 --- a/src/ui/console/mod.rs +++ b/src/ui/console/mod.rs @@ -4,12 +4,14 @@ use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::mpsc::{Receiver, SyncSender}; use std::sync::{mpsc, Arc, Mutex, Once}; -use std::thread; use std::time::Duration; +use std::{thread, vec}; use crossterm::style::{Color, Stylize}; +use itertools::Itertools; use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; +use print::style::ImportantView; use rustyline::error::ReadlineError; use rustyline::history::MemHistory; use rustyline::Editor; @@ -17,8 +19,11 @@ use timeout_readwrite::TimeoutReader; use debugger::Error; use r#break::Command as BreakpointCommand; +use trigger::TriggerRegistry; use super::command::r#async::AsyncCommandResult; +use super::command::r#async::Command as AsyncCommand; +use super::command::trigger::TriggerEvent; use crate::debugger; use crate::debugger::process::{Child, Installed}; use crate::debugger::variable::dqe::{Dqe, Selector}; @@ -59,6 +64,7 @@ use crate::ui::console::r#async::print_task_ex; use crate::ui::console::variable::render_variable; use crate::ui::DebugeeOutReader; use crate::ui::{command, supervisor}; +use command::trigger::Command as UserCommandTarget; mod r#async; mod editor; @@ -66,6 +72,7 @@ pub mod file; mod help; pub mod hook; pub mod print; +mod trigger; mod variable; const WELCOME_TEXT: &str = r#" @@ -73,6 +80,7 @@ BugStalker greets "#; const PROMT: &str = "(bs) "; const PROMT_YES_NO: &str = "(bs y/n) "; +const PROMT_USER_PROGRAM: &str = "> "; type BSEditor = Editor; @@ -100,10 +108,12 @@ impl AppBuilder { let (user_cmd_tx, user_cmd_rx) = mpsc::sync_channel::(0); let mut editor = create_editor(PROMT, oracles)?; let file_view = Rc::new(FileView::new()); + let trigger_reg = Rc::new(TriggerRegistry::default()); let hook = TerminalHook::new( ExternalPrinter::new(&mut editor)?, file_view.clone(), move |pid| DEBUGEE_PID.store(pid.as_raw(), Ordering::Release), + trigger_reg.clone(), ); let debugger = debugger_lazy(hook)?; @@ -122,6 +132,7 @@ impl AppBuilder { debugee_err: self.debugee_err, user_act_tx: user_cmd_tx, user_act_rx: user_cmd_rx, + trigger_reg, }) } @@ -173,6 +184,7 @@ enum UserAction { enum EditorMode { Default, YesNo, + UserProgram, } pub struct TerminalApplication { @@ -183,6 +195,7 @@ pub struct TerminalApplication { debugee_err: DebugeeOutReader, user_act_tx: SyncSender, user_act_rx: Receiver, + trigger_reg: Rc, } pub static HELLO_ONCE: Once = Once::new(); @@ -254,6 +267,7 @@ impl TerminalApplication { cancel_output_flag: cancel, ready_to_next_command_tx, helper: Default::default(), + trigger_reg: self.trigger_reg, }; static CTRLC_ONCE: Once = Once::new(); @@ -280,6 +294,7 @@ impl TerminalApplication { let promt = match ready_to_next_command_rx.recv() { Ok(EditorMode::Default) => PROMT, Ok(EditorMode::YesNo) => PROMT_YES_NO, + Ok(EditorMode::UserProgram) => PROMT_USER_PROGRAM, Err(_) => return, }; @@ -344,6 +359,7 @@ struct AppLoop { cancel_output_flag: Arc, helper: Helper, ready_to_next_command_tx: mpsc::Sender, + trigger_reg: Rc, } impl AppLoop { @@ -367,6 +383,59 @@ impl AppLoop { } } + fn take_user_command_list(&self, help: &str) -> Result { + self.printer.println(help); + let mut result = vec![]; + loop { + _ = self.ready_to_next_command_tx.send(EditorMode::UserProgram); + let act = self + .user_input_rx + .recv() + .expect("unexpected sender disconnect"); + match act { + UserAction::Cmd(input) => { + if input.as_str().trim() == "end" { + break; + } + + let cmd = Command::parse(&input)?; + match cmd { + Command::PrintVariables(_) + | Command::PrintArguments(_) + | Command::PrintBacktrace(_) + | Command::Frame(_) + | Command::PrintSymbol(_) + | Command::Memory(_) + | Command::Register(_) + | Command::Thread(_) + | Command::SharedLib + | Command::SourceCode(_) + | Command::Oracle(_, _) + | Command::Async(AsyncCommand::FullBacktrace) + | Command::Async(AsyncCommand::ShortBacktrace) + | Command::Async(AsyncCommand::CurrentTask(_)) => { + result.push((cmd, input)); + continue; + } + _ => { + self.printer + .println("unsupported command, try another one or `end`"); + continue; + } + } + } + + UserAction::Terminate | UserAction::ChangeMode | UserAction::Nop => { + self.printer + .println("unsupported command, try another one or `end`"); + continue; + } + }; + } + + Ok(result) + } + fn update_completer_variables(&self) -> anyhow::Result<()> { let vars = self .debugger @@ -381,12 +450,16 @@ impl AppLoop { Ok(()) } - fn handle_command(&mut self, cmd: &str) -> Result<(), CommandError> { + fn handle_command_str(&mut self, cmd: &str) -> Result<(), CommandError> { if cmd.is_empty() { return Ok(()); } - match Command::parse(cmd)? { + self.handle_command(Command::parse(cmd)?) + } + + fn handle_command(&mut self, cmd: Command) -> Result<(), CommandError> { + match cmd { Command::PrintVariables(print_var_command) => VariablesHandler::new(&self.debugger) .handle(print_var_command)? .into_iter() @@ -522,14 +595,17 @@ impl AppLoop { loop { match BreakpointHandler::new(&mut self.debugger).handle(&brkpt_cmd) { Ok(r#break::ExecutionResult::New(brkpts)) => { - brkpts - .iter() - .for_each(|brkpt| print_bp("New breakpoint", brkpt)); + brkpts.iter().for_each(|brkpt| { + print_bp("New breakpoint", brkpt); + self.trigger_reg.set_previous_brkpt(brkpt.number); + }); } Ok(r#break::ExecutionResult::Removed(brkpts)) => { - brkpts - .iter() - .for_each(|brkpt| print_bp("Removed breakpoint", brkpt)); + brkpts.iter().for_each(|brkpt| { + print_bp("Removed breakpoint", brkpt); + self.trigger_reg + .remove(TriggerEvent::Breakpoint(brkpt.number)); + }); } Ok(r#break::ExecutionResult::Dump(brkpts)) => brkpts .iter() @@ -571,9 +647,13 @@ impl AppLoop { let mut handler = WatchpointHandler::new(&mut self.debugger); let res = handler.handle(cmd)?; match res { - WatchpointExecutionResult::New(wp) => print_wp("New watchpoint", wp), + WatchpointExecutionResult::New(wp) => { + self.trigger_reg.set_previous_wp(wp.number); + print_wp("New watchpoint", wp); + } WatchpointExecutionResult::Removed(Some(wp)) => { - print_wp("Removed watchpoint", wp) + self.trigger_reg.remove(TriggerEvent::Watchpoint(wp.number)); + print_wp("Removed watchpoint", wp); } WatchpointExecutionResult::Removed(_) => { self.printer.println("No watchpoint found") @@ -746,6 +826,41 @@ impl AppLoop { } } } + Command::Trigger(cmd) => { + let event = match cmd { + UserCommandTarget::AttachToPreviouslyCreated => { + let Some(event) = self.trigger_reg.get_previous_event() else { + self.printer.println(ErrorView::from( + "No previously added watchpoints or breakpoints exist", + )); + return Ok(()); + }; + event + } + UserCommandTarget::AttachToDefined(event) => event, + UserCommandTarget::Info => { + self.printer.println(format!("{:<30} Program", "Event")); + + self.trigger_reg.for_each_trigger(|event, program| { + self.printer.println(format!( + "{:<30} {}", + event.to_string(), + KeywordView::from(program.iter().map(|(_, str)| str).join(", ")), + )); + }); + return Ok(()); + } + }; + + let help = match event { + TriggerEvent::Breakpoint(num) => format!("Print the commands to be executed on breakpoint {num}"), + TriggerEvent::Watchpoint(num) => format!("Print the commands to be executed on watchpoint {num}"), + TriggerEvent::Any => "Print the commands to be executed on each breakpoint or watchpoint, and print 'end' for end".to_string(), + }; + + let commands = self.take_user_command_list(&help)?; + self.trigger_reg.add(event, commands); + } Command::Oracle(name, subcmd) => match self.debugger.get_oracle(&name) { None => self .printer @@ -757,8 +872,40 @@ impl AppLoop { Ok(()) } + fn handle_error(&self, error: CommandError) { + match error { + CommandError::Parsing(pretty_error) => { + self.printer.println(pretty_error); + } + CommandError::FileRender(_) => { + self.printer + .println(ErrorView::from(format!("Render file error: {error:#}"))); + } + CommandError::Handle(ref err) if err.is_fatal() => { + self.printer.println(ErrorView::from("Shutdown debugger")); + self.printer + .println(ErrorView::from(format!("Fatal error: {error:#}"))); + exit(1); + } + CommandError::Handle(_) => { + self.printer + .println(ErrorView::from(format!("Error: {error:#}"))); + } + } + } + fn run(mut self) -> anyhow::Result { loop { + if let Some(user_program) = self.trigger_reg.take_program() { + self.printer + .println(ImportantView::from("Related program found:")); + user_program.into_iter().for_each(|(cmd, _)| { + if let Err(e) = self.handle_command(cmd) { + self.handle_error(e); + } + }); + }; + _ = self.ready_to_next_command_tx.send(EditorMode::Default); let Ok(action) = self.user_input_rx.recv() else { @@ -767,26 +914,8 @@ impl AppLoop { match action { UserAction::Cmd(command) => { - if let Err(e) = self.handle_command(&command) { - match e { - CommandError::Parsing(pretty_error) => { - self.printer.println(pretty_error); - } - CommandError::FileRender(_) => { - self.printer - .println(ErrorView::from(format!("Render file error: {e:#}"))); - } - CommandError::Handle(ref err) if err.is_fatal() => { - self.printer.println(ErrorView::from("Shutdown debugger")); - self.printer - .println(ErrorView::from(format!("Fatal error: {e:#}"))); - exit(1); - } - CommandError::Handle(_) => { - self.printer - .println(ErrorView::from(format!("Error: {e:#}"))); - } - } + if let Err(e) = self.handle_command_str(&command) { + self.handle_error(e); } } UserAction::Nop => {} diff --git a/src/ui/console/print.rs b/src/ui/console/print.rs index 5565315d..afc38601 100644 --- a/src/ui/console/print.rs +++ b/src/ui/console/print.rs @@ -114,6 +114,7 @@ pub mod style { view_struct!(AsmInstructionView, Color::DarkRed); view_struct!(AsmOperandsView, Color::DarkGreen); view_struct!(ErrorView, Color::DarkRed); + view_struct!(ImportantView, Color::Magenta); view_struct!(AsyncTaskView, Color::Green); view_struct!(FutureFunctionView, Color::Yellow); diff --git a/src/ui/console/trigger.rs b/src/ui/console/trigger.rs new file mode 100644 index 00000000..b4e6e651 --- /dev/null +++ b/src/ui/console/trigger.rs @@ -0,0 +1,56 @@ +use crate::ui::command::{self, trigger::TriggerEvent}; +use std::cell::{Cell, RefCell}; + +pub type UserProgram = Vec<(command::Command, String)>; + +#[derive(Default)] +pub struct TriggerRegistry { + previous_brkpt_or_wp: Cell>, + list: RefCell>, + active_event: RefCell>, +} + +impl TriggerRegistry { + pub fn set_previous_brkpt(&self, num: u32) { + self.previous_brkpt_or_wp + .set(Some(TriggerEvent::Breakpoint(num))); + } + + pub fn set_previous_wp(&self, num: u32) { + self.previous_brkpt_or_wp + .set(Some(TriggerEvent::Watchpoint(num))); + } + + pub fn get_previous_event(&self) -> Option { + self.previous_brkpt_or_wp.get() + } + + pub fn add(&self, event: TriggerEvent, program: UserProgram) { + if program.is_empty() { + self.remove(event); + } else { + self.list.borrow_mut().insert(event, program); + } + } + + pub fn remove(&self, event: TriggerEvent) { + self.list.borrow_mut().shift_remove(&event); + } + + pub fn fire_event(&self, event: TriggerEvent) { + if self.list.borrow().get(&event).is_some() { + *self.active_event.borrow_mut() = Some(event); + } else { + *self.active_event.borrow_mut() = Some(TriggerEvent::Any); + } + } + + pub fn take_program(&self) -> Option { + let trigger = self.active_event.borrow_mut().take()?; + self.list.borrow().get(&trigger).cloned() + } + + pub fn for_each_trigger(&self, f: impl Fn(&TriggerEvent, &UserProgram)) { + self.list.borrow().iter().for_each(|(k, v)| f(k, v)); + } +} diff --git a/tests/integration/test_command.py b/tests/integration/test_command.py index 67285130..4453a869 100644 --- a/tests/integration/test_command.py +++ b/tests/integration/test_command.py @@ -301,3 +301,46 @@ def test_breakpoint_at_rust_panic(): debugger.cmd('break rust_panic', 'New breakpoint') debugger.cmd('run', 'attempt to divide by zero') debugger.cmd('bt', 'rust_panic', 'panic::divided_by_zero') + + @staticmethod + def test_trigger(): + """Test trigger command""" + debugger = Debugger(path='./examples/target/debug/vars') + debugger.cmd('trigger any') + debugger.cmd('backtrace') + debugger.cmd('var int8') + debugger.cmd('end') + debugger.cmd('break vars.rs:19', 'New breakpoint 1') + debugger.cmd('run', 'Hit breakpoint 1', 'vars::scalar_types', 'int8 = i8(1)') + + # set trigger for a last created breakpoint + debugger.cmd('trigger') + debugger.cmd('var int16') + debugger.cmd('end') + + + debugger.cmd('trigger info', 'Any breakpoint or watchpoint', 'backtrace, var int8', 'Breakpoint 1', 'var int16') + + # remove trigger for any brkpt/wp + debugger.cmd('trigger any') + debugger.cmd('end') + + debugger.cmd('run') + debugger.cmd('y', 'Hit breakpoint 1', 'int16 = i16(-1)') + + # set trigger for breakpoint 2 + debugger.cmd('break vars.rs:30', 'New breakpoint 2') + debugger.cmd('trigger b 2') + debugger.cmd('var f32') + debugger.cmd('var f64') + debugger.cmd('end') + + # set trigger for watchpoint 2 + debugger.cmd('watch int16', 'New watchpoint 1') + debugger.cmd('trigger w 1') + debugger.cmd('backtrace') + debugger.cmd('end') + + debugger.cmd('c', 'f32 = f32(1.1)', 'f64 = f64(1.2)') + debugger.cmd('c', 'vars::scalar_types') +