From 959f695d6df1ae054bb6933729ef88e0ae1ed6a3 Mon Sep 17 00:00:00 2001 From: Tom Morton Date: Sun, 1 Dec 2024 17:54:33 +0000 Subject: [PATCH] Tasteful colorization and improvement of debugger outputs --- Cargo.lock | 13 ++- Cargo.toml | 2 +- agon-ez80-emulator/src/debugger.rs | 40 ++++---- agon-light-emulator-debugger/Cargo.toml | 1 + agon-light-emulator-debugger/src/lib.rs | 113 ++++++++++++++------- agon-light-emulator-debugger/src/parser.rs | 7 +- src/main.rs | 6 +- 7 files changed, 113 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8f410c..b96d382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "agon-ez80-emulator" -version = "0.9.73" +version = "0.9.74" dependencies = [ "chrono", "ez80", @@ -14,10 +14,11 @@ dependencies = [ [[package]] name = "agon-light-emulator-debugger" -version = "0.9.73" +version = "0.9.74" dependencies = [ "agon-ez80-emulator", "ctrlc", + "inline_colorization", "rustyline", ] @@ -195,7 +196,7 @@ source = "git+https://github.com/tomm/ez80.git?rev=64558c2acda1464248193f2f10804 [[package]] name = "fab-agon-emulator" -version = "0.9.73" +version = "0.9.74" dependencies = [ "agon-ez80-emulator", "agon-light-emulator-debugger", @@ -297,6 +298,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inline_colorization" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1804bdb6a9784758b200007273a8b84e2b0b0b97a8f1e18e763eceb3e9f98a" + [[package]] name = "io-kit-sys" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 22a3acf..be9469c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["agon-light-emulator-debugger"] [workspace.package] -version = "0.9.73" +version = "0.9.74" edition = "2021" authors = ["Tom Morton "] license = "GPL-3.0" diff --git a/agon-ez80-emulator/src/debugger.rs b/agon-ez80-emulator/src/debugger.rs index d44d870..b69e7ed 100644 --- a/agon-ez80-emulator/src/debugger.rs +++ b/agon-ez80-emulator/src/debugger.rs @@ -14,10 +14,18 @@ pub type Registers = ez80::Registers; pub type Reg8 = ez80::Reg8; pub type Reg16 = ez80::Reg16; +#[derive(Debug, Copy, Clone)] +pub enum PauseReason { + DebuggerRequested, + OutOfBoundsMemAccess(u32), // address + DebuggerBreakpoint, + IOBreakpoint(u8), +} + #[derive(Debug, Clone)] pub enum DebugCmd { Ping, - Pause, + Pause(PauseReason), Continue, Step, StepOver, @@ -44,7 +52,8 @@ pub enum DebugCmd { #[derive(Debug)] pub enum DebugResp { - IsPaused(bool), + Resumed, + Paused(PauseReason), Pong, Message(String), Registers(Registers), @@ -89,12 +98,10 @@ impl DebuggerServer { fn on_out_of_bounds(&mut self, machine: &mut AgonMachine, cpu: &mut ez80::Cpu) -> bool { if let Some(address) = machine.mem_out_of_bounds.get() { - self.con.tx.send(DebugResp::IsPaused(true)).unwrap(); self.con .tx - .send(DebugResp::Message(format!( - "Out of bounds memory access at PC=${:x}: ${:x}", - machine.last_pc, address + .send(DebugResp::Paused(PauseReason::OutOfBoundsMemAccess( + address, ))) .unwrap(); self.send_disassembly(machine, cpu, None, machine.last_pc, machine.last_pc + 1); @@ -115,14 +122,9 @@ impl DebuggerServer { if let Some(address) = machine.io_unhandled.get() { match address & 0xff { 0x10..=0x1f => { - self.con.tx.send(DebugResp::IsPaused(true)).unwrap(); self.con .tx - .send(DebugResp::Message(format!( - "Breakpoint triggered by IO 0x{:x} access at PC=${:x}", - address & 0xff, - machine.last_pc - ))) + .send(DebugResp::Paused(PauseReason::IOBreakpoint(address as u8))) .unwrap(); self.send_disassembly(machine, cpu, None, machine.last_pc, machine.last_pc + 1); self.send_state(machine, cpu); @@ -239,13 +241,13 @@ impl DebuggerServer { address: addr_next, once: true, actions: vec![ - DebugCmd::Pause, + DebugCmd::Pause(PauseReason::DebuggerRequested), DebugCmd::Message("Stepped over RST".to_string()), DebugCmd::GetState, ], }); machine.set_paused(false); - self.con.tx.send(DebugResp::IsPaused(false)).unwrap(); + self.con.tx.send(DebugResp::Resumed).unwrap(); } // CALL instruction at (pc) 0xc4 | 0xd4 | 0xe4 | 0xf4 | 0xcc | 0xcd | 0xdc | 0xec | 0xfc => { @@ -265,13 +267,13 @@ impl DebuggerServer { address: addr_next, once: true, actions: vec![ - DebugCmd::Pause, + DebugCmd::Pause(PauseReason::DebuggerRequested), DebugCmd::Message("Stepped over CALL".to_string()), DebugCmd::GetState, ], }); machine.set_paused(false); - self.con.tx.send(DebugResp::IsPaused(false)).unwrap(); + self.con.tx.send(DebugResp::Resumed).unwrap(); } // other instructions. just step _ => { @@ -288,16 +290,16 @@ impl DebuggerServer { machine.set_paused(true); self.send_state(machine, cpu); } - DebugCmd::Pause => { + DebugCmd::Pause(reason) => { machine.set_paused(true); - self.con.tx.send(DebugResp::IsPaused(true)).unwrap(); + self.con.tx.send(DebugResp::Paused(*reason)).unwrap(); } DebugCmd::Continue => { machine.mem_out_of_bounds.set(None); machine.set_paused(false); if !self.on_out_of_bounds(machine, cpu) { - self.con.tx.send(DebugResp::IsPaused(false)).unwrap(); + self.con.tx.send(DebugResp::Resumed).unwrap(); } } DebugCmd::AddTrigger(t) => { diff --git a/agon-light-emulator-debugger/Cargo.toml b/agon-light-emulator-debugger/Cargo.toml index d991e4c..f1b3aff 100644 --- a/agon-light-emulator-debugger/Cargo.toml +++ b/agon-light-emulator-debugger/Cargo.toml @@ -10,4 +10,5 @@ license.workspace = true [dependencies] ctrlc = "3.4.1" rustyline = "12.0.0" +inline_colorization = "0.1.6" agon-ez80-emulator = { workspace = true } diff --git a/agon-light-emulator-debugger/src/lib.rs b/agon-light-emulator-debugger/src/lib.rs index 70c66a5..0d73d2b 100644 --- a/agon-light-emulator-debugger/src/lib.rs +++ b/agon-light-emulator-debugger/src/lib.rs @@ -1,25 +1,25 @@ +use inline_colorization::*; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::sync::mpsc::{Receiver, Sender}; mod parser; -use agon_ez80_emulator::debugger::{DebugCmd, DebugResp, Reg16, Registers}; +use agon_ez80_emulator::debugger::{DebugCmd, DebugResp, PauseReason, Reg16, Registers}; #[derive(Clone)] struct EmuState { - pub ez80_paused: std::sync::Arc, + pub ez80_paused: std::cell::Cell, pub emulator_shutdown: std::sync::Arc, } impl EmuState { pub fn is_in_debugger(&self) -> bool { - self.ez80_paused.load(std::sync::atomic::Ordering::SeqCst) + self.ez80_paused.get() } pub fn set_in_debugger(&self, state: bool) { - self.ez80_paused - .store(state, std::sync::atomic::Ordering::SeqCst); + self.ez80_paused.set(state); } pub fn is_emulator_shutdown(&self) -> bool { @@ -34,35 +34,41 @@ impl EmuState { } } +fn print_help_line(command: &str, desc: &str) { + println!("{color_cyan}{: <30}{color_white}{}", command, desc); +} + fn print_help() { - println!("While CPU is paused:"); - println!("br[eak]
Set a breakpoint at the hex address"); - println!("c[ontinue] Resume (un-pause) Agon CPU"); - println!("delete
Delete a breakpoint"); - println!("dis[assemble] [start] [end] Disassemble in current ADL mode"); - println!("dis16 [start] [end] Disassemble in ADL=0 (Z80) mode"); - println!("dis24 [start] [end] Disassemble in ADL=1 (24-bit) mode"); - println!("exit Quit from Agon Light Emulator"); - println!("info breakpoints List breakpoints"); - println!("[mem]ory [len] Dump memory"); - println!("n[ext] Step over function calls"); - println!("pause Pause execution and enter debugger"); - println!("state Show CPU state"); - println!(". Show CPU state"); - println!("s[tep] Execute one instuction"); - println!("trace on Enable logging every instruction"); - println!("trace off Disable logging every instruction"); - println!("trigger
cmd1 : cmd2 : ..."); + print_help_line("br[eak]
", "Set a breakpoint at the hex address"); + print_help_line("c[ontinue]", "Resume (un-pause) Agon CPU"); + print_help_line("delete
", "Delete a breakpoint"); + print_help_line( + "dis[assemble] [start] [end]", + "Disassemble in current ADL mode", + ); + print_help_line("dis16 [start] [end]", "Disassemble in ADL=0 (Z80) mode"); + print_help_line("dis24 [start] [end]", "Disassemble in ADL=1 (24-bit) mode"); + print_help_line("exit", "Quit from Agon Light Emulator"); + print_help_line("info breakpoints", "List breakpoints"); + print_help_line("[mem]ory [len]", "Dump memory"); + print_help_line("n[ext]", "Step over function calls"); + print_help_line("pause", "Pause execution and enter debugger"); + print_help_line("state", "Show CPU state"); + print_help_line(".", "Show CPU state"); + print_help_line("s[tep]", "Execute one instuction"); + print_help_line("trace on", "Enable logging every instruction"); + print_help_line("trace off", "Disable logging every instruction"); + println!("{color_cyan}trigger
cmd1 : cmd2 : ...{color_white}"); println!(" Perform debugger commands when
is reached"); println!(" eg: break $123 is equivalent to:"); println!(" trigger $123 pause:\"CPU paused at breakpoint\":state"); println!(); - println!("triggers List triggers"); + print_help_line("triggers", "List triggers"); println!(); println!("The previous command can be repeated by pressing return."); println!(); - println!("CPU running. Press to pause"); - println!(); + println!("{color_green}CPU running. Press to pause"); + println!("{color_reset}"); } fn do_cmd(cmd: parser::Cmd, tx: &Sender, rx: &Receiver, state: &EmuState) { @@ -80,7 +86,7 @@ fn do_cmd(cmd: parser::Cmd, tx: &Sender, rx: &Receiver, sta fn eval_cmd(text: &str, tx: &Sender, rx: &Receiver, state: &EmuState) { match parser::parse_cmd(&mut parser::tokenize(text).into_iter().peekable()) { Ok(cmd) => do_cmd(cmd, tx, rx, state), - Err(msg) => println!("{}", msg), + Err(msg) => println!("{color_red}{}{color_reset}", msg), } } @@ -125,10 +131,34 @@ fn handle_debug_resp(resp: &DebugResp, state: &EmuState) { } } DebugResp::Message(s) => { - println!("{}", s); + println!("{color_yellow}{}{color_reset}", s); + } + DebugResp::Paused(reason) => { + match reason { + PauseReason::DebuggerRequested => { + println!("{color_yellow}CPU paused{color_reset}"); + } + PauseReason::OutOfBoundsMemAccess(address) => { + println!( + "{color_yellow}CPU paused (memory 0x{:x} out of bounds){color_reset}", + address + ); + } + PauseReason::DebuggerBreakpoint => { + println!("{color_yellow}CPU paused (breakpoint){color_reset}"); + } + PauseReason::IOBreakpoint(io_address) => { + println!( + "{color_yellow}CPU paused (IO breakpoint 0x{:x}){color_reset}", + io_address + ); + } + } + state.set_in_debugger(true); } - DebugResp::IsPaused(p) => { - state.set_in_debugger(*p); + DebugResp::Resumed => { + println!("{color_green}CPU running{color_reset}"); + state.set_in_debugger(false); } DebugResp::Triggers(bs) => { println!("Triggers:"); @@ -196,10 +226,10 @@ pub fn start( tx: Sender, rx: Receiver, emulator_shutdown: std::sync::Arc, - ez80_paused: std::sync::Arc, + ez80_paused: bool, ) { let state = EmuState { - ez80_paused, + ez80_paused: std::cell::Cell::new(ez80_paused), emulator_shutdown, }; let tx_from_ctrlc = tx.clone(); @@ -207,32 +237,37 @@ pub fn start( // should be able to get this from rl.history(), but couldn't figure out the API... let mut last_cmd: Option = None; - println!("Agon Light Emulator Debugger"); + println!(); + println!("{style_bold}Agon EZ80 Debugger{style_reset}"); println!(); print_help(); if state.is_in_debugger() { - println!("Interrupting execution."); + println!("{color_yellow}Interrupting execution.{color_reset}"); } { let _state = state.clone(); ctrlc::set_handler(move || { - _state.set_in_debugger(true); - println!("Interrupting execution."); - tx_from_ctrlc.send(DebugCmd::Pause).unwrap(); + print!("\r"); + tx_from_ctrlc + .send(DebugCmd::Pause(PauseReason::DebuggerRequested)) + .unwrap(); tx_from_ctrlc.send(DebugCmd::GetState).unwrap(); }) .expect("Error setting Ctrl-C handler"); } + let prompt = &format!("{color_cyan}>> "); + // `()` can be used when no completer is required let mut rl = DefaultEditor::new().unwrap(); while !state.is_emulator_shutdown() { while state.is_in_debugger() { std::thread::sleep(std::time::Duration::from_millis(50)); drain_rx(&rx, &state); - let readline = rl.readline(">> "); + let readline = rl.readline(prompt); + print!("{color_reset}"); match readline { Ok(line) => { if line != "" { @@ -255,7 +290,7 @@ pub fn start( break; } Err(err) => { - println!("Error: {:?}", err); + println!("{color_red}Error: {:?}{color_reset}", err); break; } } diff --git a/agon-light-emulator-debugger/src/parser.rs b/agon-light-emulator-debugger/src/parser.rs index f3a9381..815d950 100644 --- a/agon-light-emulator-debugger/src/parser.rs +++ b/agon-light-emulator-debugger/src/parser.rs @@ -1,4 +1,4 @@ -use agon_ez80_emulator::debugger::{DebugCmd, Trigger}; +use agon_ez80_emulator::debugger::{DebugCmd, PauseReason, Trigger}; #[derive(Debug)] pub enum Cmd { @@ -102,7 +102,7 @@ pub fn parse_cmd(tokens: &mut Tokens) -> Result { } "pause" => { expect_end_of_cmd(tokens)?; - Ok(Cmd::Core(DebugCmd::Pause)) + Ok(Cmd::Core(DebugCmd::Pause(PauseReason::DebuggerRequested))) } "help" => { expect_end_of_cmd(tokens)?; @@ -130,8 +130,7 @@ pub fn parse_cmd(tokens: &mut Tokens) -> Result { address: addr, once: false, actions: vec![ - DebugCmd::Pause, - DebugCmd::Message("CPU paused at breakpoint".to_string()), + DebugCmd::Pause(PauseReason::DebuggerBreakpoint), DebugCmd::GetState, ], }))) diff --git a/src/main.rs b/src/main.rs index 4106311..5268f54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::parse_args::parse_args; +use agon_ez80_emulator::debugger; use agon_ez80_emulator::debugger::{DebugCmd, DebugResp, DebuggerConnection, Trigger}; use agon_ez80_emulator::{gpio, AgonMachine, AgonMachineConfig, RamInit, SerialLink}; use sdl2::event::Event; @@ -76,8 +77,7 @@ pub fn main() -> Result<(), pico_args::Error> { address: *breakpoint, once: false, actions: vec![ - DebugCmd::Pause, - DebugCmd::Message("CPU paused at breakpoint".to_owned()), + DebugCmd::Pause(debugger::PauseReason::DebuggerBreakpoint), DebugCmd::GetState, ], }; @@ -97,7 +97,7 @@ pub fn main() -> Result<(), pico_args::Error> { tx_cmd_debugger, rx_resp_debugger, _emulator_shutdown, - _ez80_paused, + _ez80_paused.load(std::sync::atomic::Ordering::Relaxed), ); }); Some(DebuggerConnection {