1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
use crate::util::common_options::ProbeOptions;
use crate::util::rtt;
use crate::{FormatOptions, cmd::dap_server::DebuggerError};
use anyhow::{Result, anyhow};
use probe_rs::probe::{DebugProbeSelector, WireProtocol};
use serde::{Deserialize, Serialize};
use std::{env::current_dir, path::PathBuf};
use super::startup::TargetSessionType;
use super::uploaded_files::UploadedFiles;
/// Shared options for all session level configuration.
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct SessionConfig {
/// Level of information to be logged to the debugger console (Error, Info or Debug)
#[serde(default = "default_console_log")]
pub(crate) console_log_level: Option<ConsoleLog>,
/// Path to the requested working directory for the debugger
pub(crate) cwd: Option<PathBuf>,
/// The debug probe selector associated with the debug probe to use. Use 'list' command to see available probes
pub(crate) probe: Option<DebugProbeSelector>,
/// The target to be selected.
pub(crate) chip: Option<String>,
/// Path to a custom target description yaml.
pub(crate) chip_description_path: Option<PathBuf>,
/// Base64-encoded contents of `chip_description_path`, supplied by the DAP client when
/// `remote_server_mode` is enabled. If present, the bytes are materialized to a
/// session-scoped temporary file and `chip_description_path` is rewritten to point at that
/// temp file before [`SessionConfig::validate_config_files`] runs.
///
/// FUTURE: For very large payloads, this in-band base64 encoding may be replaced by a chunked
/// custom DAP request that streams bytes prior to the launch response.
pub(crate) chip_description_data: Option<String>,
/// Indicates that this DAP server is running on a different machine from the VSCode client.
///
/// When `true`:
/// - The server expects file content (program binary, SVD file, chip description) to be sent
/// inline as base64 alongside the corresponding path fields, rather than being read from
/// the server's local filesystem. The bytes are materialized to a session-scoped temporary
/// directory and the original path fields are rewritten to point at the materialized files.
/// - The server emits source paths from DWARF debug information verbatim, without attempting
/// to verify their existence on the server. Source resolution becomes the responsibility of
/// the VSCode client, which has access to the user's source tree.
/// - The `cwd` field is treated as a display-only string from the client's filesystem; no
/// `is_dir()` check is performed and no fallback to the server's current working directory
/// is applied. Relative-path resolution against `cwd` does not apply because all
/// client-supplied file paths arrive either pre-resolved (already absolute) or are
/// materialized to absolute temp paths.
///
/// The probe-rs VSCode extension sets this flag based on the `launch.json` field of the same
/// name. Defaults to `false` for backward compatibility with existing local stdio/loopback
/// launches.
#[serde(default)]
pub(crate) remote_server_mode: bool,
/// Assert target's reset during connect
#[serde(default)]
pub(crate) connect_under_reset: bool,
/// Protocol speed in kHz
pub(crate) speed: Option<u32>,
/// Protocol to use for target connection
pub(crate) wire_protocol: Option<WireProtocol>,
///Allow the session to erase all memory of the chip or reset it to factory default.
#[serde(default)]
pub(crate) allow_erase_all: bool,
/// Flashing configuration
#[serde(default)]
pub(crate) flashing_config: FlashingConfig,
/// Every core on the target has certain configuration.
///
/// NOTE: Although we allow specifying multiple core configurations, this is a work in progress, and probe-rs-debugger currently only supports debugging a single core.
pub(crate) core_configs: Vec<CoreConfig>,
}
impl SessionConfig {
/// Since VSCode doesn't do field validation checks for relationships in launch.json request types, check it here.
pub(crate) fn validate_configuration_option_compatibility(
&self,
requested_target_session_type: TargetSessionType,
) -> Result<(), DebuggerError> {
// Disallow flashing if the `attach` request type is used.
if requested_target_session_type == TargetSessionType::AttachRequest
&& (self.flashing_config.flashing_enabled
|| self.flashing_config.halt_after_reset
|| self.flashing_config.full_chip_erase
|| self.flashing_config.restore_unwritten_bytes)
{
let message = "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.";
return Err(DebuggerError::Other(anyhow!(message)));
}
Ok(())
}
/// In `remote_server_mode`, decode any client-supplied file payloads and rewrite the
/// corresponding path fields to point at the materialized temporary files.
///
/// In local mode (the default), this is a no-op: paths are expected to refer to files on the
/// server's own filesystem.
///
/// This must be called *before* [`SessionConfig::validate_config_files`], because the
/// validation step's `is_file()` checks are subsequently performed against the (now-materialized)
/// temporary paths.
pub(crate) fn materialize_uploaded_files(
&mut self,
uploaded_files: &mut UploadedFiles,
) -> Result<(), DebuggerError> {
if !self.remote_server_mode {
return Ok(());
}
if let Some(data) = self.chip_description_data.take() {
let hint = self
.chip_description_path
.clone()
.unwrap_or_else(|| PathBuf::from("chip-description.yaml"));
self.chip_description_path =
Some(uploaded_files.materialize("chip-description", &hint, &data)?);
}
for core_config in &mut self.core_configs {
let core_index = core_config.core_index;
if let Some(data) = core_config.program_binary_data.take() {
let hint = core_config
.program_binary
.clone()
.unwrap_or_else(|| PathBuf::from(format!("core-{core_index}.elf")));
let role = format!("core-{core_index}-program-binary");
core_config.program_binary = Some(uploaded_files.materialize(&role, &hint, &data)?);
}
if let Some(data) = core_config.svd_file_data.take() {
let hint = core_config
.svd_file
.clone()
.unwrap_or_else(|| PathBuf::from(format!("core-{core_index}.svd")));
let role = format!("core-{core_index}-svd");
core_config.svd_file = Some(uploaded_files.materialize(&role, &hint, &data)?);
}
}
Ok(())
}
/// Ensure all file names are correctly specified and that the files they point to are accessible.
pub(crate) fn validate_config_files(&mut self) -> Result<(), DebuggerError> {
// Update the `cwd`.
self.cwd = self.resolve_cwd()?;
for target_core_config in &mut self.core_configs {
// Update the `program_binary` and validate that the file exists.
target_core_config.program_binary = match get_absolute_path(
self.cwd.as_ref(),
target_core_config.program_binary.as_ref(),
) {
Ok(Some(program_binary)) => {
if !program_binary.is_file() {
return Err(DebuggerError::Other(anyhow!(
"Invalid program binary file specified '{}'",
program_binary.display()
)));
}
Some(program_binary)
}
Ok(None) => None,
Err(error) => {
return Err(DebuggerError::Other(
anyhow!("Could not load program binary.").context(error),
));
}
};
// Update the `svd_file` and validate that the file exists, or else warn the user and continue.
target_core_config.svd_file =
match get_absolute_path(self.cwd.as_ref(), target_core_config.svd_file.as_ref()) {
Ok(Some(svd_file)) => {
if !svd_file.is_file() {
tracing::warn!("SVD file {} not found.", svd_file.display());
None
} else {
Some(svd_file)
}
}
Ok(None) => None,
Err(error) => {
return Err(DebuggerError::Other(
anyhow!("Could not load SVD file.").context(error),
));
}
};
}
self.chip_description_path =
match get_absolute_path(self.cwd.as_ref(), self.chip_description_path.as_ref()) {
Ok(Some(description)) => {
if !description.is_file() {
return Err(DebuggerError::Other(anyhow!(
"Invalid chip description file specified '{}'",
description.display()
)));
}
Some(description)
}
Ok(None) => None,
Err(error) => {
return Err(DebuggerError::Other(
anyhow!("Could not load chip description file.").context(error),
));
}
};
Ok(())
}
/// Validate the new given cwd for this process exists, or else update the cwd setting to use the running process' current working directory.
///
/// In `remote_server_mode`, the `cwd` is a path on the client's filesystem and is treated as a
/// display-only string. We do not perform `is_dir()` validation, and we do not fall back to
/// the server's own current working directory. Relative-path resolution against `cwd` does
/// not apply in remote mode because all client-supplied file paths arrive either pre-resolved
/// (already absolute) or are materialized to absolute temp paths by
/// [`SessionConfig::materialize_uploaded_files`].
pub(crate) fn resolve_cwd(&self) -> Result<Option<PathBuf>, DebuggerError> {
if self.remote_server_mode {
return Ok(self.cwd.clone());
}
let path = match self.cwd {
Some(ref temp_path) if temp_path.is_dir() => Some(temp_path.to_path_buf()),
_ => {
if let Ok(current_dir) = current_dir() {
Some(current_dir)
} else {
tracing::error!(
"Cannot use current working directory. Please check existence and permissions."
);
None
}
}
};
Ok(path)
}
pub(crate) fn probe_options(&self) -> ProbeOptions {
ProbeOptions {
chip: self.chip.clone(),
chip_description_path: self.chip_description_path.clone(),
protocol: self.wire_protocol,
non_interactive: true,
probe: self.probe.clone(),
speed: self.speed,
connect_under_reset: self.connect_under_reset,
cycle_power: false,
dry_run: false,
allow_erase_all: self.allow_erase_all,
}
}
}
/// If the path to the program to be debugged is relative, we join if with the cwd.
fn get_absolute_path(
configured_cwd: Option<&PathBuf>,
os_file_to_validate: Option<&PathBuf>,
) -> Result<Option<PathBuf>, DebuggerError> {
match os_file_to_validate {
Some(temp_path) => {
let mut new_path = PathBuf::new();
if temp_path.is_relative() {
if let Some(cwd_path) = configured_cwd {
new_path.push(cwd_path);
} else {
return Err(DebuggerError::Other(anyhow!(
"Invalid value {configured_cwd:?} for `cwd`"
)));
}
}
new_path.push(temp_path);
Ok(Some(new_path))
}
None => Ok(None),
}
}
/// Configuration options to control flashing.
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct FlashingConfig {
/// Flash the target before debugging
#[serde(default)]
pub(crate) flashing_enabled: bool,
/// Halt the target after reset
#[serde(default)]
pub(crate) halt_after_reset: bool,
/// Verify chip contents before erasing, to prevent unnecessary reprogramming
#[serde(default)]
pub(crate) verify_before_flashing: bool,
/// Do a full chip erase, versus page-by-page erase
#[serde(default)]
pub(crate) full_chip_erase: bool,
/// Restore erased bytes that will not be rewritten from ELF
#[serde(default)]
pub(crate) restore_unwritten_bytes: bool,
/// Verify chip contents after flashing
#[serde(default)]
pub(crate) verify_after_flashing: bool,
/// [`FormatOptions`] to control the flashing operation, depending on the type of binary ( [`probe_rs::flashing::Format`] ) to be flashed.
#[serde(default)]
pub(crate) format_options: FormatOptions,
}
/// Configuration options for all core level configuration.
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct CoreConfig {
/// The MCU Core to debug. Default is 0
#[serde(default)]
pub(crate) core_index: usize,
/// Binary to debug as a path. Relative to `cwd`, or fully qualified.
pub(crate) program_binary: Option<PathBuf>,
/// Base64-encoded contents of `program_binary`, supplied by the DAP client when
/// `remote_server_mode` is enabled. See [`SessionConfig::chip_description_data`] for details.
pub(crate) program_binary_data: Option<String>,
/// CMSIS-SVD file for the target. Relative to `cwd`, or fully qualified.
pub(crate) svd_file: Option<PathBuf>,
/// Base64-encoded contents of `svd_file`, supplied by the DAP client when
/// `remote_server_mode` is enabled. See [`SessionConfig::chip_description_data`] for details.
pub(crate) svd_file_data: Option<String>,
#[serde(flatten)]
pub(crate) rtt_config: rtt::RttConfig,
/// Enable reset vector catch if its supported on the target.
#[serde(default = "default_true")]
pub(crate) catch_reset: bool,
/// Enable hardfault vector catch if its supported on the target.
#[serde(default = "default_true")]
pub(crate) catch_hardfault: bool,
/// Enable SVC vector catch (ARMv7-A/R only).
#[serde(default = "default_true")]
pub(crate) catch_svc: bool,
/// Enable HLT vector catch (ARMv7-A/R only).
#[serde(default = "default_true")]
pub(crate) catch_hlt: bool,
}
fn default_console_log() -> Option<ConsoleLog> {
Some(ConsoleLog::Console)
}
fn default_true() -> bool {
true
}
/// The level of information to be logged to the debugger console.
#[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum ConsoleLog {
Console,
Info,
Debug,
}
impl std::str::FromStr for ConsoleLog {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match &s.to_ascii_lowercase()[..] {
"console" => Ok(ConsoleLog::Console),
"info" => Ok(ConsoleLog::Info),
"debug" => Ok(ConsoleLog::Debug),
_ => Err(format!(
"'{s}' is not a valid console log level. Choose from [console, info, or debug]."
)),
}
}
}