use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use crate::common::{config::{Config, TransportMode}, Error, Result};
use crate::dap::{
self, Breakpoint, Capabilities, DapClient, Event, FunctionBreakpoint, LaunchArguments,
AttachArguments, Scope, SourceBreakpoint, StackFrame, Thread, Variable,
};
use crate::ipc::protocol::{BreakpointInfo, BreakpointLocation};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Idle,
Initializing,
Configuring,
Running,
Stopped,
Exited,
Terminating,
}
impl std::fmt::Display for SessionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Idle => write!(f, "idle"),
Self::Initializing => write!(f, "initializing"),
Self::Configuring => write!(f, "configuring"),
Self::Running => write!(f, "running"),
Self::Stopped => write!(f, "stopped"),
Self::Exited => write!(f, "exited"),
Self::Terminating => write!(f, "terminating"),
}
}
}
#[derive(Debug, Clone)]
struct StoredBreakpoint {
id: u32,
location: BreakpointLocation,
condition: Option<String>,
hit_count: Option<u32>,
enabled: bool,
verified: bool,
actual_line: Option<u32>,
message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct OutputEvent {
pub category: String,
pub output: String,
pub timestamp: std::time::Instant,
}
pub struct DebugSession {
client: DapClient,
events_rx: mpsc::UnboundedReceiver<Event>,
state: SessionState,
capabilities: Capabilities,
program: PathBuf,
args: Vec<String>,
adapter_name: String,
launched: bool,
source_breakpoints: HashMap<PathBuf, Vec<StoredBreakpoint>>,
function_breakpoints: Vec<StoredBreakpoint>,
next_bp_id: u32,
threads: Vec<Thread>,
selected_thread: Option<i64>,
stopped_thread: Option<i64>,
stopped_reason: Option<String>,
hit_breakpoints: Vec<u32>,
current_frame_index: usize,
current_frame: Option<i64>,
cached_frames: Vec<StackFrame>,
output_buffer: VecDeque<OutputEvent>,
max_output_events: usize,
max_output_bytes: usize,
current_output_bytes: usize,
exit_code: Option<i32>,
dap_request_timeout: std::time::Duration,
}
impl DebugSession {
#[tracing::instrument(skip(config), fields(adapter = %adapter_name.as_deref().unwrap_or("default")))]
pub async fn launch(
config: &Config,
program: &Path,
args: Vec<String>,
adapter_name: Option<String>,
stop_on_entry: bool,
initial_breakpoints: Vec<String>,
) -> Result<Self> {
let adapter_name = adapter_name.unwrap_or_else(|| config.defaults.adapter.clone());
let adapter_config = config.get_adapter(&adapter_name).ok_or_else(|| {
Error::adapter_not_found(&adapter_name, &[&adapter_name])
})?;
tracing::info!(
program = %program.display(),
adapter = %adapter_name,
adapter_path = %adapter_config.path.display(),
adapter_args = ?adapter_config.args,
transport = ?adapter_config.transport,
stop_on_entry,
"Launching debug session"
);
tracing::debug!("Spawning DAP adapter process");
let mut client = match adapter_config.transport {
TransportMode::Stdio => {
DapClient::spawn(&adapter_config.path, &adapter_config.args).await?
}
TransportMode::Tcp => {
DapClient::spawn_tcp(&adapter_config.path, &adapter_config.args, &adapter_config.spawn_style).await?
}
};
let init_timeout = std::time::Duration::from_secs(config.timeouts.dap_initialize_secs);
let request_timeout = std::time::Duration::from_secs(config.timeouts.dap_request_secs);
tracing::debug!(timeout_secs = init_timeout.as_secs(), "Sending DAP initialize request");
let capabilities = client.initialize_with_timeout(&adapter_name, init_timeout).await?;
tracing::debug!(?capabilities, "DAP adapter initialized");
let cwd = std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned());
let is_python = adapter_name == "debugpy";
let is_go = adapter_name == "go"
|| adapter_name == "delve"
|| adapter_name == "dlv";
let is_js_debug = adapter_name == "js-debug";
let is_typescript_source = program.extension().map(|e| e == "ts").unwrap_or(false)
|| (program.extension().map(|e| e == "js").unwrap_or(false)
&& program.with_extension("ts").exists());
let launch_args = LaunchArguments {
program: program.to_string_lossy().into_owned(),
args: args.clone(),
cwd,
env: None,
stop_on_entry,
init_commands: None,
pre_run_commands: None,
request: if is_python { Some("launch".to_string()) } else { None },
console: if is_python { Some("internalConsole".to_string()) } else { None },
python: None, just_my_code: if is_python { Some(true) } else { None },
mode: if is_go { Some("exec".to_string()) } else { None },
stop_at_entry: if is_go && stop_on_entry { Some(true) } else { None },
stop_at_beginning_of_main_subprogram: if (adapter_name == "gdb" || adapter_name == "cuda-gdb") && stop_on_entry { Some(true) } else { None },
type_attr: if is_js_debug { Some("pwa-node".to_string()) } else { None },
source_maps: if is_js_debug && is_typescript_source { Some(true) } else { None },
out_files: None,
runtime_executable: None,
runtime_args: None,
skip_files: None,
};
tracing::debug!(
program = %program.display(),
args = ?args,
is_python,
stop_on_entry,
"Sending DAP launch request"
);
if is_python {
client.launch_no_wait(launch_args).await?;
tracing::debug!("DAP launch request sent (no-wait mode for Python)");
} else {
client.launch(launch_args).await?;
tracing::debug!("DAP launch request successful");
}
tracing::debug!(timeout_secs = request_timeout.as_secs(), "Waiting for DAP initialized event");
client.wait_initialized_with_timeout(request_timeout).await?;
tracing::debug!("Received DAP initialized event");
let has_initial_breakpoints = !initial_breakpoints.is_empty();
if has_initial_breakpoints {
tracing::debug!(count = initial_breakpoints.len(), "Setting initial breakpoints");
let mut source_bps: std::collections::HashMap<PathBuf, Vec<dap::SourceBreakpoint>> = std::collections::HashMap::new();
let mut function_bps: Vec<dap::FunctionBreakpoint> = Vec::new();
for bp_str in &initial_breakpoints {
match BreakpointLocation::parse(bp_str) {
Ok(BreakpointLocation::Line { file, line }) => {
source_bps.entry(file).or_default().push(dap::SourceBreakpoint {
line,
column: None,
condition: None,
hit_condition: None,
log_message: None,
});
}
Ok(BreakpointLocation::Function { name }) => {
function_bps.push(dap::FunctionBreakpoint {
name,
condition: None,
hit_condition: None,
});
}
Err(e) => {
tracing::warn!(breakpoint = %bp_str, error = %e, "Failed to parse initial breakpoint");
}
}
}
for (file, bps) in source_bps {
match client.set_breakpoints(&file, bps).await {
Ok(results) => {
for bp in results {
tracing::debug!(
verified = bp.verified,
line = bp.line,
"Initial source breakpoint set"
);
}
}
Err(e) => {
tracing::warn!(file = %file.display(), error = %e, "Failed to set initial breakpoints");
}
}
}
if !function_bps.is_empty() {
match client.set_function_breakpoints(function_bps).await {
Ok(results) => {
for bp in results {
tracing::debug!(
verified = bp.verified,
line = bp.line,
"Initial function breakpoint set"
);
}
}
Err(e) => {
tracing::warn!(error = %e, "Failed to set initial function breakpoints");
}
}
}
}
tracing::debug!("Sending DAP configurationDone request");
client.configuration_done().await?;
tracing::debug!("DAP configuration complete, program starting");
let events_rx = client
.take_event_receiver()
.ok_or_else(|| Error::Internal("Failed to get event receiver".to_string()))?;
let initial_state = if stop_on_entry {
SessionState::Stopped
} else {
SessionState::Running
};
Ok(Self {
client,
events_rx,
state: initial_state,
capabilities,
program: program.to_path_buf(),
args,
adapter_name,
launched: true,
source_breakpoints: HashMap::new(),
function_breakpoints: Vec::new(),
next_bp_id: 1,
threads: Vec::new(),
selected_thread: None,
stopped_thread: None,
stopped_reason: None,
hit_breakpoints: Vec::new(),
current_frame_index: 0,
current_frame: None,
cached_frames: Vec::new(),
output_buffer: VecDeque::new(),
max_output_events: config.output.max_events,
max_output_bytes: config.output.max_bytes_mb * 1024 * 1024,
current_output_bytes: 0,
exit_code: None,
dap_request_timeout: request_timeout,
})
}
pub async fn attach(
config: &Config,
pid: u32,
adapter_name: Option<String>,
) -> Result<Self> {
let adapter_name = adapter_name.unwrap_or_else(|| config.defaults.adapter.clone());
let adapter_config = config.get_adapter(&adapter_name).ok_or_else(|| {
Error::adapter_not_found(&adapter_name, &[&adapter_name])
})?;
tracing::info!(
pid,
adapter = %adapter_name,
transport = ?adapter_config.transport,
"Attaching to process"
);
let mut client = match adapter_config.transport {
TransportMode::Stdio => {
DapClient::spawn(&adapter_config.path, &adapter_config.args).await?
}
TransportMode::Tcp => {
DapClient::spawn_tcp(&adapter_config.path, &adapter_config.args, &adapter_config.spawn_style).await?
}
};
let init_timeout = std::time::Duration::from_secs(config.timeouts.dap_initialize_secs);
let request_timeout = std::time::Duration::from_secs(config.timeouts.dap_request_secs);
let capabilities = client.initialize_with_timeout(&adapter_name, init_timeout).await?;
client
.attach(AttachArguments {
pid,
wait_for: None,
})
.await?;
client.wait_initialized_with_timeout(request_timeout).await?;
client.configuration_done().await?;
let events_rx = client
.take_event_receiver()
.ok_or_else(|| Error::Internal("Failed to get event receiver".to_string()))?;
Ok(Self {
client,
events_rx,
state: SessionState::Stopped, capabilities,
program: PathBuf::from(format!("pid:{}", pid)),
args: Vec::new(),
adapter_name,
launched: false,
source_breakpoints: HashMap::new(),
function_breakpoints: Vec::new(),
next_bp_id: 1,
threads: Vec::new(),
selected_thread: None,
stopped_thread: None,
stopped_reason: Some("attach".to_string()),
hit_breakpoints: Vec::new(),
current_frame_index: 0,
current_frame: None,
cached_frames: Vec::new(),
output_buffer: VecDeque::new(),
max_output_events: config.output.max_events,
max_output_bytes: config.output.max_bytes_mb * 1024 * 1024,
current_output_bytes: 0,
exit_code: None,
dap_request_timeout: request_timeout,
})
}
pub fn state(&self) -> SessionState {
self.state
}
pub fn program(&self) -> &Path {
&self.program
}
pub fn adapter_name(&self) -> &str {
&self.adapter_name
}
pub fn stopped_thread(&self) -> Option<i64> {
self.stopped_thread
}
pub fn stopped_reason(&self) -> Option<&str> {
self.stopped_reason.as_deref()
}
pub fn exit_code(&self) -> Option<i32> {
self.exit_code
}
pub async fn process_events(&mut self) -> Result<Vec<Event>> {
let mut events = Vec::new();
while let Ok(event) = self.events_rx.try_recv() {
self.handle_event(&event);
events.push(event);
}
Ok(events)
}
fn drain_pending_events(&mut self) {
while let Ok(event) = self.events_rx.try_recv() {
self.handle_event(&event);
}
}
fn handle_event(&mut self, event: &Event) {
match event {
Event::Stopped(body) => {
self.state = SessionState::Stopped;
self.stopped_thread = body.thread_id;
self.stopped_reason = Some(body.reason.clone());
self.hit_breakpoints = body.hit_breakpoint_ids.clone();
self.current_frame = None;
self.current_frame_index = 0;
self.cached_frames.clear();
tracing::debug!("Stopped: {:?}", body);
}
Event::Continued { thread_id, .. } => {
self.state = SessionState::Running;
self.stopped_thread = None;
self.stopped_reason = None;
self.hit_breakpoints.clear();
self.current_frame = None;
self.current_frame_index = 0;
self.cached_frames.clear();
tracing::debug!("Continued: thread {}", thread_id);
}
Event::Exited(body) => {
self.state = SessionState::Exited;
self.exit_code = Some(body.exit_code);
tracing::info!("Program exited with code {}", body.exit_code);
}
Event::Terminated(_) => {
self.state = SessionState::Exited;
tracing::info!("Session terminated");
}
Event::Output(body) => {
let category = body.category.clone().unwrap_or_else(|| "console".to_string());
self.buffer_output(&category, &body.output);
}
Event::Thread(body) => {
tracing::debug!("Thread {}: {}", body.thread_id, body.reason);
if body.reason == "exited" {
self.threads.retain(|t| t.id != body.thread_id);
if self.selected_thread == Some(body.thread_id) {
self.selected_thread = None;
}
}
}
Event::Breakpoint { reason, breakpoint } => {
tracing::debug!("Breakpoint {}: {:?}", reason, breakpoint);
if let Some(bp_id) = breakpoint.id {
self.update_breakpoint_from_event(bp_id as u32, breakpoint);
}
}
_ => {}
}
}
fn update_breakpoint_from_event(&mut self, _id: u32, bp: &dap::Breakpoint) {
if let (Some(source), Some(line)) = (&bp.source, bp.line) {
if let Some(path) = &source.path {
let path = PathBuf::from(path);
if let Some(stored_bps) = self.source_breakpoints.get_mut(&path) {
for stored in stored_bps.iter_mut() {
if let BreakpointLocation::Line { line: stored_line, .. } = &stored.location {
if *stored_line == line || stored.actual_line == Some(line) {
stored.verified = bp.verified;
stored.actual_line = bp.line;
stored.message = bp.message.clone();
break;
}
}
}
}
}
}
}
fn buffer_output(&mut self, category: &str, output: &str) {
let output = if output.len() > self.max_output_bytes {
tracing::warn!(
"Output message ({} bytes) exceeds max buffer size ({} bytes), truncating",
output.len(),
self.max_output_bytes
);
let truncated: String = output.chars().take(self.max_output_bytes).collect();
truncated
} else {
output.to_string()
};
let output_bytes = output.len();
while self.current_output_bytes + output_bytes > self.max_output_bytes
&& !self.output_buffer.is_empty()
{
if let Some(removed) = self.output_buffer.pop_front() {
self.current_output_bytes = self.current_output_bytes.saturating_sub(removed.output.len());
}
}
while self.output_buffer.len() >= self.max_output_events && !self.output_buffer.is_empty() {
if let Some(removed) = self.output_buffer.pop_front() {
self.current_output_bytes = self.current_output_bytes.saturating_sub(removed.output.len());
}
}
self.output_buffer.push_back(OutputEvent {
category: category.to_string(),
output,
timestamp: std::time::Instant::now(),
});
self.current_output_bytes += output_bytes;
}
pub async fn wait_stopped(&mut self, timeout_secs: u64) -> Result<Event> {
let timeout = std::time::Duration::from_secs(timeout_secs);
let deadline = tokio::time::Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(Error::AwaitTimeout(timeout_secs));
}
match tokio::time::timeout(remaining, self.events_rx.recv()).await {
Ok(Some(event)) => {
self.handle_event(&event);
match &event {
Event::Stopped(_) | Event::Exited(_) | Event::Terminated(_) => {
return Ok(event);
}
_ => {
}
}
}
Ok(None) => {
return Err(Error::AdapterCrashed);
}
Err(_) => {
return Err(Error::AwaitTimeout(timeout_secs));
}
}
}
}
pub async fn add_breakpoint(
&mut self,
location: BreakpointLocation,
condition: Option<String>,
hit_count: Option<u32>,
) -> Result<BreakpointInfo> {
let bp_id = self.next_bp_id;
self.next_bp_id += 1;
match &location {
BreakpointLocation::Line { file, line: _ } => {
let stored = StoredBreakpoint {
id: bp_id,
location: location.clone(),
condition: condition.clone(),
hit_count,
enabled: true,
verified: false,
actual_line: None,
message: None,
};
let entry = self.source_breakpoints.entry(file.clone()).or_default();
entry.push(stored);
let source_bps = self.collect_source_breakpoints(file);
let results = self.client.set_breakpoints(file, source_bps).await?;
self.update_source_breakpoint_status(file, &results);
let info = self.get_breakpoint_info(bp_id)?;
Ok(info)
}
BreakpointLocation::Function { name: _ } => {
let stored = StoredBreakpoint {
id: bp_id,
location: location.clone(),
condition: condition.clone(),
hit_count,
enabled: true,
verified: false,
actual_line: None,
message: None,
};
self.function_breakpoints.push(stored);
let func_bps = self.collect_function_breakpoints();
let results = self.client.set_function_breakpoints(func_bps).await?;
self.update_function_breakpoint_status(&results);
let info = self.get_breakpoint_info(bp_id)?;
Ok(info)
}
}
}
fn collect_source_breakpoints(&self, file: &Path) -> Vec<SourceBreakpoint> {
self.source_breakpoints
.get(file)
.map(|bps| {
bps.iter()
.filter(|bp| bp.enabled)
.map(|bp| {
let line = match &bp.location {
BreakpointLocation::Line { line, .. } => *line,
_ => 0,
};
SourceBreakpoint {
line,
column: None,
condition: bp.condition.clone(),
hit_condition: bp.hit_count.map(|n| n.to_string()),
log_message: None,
}
})
.collect()
})
.unwrap_or_default()
}
fn collect_function_breakpoints(&self) -> Vec<FunctionBreakpoint> {
self.function_breakpoints
.iter()
.filter(|bp| bp.enabled)
.map(|bp| {
let name = match &bp.location {
BreakpointLocation::Function { name } => name.clone(),
_ => String::new(),
};
FunctionBreakpoint {
name,
condition: bp.condition.clone(),
hit_condition: bp.hit_count.map(|n| n.to_string()),
}
})
.collect()
}
fn update_source_breakpoint_status(&mut self, file: &Path, results: &[Breakpoint]) {
if let Some(stored) = self.source_breakpoints.get_mut(file) {
for (stored_bp, result) in stored.iter_mut().zip(results.iter()) {
stored_bp.verified = result.verified;
stored_bp.actual_line = result.line;
stored_bp.message = result.message.clone();
}
}
}
fn update_function_breakpoint_status(&mut self, results: &[Breakpoint]) {
for (stored_bp, result) in self.function_breakpoints.iter_mut().zip(results.iter()) {
stored_bp.verified = result.verified;
stored_bp.actual_line = result.line;
stored_bp.message = result.message.clone();
}
}
fn get_breakpoint_info(&self, id: u32) -> Result<BreakpointInfo> {
for (file, bps) in &self.source_breakpoints {
if let Some(bp) = bps.iter().find(|bp| bp.id == id) {
return Ok(BreakpointInfo {
id: bp.id,
verified: bp.verified,
source: Some(file.to_string_lossy().into_owned()),
line: bp.actual_line.or(match &bp.location {
BreakpointLocation::Line { line, .. } => Some(*line),
_ => None,
}),
message: bp.message.clone(),
enabled: bp.enabled,
condition: bp.condition.clone(),
hit_count: bp.hit_count,
});
}
}
if let Some(bp) = self.function_breakpoints.iter().find(|bp| bp.id == id) {
return Ok(BreakpointInfo {
id: bp.id,
verified: bp.verified,
source: match &bp.location {
BreakpointLocation::Function { name } => Some(name.clone()),
_ => None,
},
line: bp.actual_line,
message: bp.message.clone(),
enabled: bp.enabled,
condition: bp.condition.clone(),
hit_count: bp.hit_count,
});
}
Err(Error::BreakpointNotFound { id })
}
pub async fn remove_breakpoint(&mut self, id: u32) -> Result<()> {
let mut file_to_update = None;
for (file, bps) in &mut self.source_breakpoints {
if let Some(pos) = bps.iter().position(|bp| bp.id == id) {
bps.remove(pos);
file_to_update = Some(file.clone());
break;
}
}
if let Some(file) = file_to_update {
let source_bps = self.collect_source_breakpoints(&file);
self.client.set_breakpoints(&file, source_bps).await?;
return Ok(());
}
if let Some(pos) = self.function_breakpoints.iter().position(|bp| bp.id == id) {
self.function_breakpoints.remove(pos);
let func_bps = self.collect_function_breakpoints();
self.client.set_function_breakpoints(func_bps).await?;
return Ok(());
}
Err(Error::BreakpointNotFound { id })
}
pub async fn remove_all_breakpoints(&mut self) -> Result<()> {
let files: Vec<_> = self.source_breakpoints.keys().cloned().collect();
for file in files {
self.client.set_breakpoints(&file, vec![]).await?;
}
self.source_breakpoints.clear();
self.client.set_function_breakpoints(vec![]).await?;
self.function_breakpoints.clear();
Ok(())
}
pub fn list_breakpoints(&self) -> Vec<BreakpointInfo> {
let mut result = Vec::new();
for (file, bps) in &self.source_breakpoints {
for bp in bps {
result.push(BreakpointInfo {
id: bp.id,
verified: bp.verified,
source: Some(file.to_string_lossy().into_owned()),
line: bp.actual_line.or(match &bp.location {
BreakpointLocation::Line { line, .. } => Some(*line),
_ => None,
}),
message: bp.message.clone(),
enabled: bp.enabled,
condition: bp.condition.clone(),
hit_count: bp.hit_count,
});
}
}
for bp in &self.function_breakpoints {
result.push(BreakpointInfo {
id: bp.id,
verified: bp.verified,
source: match &bp.location {
BreakpointLocation::Function { name } => Some(name.clone()),
_ => None,
},
line: bp.actual_line,
message: bp.message.clone(),
enabled: bp.enabled,
condition: bp.condition.clone(),
hit_count: bp.hit_count,
});
}
result
}
pub async fn continue_execution(&mut self) -> Result<()> {
self.ensure_stopped()?;
self.drain_pending_events();
let thread_id = self.get_thread_id().await?;
self.client.continue_execution(thread_id).await?;
self.state = SessionState::Running;
self.stopped_thread = None;
self.stopped_reason = None;
Ok(())
}
pub async fn next(&mut self) -> Result<()> {
self.ensure_stopped()?;
self.drain_pending_events();
let thread_id = self.get_thread_id().await?;
self.client.next(thread_id).await?;
self.state = SessionState::Running;
Ok(())
}
pub async fn step_in(&mut self) -> Result<()> {
self.ensure_stopped()?;
self.drain_pending_events();
let thread_id = self.get_thread_id().await?;
self.client.step_in(thread_id).await?;
self.state = SessionState::Running;
Ok(())
}
pub async fn step_out(&mut self) -> Result<()> {
self.ensure_stopped()?;
self.drain_pending_events();
let thread_id = self.get_thread_id().await?;
self.client.step_out(thread_id).await?;
self.state = SessionState::Running;
Ok(())
}
pub async fn pause(&mut self) -> Result<()> {
if self.state != SessionState::Running {
return Err(Error::invalid_state("pause", &self.state.to_string()));
}
let thread_id = self.get_thread_id().await?;
self.client.pause(thread_id).await?;
Ok(())
}
pub async fn stack_trace(&mut self, limit: usize) -> Result<Vec<StackFrame>> {
self.ensure_stopped()?;
let thread_id = self.get_thread_id().await?;
let frames = self.client.stack_trace(thread_id, limit as i64).await?;
if let Some(frame) = frames.first() {
self.current_frame = Some(frame.id);
}
Ok(frames)
}
pub async fn get_threads(&mut self) -> Result<Vec<Thread>> {
self.threads = self.client.threads().await?;
Ok(self.threads.clone())
}
pub async fn get_scopes(&mut self, frame_id: Option<i64>) -> Result<Vec<Scope>> {
self.ensure_stopped()?;
let frame_id = match frame_id.or(self.current_frame) {
Some(id) => id,
None => {
let thread_id = self.get_thread_id().await?;
let frames = self.client.stack_trace(thread_id, 1).await?;
let frame = frames.first().ok_or_else(|| {
Error::Internal("No stack frames available".to_string())
})?;
self.current_frame = Some(frame.id);
frame.id
}
};
self.client.scopes(frame_id).await
}
pub async fn get_variables(&mut self, reference: i64) -> Result<Vec<Variable>> {
self.ensure_stopped()?;
self.client.variables(reference).await
}
pub async fn get_locals(&mut self, frame_id: Option<i64>) -> Result<Vec<Variable>> {
let scopes = self.get_scopes(frame_id).await?;
let locals_scope = scopes.iter().find(|s| s.name == "Locals" || s.name == "Local");
if let Some(scope) = locals_scope {
self.get_variables(scope.variables_reference).await
} else if let Some(scope) = scopes.first() {
self.get_variables(scope.variables_reference).await
} else {
Ok(Vec::new())
}
}
pub async fn evaluate(
&mut self,
expression: &str,
frame_id: Option<i64>,
context: &str,
) -> Result<dap::EvaluateResponseBody> {
self.ensure_stopped()?;
let frame_id = match frame_id.or(self.current_frame) {
Some(id) => Some(id),
None => {
let thread_id = self.get_thread_id().await?;
let frames = self.client.stack_trace(thread_id, 1).await?;
if let Some(frame) = frames.first() {
self.current_frame = Some(frame.id);
Some(frame.id)
} else {
None
}
}
};
self.client.evaluate(expression, frame_id, context).await
}
pub fn get_output(&mut self, tail: Option<usize>, clear: bool) -> Vec<OutputEvent> {
let result: Vec<OutputEvent> = if let Some(n) = tail {
self.output_buffer.iter().rev().take(n).cloned().rev().collect()
} else {
self.output_buffer.iter().cloned().collect()
};
if clear {
self.output_buffer.clear();
}
result
}
pub async fn detach(&mut self) -> Result<()> {
self.state = SessionState::Terminating;
self.client.disconnect(false).await?;
Ok(())
}
pub async fn stop(&mut self) -> Result<()> {
self.state = SessionState::Terminating;
self.client.disconnect(self.launched).await?;
self.client.terminate().await?;
Ok(())
}
pub async fn restart(&mut self) -> Result<()> {
self.client.restart(false).await?;
self.state = SessionState::Running;
self.stopped_thread = None;
self.stopped_reason = None;
self.current_frame = None;
self.current_frame_index = 0;
self.cached_frames.clear();
Ok(())
}
pub fn select_thread(&mut self, thread_id: i64) -> Result<()> {
if !self.threads.iter().any(|t| t.id == thread_id) {
return Err(Error::Internal(format!(
"Thread {} not found. Use 'threads' command to see available threads.",
thread_id
)));
}
self.selected_thread = Some(thread_id);
self.current_frame_index = 0;
self.current_frame = None;
self.cached_frames.clear();
Ok(())
}
pub fn get_selected_thread(&self) -> Option<i64> {
self.selected_thread.or(self.stopped_thread)
}
pub async fn select_frame(&mut self, frame_index: usize) -> Result<StackFrame> {
self.ensure_stopped()?;
if self.cached_frames.is_empty() || frame_index >= self.cached_frames.len() {
let thread_id = self.get_thread_id().await?;
let needed = (frame_index + 1).max(20);
self.cached_frames = self.client.stack_trace(thread_id, needed as i64).await?;
}
if frame_index >= self.cached_frames.len() {
return Err(Error::FrameNotFound(frame_index));
}
self.current_frame_index = frame_index;
self.current_frame = Some(self.cached_frames[frame_index].id);
Ok(self.cached_frames[frame_index].clone())
}
pub async fn frame_up(&mut self) -> Result<StackFrame> {
let new_index = self.current_frame_index + 1;
self.select_frame(new_index).await
}
pub async fn frame_down(&mut self) -> Result<StackFrame> {
if self.current_frame_index == 0 {
return Err(Error::invalid_state("frame down", "already at innermost frame"));
}
let new_index = self.current_frame_index - 1;
self.select_frame(new_index).await
}
pub fn get_current_frame_index(&self) -> usize {
self.current_frame_index
}
pub async fn enable_breakpoint(&mut self, id: u32) -> Result<()> {
self.set_breakpoint_enabled(id, true).await
}
pub async fn disable_breakpoint(&mut self, id: u32) -> Result<()> {
self.set_breakpoint_enabled(id, false).await
}
async fn set_breakpoint_enabled(&mut self, id: u32, enabled: bool) -> Result<()> {
let mut file_to_update = None;
let mut is_function_bp = false;
for (file, bps) in &mut self.source_breakpoints {
if let Some(bp) = bps.iter_mut().find(|bp| bp.id == id) {
bp.enabled = enabled;
file_to_update = Some(file.clone());
break;
}
}
if file_to_update.is_none() {
if let Some(bp) = self.function_breakpoints.iter_mut().find(|bp| bp.id == id) {
bp.enabled = enabled;
is_function_bp = true;
} else {
return Err(Error::BreakpointNotFound { id });
}
}
if let Some(file) = file_to_update {
let source_bps = self.collect_source_breakpoints(&file);
let results = self.client.set_breakpoints(&file, source_bps).await?;
self.update_source_breakpoint_status(&file, &results);
} else if is_function_bp {
let func_bps = self.collect_function_breakpoints();
let results = self.client.set_function_breakpoints(func_bps).await?;
self.update_function_breakpoint_status(&results);
}
Ok(())
}
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
pub fn supports_function_breakpoints(&self) -> bool {
self.capabilities.supports_function_breakpoints
}
pub fn supports_conditional_breakpoints(&self) -> bool {
self.capabilities.supports_conditional_breakpoints
}
pub fn supports_hit_conditional_breakpoints(&self) -> bool {
self.capabilities.supports_hit_conditional_breakpoints
}
fn ensure_stopped(&self) -> Result<()> {
match self.state {
SessionState::Stopped => Ok(()),
SessionState::Exited => {
Err(Error::ProgramExited(self.exit_code.unwrap_or(0)))
}
_ => Err(Error::invalid_state("inspect", &self.state.to_string())),
}
}
async fn get_thread_id(&mut self) -> Result<i64> {
if let Some(id) = self.selected_thread {
return Ok(id);
}
if let Some(id) = self.stopped_thread {
return Ok(id);
}
if self.threads.is_empty() {
self.threads = self.client.threads().await?;
}
self.threads
.first()
.map(|t| t.id)
.ok_or_else(|| Error::Internal("No threads available".to_string()))
}
}