use super::{
configuration::{self, ConsoleLog},
logger::DebugLogger,
session_data::SessionData,
startup::{TargetSessionType, get_file_timestamp},
};
use crate::{
cmd::dap_server::{
DebuggerError,
debug_adapter::{
dap::{
adapter::{DebugAdapter, get_arguments},
dap_types::{
Capabilities, DisconnectResponse, Event, ExitedEventBody,
InitializeRequestArguments, MessageSeverity, Request, RttWindowOpenedArguments,
TerminatedEventBody,
},
request_helpers::halt_core,
},
protocol::ProtocolAdapter,
},
peripherals::svd_variables::SvdCache,
server::configuration::SessionConfig,
},
rpc::functions::flash::Operation,
util::flash::build_loader,
};
use anyhow::{Context, anyhow};
use probe_rs::{
CoreStatus,
config::Registry,
flashing::{DownloadOptions, FileDownloadError, FlashError, FlashProgress, ProgressEvent},
probe::list::Lister,
};
use std::{
collections::HashMap,
fs,
path::Path,
time::{Duration, UNIX_EPOCH},
};
use time::UtcOffset;
#[derive(Debug)]
pub(crate) enum DebugSessionStatus {
Continue(Duration),
Terminate,
Restart(Request),
}
pub struct Debugger {
config: configuration::SessionConfig,
timestamp_offset: UtcOffset,
binary_timestamp: Option<Duration>,
pub(crate) debug_logger: DebugLogger,
}
impl Debugger {
pub fn new(
timestamp_offset: UtcOffset,
log_file: Option<&Path>,
) -> Result<Self, DebuggerError> {
let mut debugger = Self {
config: configuration::SessionConfig::default(),
timestamp_offset,
binary_timestamp: None,
debug_logger: DebugLogger::new(log_file)?,
};
debugger
.debug_logger
.log_to_console("Starting probe-rs as a DAP Protocol server")?;
Ok(debugger)
}
pub(crate) async fn process_next_request<P: ProtocolAdapter>(
&mut self,
session_data: &mut SessionData,
debug_adapter: &mut DebugAdapter<P>,
) -> Result<DebugSessionStatus, DebuggerError> {
self.debug_logger.flush_to_dap(debug_adapter)?;
let Some(request) = debug_adapter.listen_for_request()? else {
let _poll_span = tracing::trace_span!("Polling for core status").entered();
let mut delay = Duration::ZERO;
if debug_adapter.all_cores_halted {
tracing::trace!(
"Sleeping (all cores are halted) for 100ms to reduce polling overheaads."
);
delay = Duration::from_millis(100);
} else {
let (_, suggest_delay_required) =
session_data.poll_cores(&self.config, debug_adapter).await?;
if debug_adapter.configuration_is_done() && suggest_delay_required {
tracing::trace!(
"Sleeping (core is running) for 50ms to reduce polling overheads."
);
delay = Duration::from_millis(50);
} else {
tracing::trace!(
"Retrieving data from the core, no delay required between iterations of polling the core."
);
};
}
return Ok(DebugSessionStatus::Continue(delay));
};
let _req_span = tracing::info_span!("Handling request", request = ?request).entered();
let (core_statuses, _) = session_data.poll_cores(&self.config, debug_adapter).await?;
if core_statuses.is_empty() {
if debug_adapter.configuration_is_done() {
return Err(DebuggerError::Other(anyhow!(
"Cannot continue unless one target core configuration is defined."
)));
}
return Ok(DebugSessionStatus::Continue(Duration::ZERO));
}
let core_id = 0;
let Some(target_core_config) = self.config.core_configs.get(core_id) else {
return Err(DebuggerError::Other(anyhow!(
"No core configuration found for core id {core_id}"
)));
};
let new_status = core_statuses[core_id];
let mut target_core = session_data
.attach_core(target_core_config.core_index)
.context("Unable to connect to target core")?;
let mut unhalt_me = false;
match request.command.as_ref() {
"configurationDone"
| "setBreakpoints"
| "setInstructionBreakpoints"
| "clearBreakpoint"
| "stackTrace"
| "threads"
| "scopes"
| "variables"
| "readMemory"
| "writeMemory"
| "disassemble" => {
if new_status == CoreStatus::Sleeping {
match target_core.core.halt(Duration::from_millis(100)) {
Ok(_) => unhalt_me = true,
Err(error) => {
let err = DebuggerError::from(error);
debug_adapter.send_response::<()>(&request, Err(&err))?;
return Err(err);
}
}
}
}
_ => {}
}
let mut debug_session = DebugSessionStatus::Continue(Duration::ZERO);
let result = match request.command.as_ref() {
"rttWindowOpened" => {
if let Some(debugger_rtt_target) = target_core.core_data.rtt_connection.as_mut() {
let arguments: RttWindowOpenedArguments =
get_arguments(debug_adapter, &request)?;
if let Some(rtt_channel) = debugger_rtt_target
.debugger_rtt_channels
.iter_mut()
.find(|debugger_rtt_channel| {
debugger_rtt_channel.channel_number == arguments.channel_number
})
{
rtt_channel.has_client_window = arguments.window_is_open;
}
debug_adapter
.send_response::<()>(&request, Ok(None))
.map_err(|error| {
DebuggerError::Other(anyhow!(
"Could not deserialize arguments for RttWindowOpened : {error:?}."
))
})?;
}
Ok(())
}
"disconnect" => {
let result = debug_adapter.disconnect(&mut target_core, &request);
debug_session = DebugSessionStatus::Terminate;
result
}
"next" => debug_adapter.next(&mut target_core, &request),
"stepIn" => debug_adapter.step_in(&mut target_core, &request),
"stepOut" => debug_adapter.step_out(&mut target_core, &request),
"pause" => debug_adapter.pause(&mut target_core, &request),
"readMemory" => debug_adapter.read_memory(&mut target_core, &request),
"writeMemory" => debug_adapter.write_memory(&mut target_core, &request),
"setVariable" => debug_adapter.set_variable(&mut target_core, &request),
"configurationDone" => debug_adapter.configuration_done(&mut target_core, &request),
"threads" => debug_adapter.threads(&mut target_core, &request),
"restart" => {
let result = target_core
.core
.halt(Duration::from_millis(500))
.map_err(|error| anyhow!("Failed to halt core: {error:?}"))
.and(Ok(()));
debug_session = DebugSessionStatus::Restart(request);
result
}
"setBreakpoints" => debug_adapter.set_breakpoints(&mut target_core, &request),
"setInstructionBreakpoints" => {
debug_adapter.set_instruction_breakpoints(&mut target_core, &request)
}
"stackTrace" => debug_adapter.stack_trace(&mut target_core, &request),
"scopes" => debug_adapter.scopes(&mut target_core, &request),
"disassemble" => debug_adapter.disassemble(&mut target_core, &request),
"variables" => debug_adapter.variables(&mut target_core, &request),
"continue" => debug_adapter.r#continue(&mut target_core, &request),
"evaluate" => debug_adapter.evaluate(&mut target_core, &request),
"completions" => debug_adapter.completions(&mut target_core, &request),
other_command => {
debug_adapter.send_response::<()>(
&request,
Err(&DebuggerError::Other(anyhow!(
"Received request '{other_command}', which is not supported or not implemented yet"
))),
)
}
};
result.map_err(|e| DebuggerError::Other(e.context("Error executing request.")))?;
if unhalt_me && let Err(error) = target_core.core.run() {
let error = DebuggerError::Other(anyhow!(error).context("Failed to resume target."));
debug_adapter.show_error_message(&error)?;
return Err(error);
}
Ok(debug_session)
}
pub(crate) async fn debug_session<P: ProtocolAdapter + 'static>(
&mut self,
mut debug_adapter: DebugAdapter<P>,
lister: &Lister,
) -> Result<(), DebuggerError> {
let mut registry = Registry::from_builtin_families();
if self.handle_initialize(&mut debug_adapter).is_err() {
return Ok(());
}
let expected_commands = ["launch", "attach"];
let launch_attach_request = loop {
if let Some(request) = debug_adapter.listen_for_request()? {
if expected_commands.contains(&request.command.as_str()) {
self.debug_logger.flush_to_dap(&mut debug_adapter)?;
break request;
} else if request.command == "disconnect" {
debug_adapter.send_response::<DisconnectResponse>(&request, Ok(None))?;
return Ok(());
} else {
debug_adapter.log_to_console(format!(
"Ignoring request with command '{}', we can only handle 'launch' and 'attach' commands.", request.command
));
let err = DebuggerError::Other(anyhow!(
"Unable to process request with command {} before an attach or launch request is received",
request.command
));
debug_adapter.send_response::<()>(&request, Err(&err))?;
}
}
};
let mut session_data = match self
.handle_launch_attach(
&mut registry,
&launch_attach_request,
&mut debug_adapter,
lister,
)
.await
{
Ok(session_data) => session_data,
Err(error) => {
debug_adapter.send_response::<()>(&launch_attach_request, Err(&error))?;
return Ok(());
}
};
if debug_adapter
.send_event::<Event>("initialized", None)
.is_err()
{
let error =
DebuggerError::Other(anyhow!("Failed sending 'initialized' event to DAP Client"));
debug_adapter.show_error_message(&error)?;
return Err(error);
}
let error = loop {
let debug_session_status = match self
.process_next_request(&mut session_data, &mut debug_adapter)
.await
{
Ok(status) => status,
Err(error) => break error,
};
match debug_session_status {
DebugSessionStatus::Continue(delay) => {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
}
DebugSessionStatus::Restart(request) => {
if let Err(error) = self
.restart(&mut debug_adapter, &mut session_data, &request)
.await
{
debug_adapter.send_response::<()>(&request, Err(&error))?;
return Err(error);
}
}
DebugSessionStatus::Terminate => {
session_data.clean_up(&self.config)?;
return Ok(());
}
};
};
debug_adapter.show_message(
MessageSeverity::Error,
format!("Debug Adapter terminated unexpectedly with an error: {error:?}"),
);
debug_adapter.send_event("terminated", Some(TerminatedEventBody { restart: None }))?;
debug_adapter.send_event("exited", Some(ExitedEventBody { exit_code: 1 }))?;
for _loop_count in 0..10 {
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(error)
}
#[tracing::instrument(skip_all, name = "Handle Launch/Attach Request")]
pub(crate) async fn handle_launch_attach<P: ProtocolAdapter + 'static>(
&mut self,
registry: &mut Registry,
launch_attach_request: &Request,
debug_adapter: &mut DebugAdapter<P>,
lister: &Lister,
) -> Result<SessionData, DebuggerError> {
let requested_target_session_type = match launch_attach_request.command.as_str() {
"attach" => TargetSessionType::AttachRequest,
"launch" => TargetSessionType::LaunchRequest,
other => {
return Err(DebuggerError::Other(anyhow!(
"Expected request 'launch' or 'attach', but received '{other}'"
)));
}
};
self.config = get_arguments(debug_adapter, launch_attach_request)?;
self.config
.validate_configuration_option_compatibility(requested_target_session_type)?;
debug_adapter
.set_console_log_level(self.config.console_log_level.unwrap_or(ConsoleLog::Console));
self.config.validate_config_files()?;
let mut session_data =
SessionData::new(registry, lister, &mut self.config, self.timestamp_offset)?;
debug_adapter.halt_after_reset = self.config.flashing_config.halt_after_reset;
let Some(target_core_config) = self.config.core_configs.first() else {
return Err(DebuggerError::Other(anyhow!(
"Cannot continue unless one target core configuration is defined."
)));
};
if self.config.flashing_config.flashing_enabled {
let Some(path_to_elf) = &target_core_config.program_binary else {
return Err(DebuggerError::Other(anyhow!(
"Please specify use the `program-binary` option in `launch.json` to specify an executable"
)));
};
self.binary_timestamp = get_file_timestamp(path_to_elf);
Self::flash(
&self.config,
path_to_elf,
debug_adapter,
launch_attach_request,
&mut session_data,
)?;
}
let mut target_core = session_data.attach_core(target_core_config.core_index)?;
halt_core(&mut target_core.core)?;
if let Some(svd_file) = &target_core_config.svd_file {
target_core.core_data.core_peripherals =
match SvdCache::new(svd_file, debug_adapter, launch_attach_request.seq) {
Ok(core_peripherals) => Some(core_peripherals),
Err(error) => {
tracing::warn!("{:?}", error);
None
}
};
}
if requested_target_session_type == TargetSessionType::LaunchRequest {
debug_adapter
.restart(&mut target_core, None)
.context("Failed to restart core")?;
} else {
target_core.core.debug_on_sw_breakpoint(true)?;
}
drop(target_core);
session_data.poll_cores(&self.config, debug_adapter).await?;
debug_adapter.send_response::<()>(launch_attach_request, Ok(None))?;
self.debug_logger.flush_to_dap(debug_adapter)?;
Ok(session_data)
}
#[tracing::instrument(skip_all)]
async fn restart<P: ProtocolAdapter + 'static>(
&mut self,
debug_adapter: &mut DebugAdapter<P>,
session_data: &mut SessionData,
request: &Request,
) -> Result<(), DebuggerError> {
let Some(target_core_config) = self.config.core_configs.first() else {
return Err(DebuggerError::Other(anyhow!(
"Cannot continue unless one target core configuration is defined."
)));
};
if self.config.flashing_config.flashing_enabled {
let Some(path_to_elf) = &target_core_config.program_binary else {
return Err(DebuggerError::Other(anyhow!(
"Please specify use the `program-binary` option in `launch.json` to specify an executable"
)));
};
if is_file_newer(&mut self.binary_timestamp, path_to_elf) {
session_data.load_debug_info_for_core(target_core_config)?;
session_data
.attach_core(target_core_config.core_index)
.map(|mut target_core| target_core.recompute_breakpoints())??;
session_data.load_rtt_location(&self.config)?;
Self::flash(
&self.config,
path_to_elf,
debug_adapter,
request,
session_data,
)?;
}
}
let mut target_core = session_data.attach_core(target_core_config.core_index)?;
halt_core(&mut target_core.core)?;
target_core.core_data.rtt_connection = None;
drop(target_core);
session_data.poll_cores(&self.config, debug_adapter).await?;
let mut target_core = session_data.attach_core(target_core_config.core_index)?;
debug_adapter
.restart(&mut target_core, Some(request))
.context("Failed to restart core")?;
Ok(())
}
fn flash<P: ProtocolAdapter + 'static>(
config: &SessionConfig,
path_to_elf: &Path,
debug_adapter: &mut DebugAdapter<P>,
launch_attach_request: &Request,
session_data: &mut SessionData,
) -> Result<(), DebuggerError> {
debug_adapter.log_to_console(format!(
"FLASHING: Starting write of {:?} to device memory",
&path_to_elf
));
let progress_id = debug_adapter
.start_progress("Flashing device", Some(launch_attach_request.seq))
.ok();
let mut download_options = DownloadOptions::default();
download_options.keep_unwritten_bytes = config.flashing_config.restore_unwritten_bytes;
download_options.do_chip_erase = config.flashing_config.full_chip_erase;
download_options.verify = config.flashing_config.verify_after_flashing;
#[derive(Default)]
struct ProgressBarState {
total_size: u64,
size_done: u64,
}
type ProgressState = HashMap<Operation, ProgressBarState>;
download_options.progress = progress_id
.map(|id| {
let describe_op = |operation| match Operation::from(operation) {
Operation::Fill => "Reading Old Pages",
Operation::Erase => "Erasing Sectors",
Operation::Program => "Programming Pages",
Operation::Verify => "Verifying",
};
let mut flash_progress = ProgressState::default();
let debug_adapter = &mut *debug_adapter;
FlashProgress::new(move |event| {
match event {
ProgressEvent::AddProgressBar { operation, total } => {
let pbar_state = flash_progress.entry(operation.into()).or_default();
if let Some(total) = total {
pbar_state.total_size += total; pbar_state.size_done = 0;
};
}
ProgressEvent::Started(operation) => {
debug_adapter
.update_progress(None, Some(describe_op(operation)), id)
.ok();
}
ProgressEvent::Progress {
operation, size, ..
} => {
let pbar_state = flash_progress.entry(operation.into()).or_default();
pbar_state.size_done += size;
let progress =
pbar_state.size_done as f64 / pbar_state.total_size as f64;
debug_adapter
.update_progress(Some(progress), Some(describe_op(operation)), id)
.ok();
}
ProgressEvent::Failed(operation) => {
debug_adapter
.update_progress(
Some(1.0),
Some(format!("{} Failed!", describe_op(operation))),
id,
)
.ok();
}
ProgressEvent::Finished(operation) => {
debug_adapter
.update_progress(
Some(1.0),
Some(format!("{} Complete!", describe_op(operation))),
id,
)
.ok();
}
ProgressEvent::FlashLayoutReady { .. } => {}
ProgressEvent::DiagnosticMessage { .. } => {}
}
})
})
.unwrap_or_default();
let result = match build_loader(
&mut session_data.session,
path_to_elf,
config.flashing_config.format_options.clone(),
None,
) {
Ok(loader) => {
let do_flashing = if config.flashing_config.verify_before_flashing {
match loader.verify(&mut session_data.session, &mut download_options.progress) {
Ok(_) => false,
Err(FlashError::Verify) => true,
Err(other) => {
return Err(DebuggerError::FileDownload(FileDownloadError::Flash(
other,
)));
}
}
} else {
true
};
for core_data in session_data.core_data.iter_mut() {
if let probe_rs::rtt::ScanRegion::Exact(address) = core_data.rtt_scan_ranges {
core_data.clear_rtt_header = !loader.has_data_for_address(address);
}
}
if do_flashing {
loader
.commit(&mut session_data.session, download_options)
.map_err(FileDownloadError::Flash)
} else {
drop(download_options);
Ok(())
}
}
Err(error) => {
drop(download_options);
Err(error)
}
};
if let Some(id) = progress_id {
let _ = debug_adapter.end_progress(id);
}
if result.is_ok() {
debug_adapter.log_to_console(format!(
"FLASHING: Completed write of {:?} to device memory",
&path_to_elf
));
}
result.map_err(DebuggerError::FileDownload)
}
#[tracing::instrument(skip_all, name = "Handling initialize request")]
pub(crate) fn handle_initialize<P: ProtocolAdapter>(
&mut self,
debug_adapter: &mut DebugAdapter<P>,
) -> Result<(), DebuggerError> {
let initialize_request = loop {
if let Some(current_request) = debug_adapter.listen_for_request()? {
if current_request.command == "initialize" {
break current_request;
} else {
let error = DebuggerError::Other(anyhow!(
"Received request with command'{}', expected to receive the initialize command",
current_request.command,
));
debug_adapter.send_response::<()>(¤t_request, Err(&error))?;
return Err(error);
}
}
};
let initialize_arguments =
get_arguments::<InitializeRequestArguments, _>(debug_adapter, &initialize_request)?;
if let Some(client_id) = initialize_arguments.client_id
&& client_id == "vscode"
{
tracing::info!(
"DAP client reports its 'ClientID' is 'vscode', enabling vscode_quirks."
);
debug_adapter.vscode_quirks = true;
}
if !(initialize_arguments.columns_start_at_1.unwrap_or(true)
&& initialize_arguments.lines_start_at_1.unwrap_or(true))
{
let error = DebuggerError::Other(anyhow!(
"Unsupported Capability: Client requested column and row numbers start at 0."
));
debug_adapter.send_response::<()>(&initialize_request, Err(&error))?;
return Err(error);
}
if let Some(progress_support) = initialize_arguments.supports_progress_reporting {
debug_adapter.supports_progress_reporting = progress_support;
}
if let Some(lines_start_at_1) = initialize_arguments.lines_start_at_1 {
debug_adapter.lines_start_at_1 = lines_start_at_1;
}
if let Some(columns_start_at_1) = initialize_arguments.columns_start_at_1 {
debug_adapter.columns_start_at_1 = columns_start_at_1;
}
let capabilities = Capabilities {
supports_configuration_done_request: Some(true),
supports_restart_request: Some(true),
support_suspend_debuggee: Some(true),
supports_delayed_stack_trace_loading: Some(true),
supports_read_memory_request: Some(true),
supports_write_memory_request: Some(true),
supports_set_variable: Some(true),
supports_clipboard_context: Some(true),
supports_disassemble_request: Some(true),
supports_instruction_breakpoints: Some(true),
supports_stepping_granularity: Some(true),
supports_completions_request: Some(true),
support_terminate_debuggee: Some(true),
..Default::default()
};
debug_adapter.send_response(&initialize_request, Ok(Some(capabilities)))?;
self.debug_logger.flush_to_dap(debug_adapter)?;
Ok(())
}
}
pub(crate) fn is_file_newer(
saved_binary_timestamp: &mut Option<Duration>,
path_to_elf: &Path,
) -> bool {
if let Some(check_current_binary_timestamp) = *saved_binary_timestamp {
if let Some(new_binary_timestamp) = get_file_timestamp(path_to_elf) {
if new_binary_timestamp > check_current_binary_timestamp {
*saved_binary_timestamp = Some(new_binary_timestamp);
true
} else {
false
}
} else {
tracing::warn!("Could not get timestamp for new binary. Assuming it is new.");
true
}
} else {
*saved_binary_timestamp = fs::metadata(path_to_elf)
.and_then(|metadata| metadata.modified())
.map(|modified| modified.duration_since(UNIX_EPOCH).ok())
.ok()
.flatten();
true
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod test {
use crate::cmd::dap_server::{
DebuggerError,
debug_adapter::{
dap::{
adapter::DebugAdapter,
dap_types::{
Capabilities, DisassembleArguments, DisassembleResponseBody,
DisassembledInstruction, DisconnectArguments, ErrorResponseBody,
InitializeRequestArguments, Message, Request, Response, Source, Thread,
ThreadsResponseBody,
},
},
protocol::ProtocolAdapter,
},
server::configuration::{ConsoleLog, CoreConfig, FlashingConfig, SessionConfig},
test::TestLister,
};
use probe_rs::{
architecture::arm::FullyQualifiedApAddress,
integration::{FakeProbe, Operation},
probe::{
DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, ProbeFactory,
list::Lister,
},
};
use serde_json::json;
use std::{
collections::{BTreeMap, HashMap, VecDeque},
fmt::Display,
path::PathBuf,
};
use test_case::test_case;
use time::UtcOffset;
const TEST_CHIP_NAME: &str = "nRF52833_xxAA";
#[derive(Debug)]
struct MockProbeFactory;
impl Display for MockProbeFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Mocked Probe")
}
}
impl ProbeFactory for MockProbeFactory {
fn open(
&self,
_selector: &DebugProbeSelector,
) -> Result<Box<dyn DebugProbe>, DebugProbeError> {
todo!()
}
fn list_probes(&self) -> Vec<DebugProbeInfo> {
todo!()
}
}
fn expected_capabilites() -> Capabilities {
Capabilities {
support_suspend_debuggee: Some(true),
supports_clipboard_context: Some(true),
supports_completions_request: Some(true),
supports_configuration_done_request: Some(true),
supports_delayed_stack_trace_loading: Some(true),
supports_disassemble_request: Some(true),
supports_instruction_breakpoints: Some(true),
supports_read_memory_request: Some(true),
supports_write_memory_request: Some(true),
supports_restart_request: Some(true),
supports_set_variable: Some(true),
supports_stepping_granularity: Some(true),
support_terminate_debuggee: Some(true),
..Default::default()
}
}
fn default_initialize_args() -> InitializeRequestArguments {
InitializeRequestArguments {
client_id: Some("mock_client".to_owned()),
client_name: Some("Mock client for testing".to_owned()),
adapter_id: "mock_adapter".to_owned(),
columns_start_at_1: None,
lines_start_at_1: None,
locale: None,
path_format: None,
supports_args_can_be_interpreted_by_shell: None,
supports_invalidated_event: None,
supports_memory_event: None,
supports_memory_references: None,
supports_progress_reporting: None,
supports_run_in_terminal_request: None,
supports_start_debugging_request: None,
supports_variable_paging: None,
supports_variable_type: None,
supports_ansi_styling: None,
}
}
fn error_response_body(msg: &str) -> ErrorResponseBody {
ErrorResponseBody {
error: Some(error_message(msg)),
}
}
fn error_message(msg: &str) -> Message {
Message {
format: "{response_message}".to_string(),
id: 0,
send_telemetry: Some(false),
show_user: Some(true),
url: Some("https://probe.rs/docs/tools/debugger/".to_string()),
url_label: Some("Documentation".to_string()),
variables: Some(BTreeMap::from([(
"response_message".to_string(),
msg.to_string(),
)])),
}
}
struct RequestBuilder<'r> {
adapter: &'r mut MockProtocolAdapter,
}
impl<'r> RequestBuilder<'r> {
fn with_arguments(self, arguments: impl serde::Serialize) -> Self {
self.adapter.requests.back_mut().unwrap().arguments =
Some(serde_json::to_value(arguments).unwrap());
self
}
fn and_succesful_response(self) -> ResponseBuilder<'r> {
let req = self.adapter.requests.back_mut().unwrap();
let response = Response {
command: req.command.clone(),
request_seq: req.seq,
seq: 0, success: true,
message: None,
body: None,
type_: "response".to_string(),
};
self.adapter.expect_response(response)
}
fn and_error_response(self) -> ResponseBuilder<'r> {
let req = self.adapter.requests.back_mut().unwrap();
let response = Response {
command: req.command.clone(),
request_seq: req.seq,
seq: 0, success: false,
message: Some("cancelled".to_string()), body: None,
type_: "response".to_string(),
};
self.adapter.expect_error_response(response)
}
}
struct ResponseBuilder<'r> {
adapter: &'r mut MockProtocolAdapter,
}
impl ResponseBuilder<'_> {
fn with_body(self, body: impl serde::Serialize) {
let resp = self.adapter.expected_responses.last_mut().unwrap();
resp.body = Some(serde_json::to_value(body).unwrap());
}
}
use super::Debugger;
struct MockProtocolAdapter {
requests: VecDeque<Request>,
pending_requests: HashMap<i64, String>,
sequence_number: i64,
console_log_level: ConsoleLog,
response_index: usize,
expected_responses: Vec<Response>,
event_index: usize,
expected_events: Vec<(String, Option<serde_json::Value>)>,
}
impl MockProtocolAdapter {
fn new() -> Self {
Self {
requests: VecDeque::new(),
sequence_number: 0,
pending_requests: HashMap::new(),
console_log_level: ConsoleLog::Console,
response_index: 0,
expected_responses: Vec::new(),
expected_events: Vec::new(),
event_index: 0,
}
}
fn add_request<'m>(&'m mut self, command: &str) -> RequestBuilder<'m> {
let request = Request {
arguments: None,
command: command.to_string(),
seq: self.sequence_number,
type_: "request".to_string(),
};
self.pending_requests
.insert(self.sequence_number, command.to_string());
self.sequence_number += 1;
self.requests.push_back(request);
RequestBuilder { adapter: self }
}
fn expect_response(&mut self, response: Response) -> ResponseBuilder<'_> {
assert!(
response.success,
"success field must be true for succesful response"
);
self.expected_responses.push(response);
ResponseBuilder { adapter: self }
}
fn expect_error_response(&mut self, response: Response) -> ResponseBuilder<'_> {
assert!(
!response.success,
"success field must be false for error response"
);
self.expected_responses.push(response);
ResponseBuilder { adapter: self }
}
fn expect_event(&mut self, event_type: &str, event_body: Option<impl serde::Serialize>) {
let event_body = event_body.map(|s| serde_json::to_value(s).unwrap());
self.expected_events
.push((event_type.to_owned(), event_body));
}
fn expect_output_event(&mut self, msg: &str) {
self.expect_event(
"output",
Some(json!({
"category": "console",
"group": "probe-rs-debug",
"output": msg
})),
);
}
}
impl ProtocolAdapter for MockProtocolAdapter {
fn listen_for_request(&mut self) -> anyhow::Result<Option<Request>> {
let next_request = self
.requests
.pop_front()
.ok_or_else(|| anyhow::anyhow!("No more responses to listen for."))?;
Ok(Some(next_request))
}
fn send_event<S: serde::Serialize>(
&mut self,
event_type: &str,
event_body: Option<S>,
) -> anyhow::Result<()> {
let event_body = event_body.map(|s| serde_json::to_value(s).unwrap());
if self.event_index >= self.expected_events.len() {
panic!(
"No more events expected, but got event_type={event_type:?}, event_body={event_body:?}"
);
}
let (expected_event_type, expected_event_body) =
&self.expected_events[self.event_index];
pretty_assertions::assert_eq!(
(event_type, &event_body),
(expected_event_type.as_str(), expected_event_body)
);
self.event_index += 1;
Ok(())
}
fn set_console_log_level(
&mut self,
_log_level: crate::cmd::dap_server::server::configuration::ConsoleLog,
) {
}
fn console_log_level(&self) -> crate::cmd::dap_server::server::configuration::ConsoleLog {
self.console_log_level
}
fn send_raw_response(&mut self, response: Response) -> anyhow::Result<()> {
if self.response_index >= self.expected_responses.len() {
panic!("No more responses expected, but got {response:?}");
}
let expected_response = &self.expected_responses[self.response_index];
let response = Response {
seq: expected_response.seq,
..response.clone()
};
pretty_assertions::assert_eq!(&response, expected_response);
self.response_index += 1;
Ok(())
}
fn remove_pending_request(&mut self, request_seq: i64) -> Option<String> {
self.pending_requests.remove(&request_seq)
}
fn get_next_seq(&mut self) -> i64 {
self.sequence_number += 1;
self.sequence_number
}
}
fn initialized_protocol_adapter() -> MockProtocolAdapter {
let mut protocol_adapter = MockProtocolAdapter::new();
protocol_adapter
.add_request("initialize")
.with_arguments(default_initialize_args())
.and_succesful_response()
.with_body(expected_capabilites());
protocol_adapter.expect_output_event("probe-rs-debug: Log output for \"probe_rs=warn\" will be written to the Debug Console.\n");
protocol_adapter
.expect_output_event("probe-rs-debug: Starting probe-rs as a DAP Protocol server\n");
protocol_adapter
}
fn program_binary() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../probe-rs-debug/tests/debug-unwind-tests/nRF52833_xxAA_full_unwind.elf")
}
fn valid_session_config() -> SessionConfig {
SessionConfig {
chip: Some(TEST_CHIP_NAME.to_owned()),
core_configs: vec![CoreConfig {
core_index: 0,
program_binary: Some(program_binary()),
..CoreConfig::default()
}],
..SessionConfig::default()
}
}
fn launched_protocol_adapter() -> MockProtocolAdapter {
let mut protocol_adapter = initialized_protocol_adapter();
let launch_args = valid_session_config();
protocol_adapter
.add_request("launch")
.with_arguments(launch_args)
.and_succesful_response();
protocol_adapter.expect_event("initialized", None::<u32>);
protocol_adapter
}
fn disconnect_protocol_adapter(protocol_adapter: &mut MockProtocolAdapter) {
protocol_adapter
.add_request("disconnect")
.with_arguments(DisconnectArguments {
restart: Some(false),
suspend_debuggee: Some(false),
terminate_debuggee: Some(false),
})
.and_succesful_response();
}
fn fake_probe() -> (DebugProbeInfo, FakeProbe) {
let probe_info = DebugProbeInfo::new(
"Mock probe",
0x12,
0x23,
Some("mock_serial".to_owned()),
&MockProbeFactory,
None,
);
let fake_probe = FakeProbe::with_mocked_core_and_binary(program_binary().as_path());
fake_probe.expect_operation(Operation::ReadRawApRegister {
ap: FullyQualifiedApAddress::v1_with_default_dp(1),
address: 0xC,
result: 1,
});
(probe_info, fake_probe)
}
async fn execute_test(
protocol_adapter: MockProtocolAdapter,
with_probe: bool,
) -> Result<(), DebuggerError> {
let debug_adapter = DebugAdapter::new(protocol_adapter);
let lister = TestLister::new();
if with_probe {
lister.probes.borrow_mut().push(fake_probe());
}
let lister = Lister::with_lister(Box::new(lister));
let mut debugger = Debugger::new(UtcOffset::UTC, None)?;
debugger.debug_session(debug_adapter, &lister).await
}
#[tokio::test]
async fn test_initalize_request() {
let protocol_adapter = initialized_protocol_adapter();
execute_test(protocol_adapter, false).await.unwrap_err();
}
#[tokio::test]
async fn test_launch_no_probes() {
let mut protocol_adapter = initialized_protocol_adapter();
let expected_error = "No connected probes were found.";
protocol_adapter.expect_output_event(&format!("{expected_error}\n"));
protocol_adapter
.add_request("launch")
.with_arguments(SessionConfig::default())
.and_error_response()
.with_body(error_response_body(expected_error));
execute_test(protocol_adapter, false).await.unwrap();
}
#[tokio::test]
async fn test_launch_and_terminate() {
let mut protocol_adapter = launched_protocol_adapter();
disconnect_protocol_adapter(&mut protocol_adapter);
execute_test(protocol_adapter, true).await.unwrap();
}
#[tokio::test]
async fn launch_with_config_error() {
let mut protocol_adapter = initialized_protocol_adapter();
let invalid_launch_args = SessionConfig {
chip: Some(TEST_CHIP_NAME.to_owned()),
core_configs: vec![CoreConfig {
core_index: 0,
..CoreConfig::default()
}],
..SessionConfig::default()
};
let expected_error = "Please use the `program-binary` option to specify an executable for this target core. Other(Missing value for file.)";
protocol_adapter.expect_output_event(&format!("{expected_error}\n"));
protocol_adapter
.add_request("launch")
.with_arguments(invalid_launch_args)
.and_error_response()
.with_body(error_response_body(expected_error));
execute_test(protocol_adapter, true).await.unwrap();
}
#[tokio::test]
async fn wrong_request_after_init() {
let mut protocol_adapter = initialized_protocol_adapter();
let expected_error = "Unable to process request with command threads before an attach or launch request is received";
protocol_adapter.expect_output_event("Ignoring request with command 'threads', we can only handle 'launch' and 'attach' commands.\n");
protocol_adapter
.add_request("threads")
.and_error_response()
.with_body(error_response_body(expected_error));
protocol_adapter.expect_output_event("Unable to process request with command threads before an attach or launch request is received\n");
disconnect_protocol_adapter(&mut protocol_adapter);
execute_test(protocol_adapter, true).await.unwrap();
}
#[tokio::test]
async fn attach_request() {
let mut protocol_adapter = initialized_protocol_adapter();
let attach_args = valid_session_config();
protocol_adapter
.add_request("attach")
.with_arguments(attach_args)
.and_succesful_response();
protocol_adapter.expect_event("initialized", None::<u32>);
disconnect_protocol_adapter(&mut protocol_adapter);
execute_test(protocol_adapter, true).await.unwrap();
}
#[tokio::test]
async fn attach_with_flashing() {
let mut protocol_adapter = initialized_protocol_adapter();
let attach_args = SessionConfig {
flashing_config: FlashingConfig {
flashing_enabled: true,
halt_after_reset: true,
..Default::default()
},
..valid_session_config()
};
let expected_error = "Please do not use any of the `flashing_enabled`, `reset_after_flashing`, halt_after_reset`, `full_chip_erase`, or `restore_unwritten_bytes` options when using `attach` request type.";
protocol_adapter.expect_output_event(&format!("{expected_error}\n"));
protocol_adapter
.add_request("attach")
.with_arguments(attach_args)
.and_error_response()
.with_body(error_response_body(expected_error));
execute_test(protocol_adapter, true).await.unwrap();
}
#[tokio::test]
async fn launch_and_threads() {
let mut protocol_adapter = launched_protocol_adapter();
protocol_adapter
.add_request("configurationDone")
.and_succesful_response();
protocol_adapter
.add_request("threads")
.and_succesful_response()
.with_body(ThreadsResponseBody {
threads: vec![Thread {
id: 0,
name: format!("0-{TEST_CHIP_NAME}"),
}],
});
disconnect_protocol_adapter(&mut protocol_adapter);
execute_test(protocol_adapter, true).await.unwrap();
}
#[test_case(0; "instructions before and not including the ref address, multiple locations")]
#[test_case(1; "instructions including the ref address, location cloned from earlier line")]
#[test_case(2; "instructions after and not including the ref address")]
#[test_case(3; "negative byte offset of exactly one instruction (aligned)")]
#[test_case(4; "positive byte offset that lands in the middle of an instruction (unaligned)")]
#[tokio::test]
async fn disassemble(test_case: usize) {
#[rustfmt::skip]
mod config {
use std::collections::HashMap;
type TestInstruction = (&'static str, &'static str, &'static str);
const TEST_INSTRUCTIONS: [TestInstruction; 10] = [
("0x00000772", "b #0x7a8", "19 E0"), ("0x00000774", "ldr r0, [sp, #4]", "01 98"), ("0x00000776", "mov.w r1, #0x55555555", "4F F0 55 31"),
("0x0000077A", "and.w r1, r1, r0, lsr #1", "01 EA 50 01"),
("0x0000077E", "subs r0, r0, r1", "40 1A"),
("0x00000780", "mov.w r1, #0x33333333", "4F F0 33 31"),
("0x00000784", "and.w r1, r1, r0, lsr #2", "01 EA 90 01"),
("0x00000788", "bic r0, r0, #0xcccccccc", "20 F0 CC 30"),
("0x0000078C", "add r0, r1", "08 44"),
("0x0000078E", "add.w r0, r0, r0, lsr #4", "00 EB 10 10"),
];
type TestLocation = (i64, i64, &'static str, &'static str, &'static str);
const TEST_LOCATIONS: [TestLocation; 3] = [
(115, 5, "<unavailable>: ub_checks.rs", "/rustc/7f2fc33da6633f5a764ddc263c769b6b2873d167/library/core/src/ub_checks.rs", "deemphasize"),
(0, 5, "<unavailable>: ub_checks.rs", "/rustc/7f2fc33da6633f5a764ddc263c769b6b2873d167/library/core/src/ub_checks.rs", "deemphasize"),
(1244, 5, "<unavailable>: mod.rs", "/rustc/7f2fc33da6633f5a764ddc263c769b6b2873d167/library/core/src/num/mod.rs", "deemphasize"),
];
type TestCase = (&'static str, i64, i64, i64, &'static [TestInstruction], HashMap<&'static str, &'static TestLocation>);
pub(super) fn test_cases() -> [TestCase; 5] {[
("0x00000788", 0, -7, 6, &TEST_INSTRUCTIONS[0..6],
HashMap::from([("0x00000772", &TEST_LOCATIONS[0]), ("0x00000774", &TEST_LOCATIONS[1]), ("0x0000077A", &TEST_LOCATIONS[2])])),
("0x00000788", 0, -3, 6, &TEST_INSTRUCTIONS[4..10],
HashMap::from([("0x0000077E", &TEST_LOCATIONS[2])])),
("0x00000772", 0, 3, 6, &TEST_INSTRUCTIONS[3..9],
HashMap::from([("0x0000077A", &TEST_LOCATIONS[2])])),
("0x00000772", -4, 3, 6, &TEST_INSTRUCTIONS[2..8],
HashMap::from([("0x00000776", &TEST_LOCATIONS[1]), ("0x0000077A", &TEST_LOCATIONS[2])])),
("0x00000776", 6, 0, 6, &TEST_INSTRUCTIONS[4..10],
HashMap::from([("0x0000077E", &TEST_LOCATIONS[2])])),
]}
}
let mut protocol_adapter = launched_protocol_adapter();
protocol_adapter
.add_request("configurationDone")
.and_succesful_response();
let default_instruction_fields = DisassembledInstruction {
address: "".to_string(),
column: None,
end_column: None,
end_line: None,
instruction: "".to_string(),
instruction_bytes: None,
line: None,
location: None,
symbol: None,
presentation_hint: None,
};
let default_source_fields = Source {
adapter_data: None,
checksums: None,
name: None,
origin: None,
path: None,
presentation_hint: None,
source_reference: None,
sources: None,
};
let (mem, off, inst_off, inst_cnt, test_instrs, test_locs) =
&config::test_cases()[test_case];
protocol_adapter
.add_request("disassemble")
.with_arguments(DisassembleArguments {
memory_reference: mem.to_string(),
offset: Some(*off),
instruction_offset: Some(*inst_off),
instruction_count: *inst_cnt,
resolve_symbols: None,
})
.and_succesful_response()
.with_body(DisassembleResponseBody {
instructions: test_instrs
.iter()
.map(|(address, instruction, instruction_bytes)| {
let mut instruction = DisassembledInstruction {
address: (*address).to_owned(),
instruction: (*instruction).to_owned(),
instruction_bytes: Some((*instruction_bytes).to_owned()),
..default_instruction_fields.clone()
};
if let Some(&(line, column, name, path, hint)) = test_locs.get(address) {
instruction.line = if *line == 0 { None } else { Some(*line) };
instruction.column = Some(*column);
instruction.location = Some(Source {
name: Some(name.to_string()),
path: Some(path.to_string()),
presentation_hint: Some(hint.to_string()),
..default_source_fields.clone()
})
}
instruction
})
.collect(),
});
disconnect_protocol_adapter(&mut protocol_adapter);
execute_test(protocol_adapter, true).await.unwrap();
}
}