Skip to content

Commit

Permalink
refactor: Improve DAP server code organisation and readability (#6770)
Browse files Browse the repository at this point in the history
## Description
This PR improves the organization and readability of the forc-debug
codebase through several changes:

- Moves CLI functionality into a dedicated CLI module
- Creates a unified error handling system using `thiserror`
- Improves command handler organization and readability
- Extracts common functionality into reusable helper methods
- Adds clear documentation for public interfaces
- Introduces the `HandlerResult` type to simplify DAP server response
handling
- Implements consistent error propagation throughout the codebase
- Ran clippy pedantic

Improvements:
- Better separation of concerns between CLI and server code
- More descriptive error messages with proper context
- Cleaner command handling 

## Checklist

- [ ] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
JoshuaBatty authored Dec 3, 2024
1 parent 5423c56 commit 48f633d
Show file tree
Hide file tree
Showing 19 changed files with 1,117 additions and 850 deletions.
332 changes: 332 additions & 0 deletions forc-plugins/forc-debug/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
use crate::{
error::{ArgumentError, Error, Result},
names::{register_index, register_name},
ContractId, FuelClient, RunResult, Transaction,
};
use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
use shellfish::{handler::DefaultAsyncHandler, input_handler::IO, Command as ShCommand, Shell};

pub struct State {
client: FuelClient,
session_id: String,
}

/// Start the CLI debug interface
pub async fn start_cli(api_url: &str) -> Result<()> {
let mut shell = Shell::new_async(
State {
client: FuelClient::new(api_url).map_err(|e| Error::FuelClientError(e.to_string()))?,
session_id: String::new(), // Placeholder
},
">> ",
);

register_commands(&mut shell);

let session_id = shell
.state
.client
.start_session()
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
shell.state.session_id.clone_from(&session_id);

shell
.run_async()
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

shell
.state
.client
.end_session(&session_id)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

Ok(())
}

fn register_commands(shell: &mut Shell<'_, State, &str, DefaultAsyncHandler, IO>) {
// Registers an async command by wrapping the handler function `$f`,
// converting its error type into `Box<dyn std::error::Error>`, and
// associating it with the provided command names.
macro_rules! command {
($f:ident, $help:literal, $names:expr) => {
for c in $names {
shell.commands.insert(
c,
ShCommand::new_async($help.to_string(), |state, args| {
Box::pin(async move {
$f(state, args)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
})
}),
);
}
};
}

command!(
cmd_start_tx,
"path/to/tx.json -- start a new transaction",
["n", "tx", "new_tx", "start_tx"]
);
command!(
cmd_reset,
"-- reset, removing breakpoints and other state",
["reset"]
);
command!(
cmd_continue,
"-- run until next breakpoint or termination",
["c", "continue"]
);
command!(
cmd_step,
"[on|off] -- turn single-stepping on or off",
["s", "step"]
);
command!(
cmd_breakpoint,
"[contract_id] offset -- set a breakpoint",
["b", "breakpoint"]
);
command!(
cmd_registers,
"[regname ...] -- dump registers",
["r", "reg", "register", "registers"]
);
command!(cmd_memory, "[offset] limit -- dump memory", ["m", "memory"]);
}

async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name
ArgumentError::ensure_arg_count(&args, 1, 1)?; // Ensure exactly one argument

let path_to_tx_json = args.pop().unwrap(); // Safe due to arg count check

// Read and parse the transaction JSON
let tx_json = std::fs::read(&path_to_tx_json).map_err(Error::IoError)?;
let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?;

// Start the transaction
let status = state
.client
.start_tx(&state.session_id, &tx)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

pretty_print_run_result(&status);
Ok(())
}

async fn cmd_reset(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name
ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments

// Reset the session
state
.client
.reset(&state.session_id)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

Ok(())
}

async fn cmd_continue(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name
ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments

// Continue the transaction
let status = state
.client
.continue_tx(&state.session_id)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

pretty_print_run_result(&status);
Ok(())
}

async fn cmd_step(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name
ArgumentError::ensure_arg_count(&args, 0, 1)?; // Ensure the argument count is at most 1

// Determine whether to enable or disable single stepping
let enable = args
.first()
.map_or(true, |v| !["off", "no", "disable"].contains(&v.as_str()));

// Call the client
state
.client
.set_single_stepping(&state.session_id, enable)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

Ok(())
}

async fn cmd_breakpoint(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove command name
ArgumentError::ensure_arg_count(&args, 1, 2)?;

let offset_str = args.pop().unwrap(); // Safe due to arg count check
let offset = parse_int(&offset_str).ok_or(ArgumentError::InvalidNumber(offset_str))?;

let contract = if let Some(contract_id) = args.pop() {
contract_id
.parse::<ContractId>()
.map_err(|_| ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id)))?
} else {
ContractId::zeroed()
};

// Call client
state
.client
.set_breakpoint(&state.session_id, contract, offset as u64)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

Ok(())
}

async fn cmd_registers(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name

if args.is_empty() {
// Print all registers
for r in 0..VM_REGISTER_COUNT {
let value = state
.client
.register(&state.session_id, r as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{:#x}] = {:<8} # {}", r, value, register_name(r));
}
} else {
// Process specific registers provided as arguments
for arg in &args {
if let Some(v) = parse_int(arg) {
if v < VM_REGISTER_COUNT {
let value = state
.client
.register(&state.session_id, v as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{:#02x}] = {:<8} # {}", v, value, register_name(v));
} else {
return Err(ArgumentError::InvalidNumber(format!(
"Register index too large: {v}"
))
.into());
}
} else if let Some(index) = register_index(arg) {
let value = state
.client
.register(&state.session_id, index as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{index:#02x}] = {value:<8} # {arg}");
} else {
return Err(ArgumentError::Invalid(format!("Unknown register name: {arg}")).into());
}
}
}
Ok(())
}

async fn cmd_memory(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name

// Parse limit argument or use the default
let limit = args
.pop()
.map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
.transpose()?
.unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize));

// Parse offset argument or use the default
let offset = args
.pop()
.map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
.transpose()?
.unwrap_or(0);

// Ensure no extra arguments
ArgumentError::ensure_arg_count(&args, 0, 0)?;

// Fetch memory from the client
let mem = state
.client
.memory(&state.session_id, offset as u32, limit as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

// Print memory contents
for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() {
print!(" {:06x}:", offset + i * WORD_SIZE);
for byte in chunk {
print!(" {byte:02x}");
}
println!();
}
Ok(())
}

/// Pretty-prints the result of a run, including receipts and breakpoint information.
///
/// Outputs each receipt in the `RunResult` and details about the breakpoint if present.
/// If the execution terminated without hitting a breakpoint, it prints "Terminated".
fn pretty_print_run_result(rr: &RunResult) {
for receipt in rr.receipts() {
println!("Receipt: {receipt:?}");
}
if let Some(bp) = &rr.breakpoint {
println!(
"Stopped on breakpoint at address {} of contract {}",
bp.pc.0, bp.contract
);
} else {
println!("Terminated");
}
}

/// Parses a string representing a number and returns it as a `usize`.
///
/// The input string can be in decimal or hexadecimal format:
/// - Decimal numbers are parsed normally (e.g., `"123"`).
/// - Hexadecimal numbers must be prefixed with `"0x"` (e.g., `"0x7B"`).
/// - Underscores can be used as visual separators (e.g., `"1_000"` or `"0x1_F4"`).
///
/// If the input string is not a valid number in the specified format, `None` is returned.
///
/// # Examples
///
/// ```
/// use forc_debug::cli::parse_int;
/// /// Use underscores as separators in decimal and hexadecimal numbers
/// assert_eq!(parse_int("123"), Some(123));
/// assert_eq!(parse_int("1_000"), Some(1000));
///
/// /// Parse hexadecimal numbers with "0x" prefix
/// assert_eq!(parse_int("0x7B"), Some(123));
/// assert_eq!(parse_int("0x1_F4"), Some(500));
///
/// /// Handle invalid inputs gracefully
/// assert_eq!(parse_int("abc"), None);
/// assert_eq!(parse_int("0xZZZ"), None);
/// assert_eq!(parse_int(""), None);
/// ```
///
/// # Errors
///
/// Returns `None` if the input string contains invalid characters,
/// is not properly formatted, or cannot be parsed into a `usize`.
pub fn parse_int(s: &str) -> Option<usize> {
let (s, radix) = s.strip_prefix("0x").map_or((s, 10), |s| (s, 16));
usize::from_str_radix(&s.replace('_', ""), radix).ok()
}
Loading

0 comments on commit 48f633d

Please sign in to comment.