Public release release-09ca780bd67a
Source commit: 09ca780bd67a00467e78139cf38466b8201b66ca Public tree identity: sha256:2f744c260b5fc2cf074ad9129f97f87f03714902e5b9fe174128afdc342ec2d0
This commit is contained in:
commit
eb1077f380
221 changed files with 81144 additions and 0 deletions
375
crates/clusterflux-dap/src/breakpoints.rs
Normal file
375
crates/clusterflux-dap/src/breakpoints.rs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
use std::fs;
|
||||
|
||||
use clusterflux_core::{
|
||||
discover_source_debug_probes, BundleDebugProbe, DebugRuntimeState, TaskInstanceId,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::demo_backend::{LINUX_THREAD, MAIN_THREAD, PACKAGE_THREAD, WINDOWS_THREAD};
|
||||
use crate::virtual_model::{AdapterState, VirtualThread};
|
||||
|
||||
pub(crate) fn request_thread<'a>(request: &Value, state: &'a AdapterState) -> &'a VirtualThread {
|
||||
let thread_id = request_thread_id(request).unwrap_or(MAIN_THREAD);
|
||||
state
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.unwrap_or_else(|| &state.threads[&MAIN_THREAD])
|
||||
}
|
||||
|
||||
pub(crate) fn request_thread_id(request: &Value) -> Option<i64> {
|
||||
request
|
||||
.get("arguments")
|
||||
.and_then(|value| value.get("threadId"))
|
||||
.and_then(Value::as_i64)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ResolvedBreakpoint {
|
||||
pub(crate) id: usize,
|
||||
pub(crate) line: i64,
|
||||
pub(crate) verified: bool,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl ResolvedBreakpoint {
|
||||
pub(crate) fn to_dap(&self) -> Value {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"verified": self.verified,
|
||||
"line": self.line,
|
||||
"message": self.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_bundle_debug_probes(project: &str, source_path: &str) -> Vec<BundleDebugProbe> {
|
||||
let source = fs::read_to_string(crate::source::resolve_source_path(project, source_path));
|
||||
source
|
||||
.map(|source| discover_source_debug_probes(source_path, &source))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_breakpoints(
|
||||
state: &mut AdapterState,
|
||||
requested_lines: Vec<i64>,
|
||||
) -> Vec<ResolvedBreakpoint> {
|
||||
let resolved = requested_lines
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, line)| {
|
||||
let probe = debug_probe_for_line(state, line);
|
||||
let verified = probe.is_some()
|
||||
|| (state.debug_probes.is_empty()
|
||||
&& source_function_name_at_line(state, line).is_some());
|
||||
let message = match probe {
|
||||
Some(probe) => format!(
|
||||
"Mapped to Clusterflux debug probe {} for task {}",
|
||||
probe.id, probe.task
|
||||
),
|
||||
None if verified => "Mapped to Clusterflux virtual source location".to_owned(),
|
||||
None => "No Clusterflux debug probe metadata covers this source line".to_owned(),
|
||||
};
|
||||
ResolvedBreakpoint {
|
||||
id: index + 1,
|
||||
line,
|
||||
verified,
|
||||
message,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
state.breakpoints = resolved
|
||||
.iter()
|
||||
.filter(|breakpoint| breakpoint.verified)
|
||||
.map(|breakpoint| breakpoint.line)
|
||||
.collect();
|
||||
resolved
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_breakpoints_for_source(
|
||||
state: &mut AdapterState,
|
||||
requested_source_path: Option<&str>,
|
||||
requested_lines: Vec<i64>,
|
||||
) -> Vec<ResolvedBreakpoint> {
|
||||
if requested_source_path.is_none_or(|requested_source_path| {
|
||||
crate::source::source_paths_match(&state.project, &state.source_path, requested_source_path)
|
||||
}) {
|
||||
return resolve_breakpoints(state, requested_lines);
|
||||
}
|
||||
|
||||
requested_lines
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, line)| ResolvedBreakpoint {
|
||||
id: index + 1,
|
||||
line,
|
||||
verified: false,
|
||||
message: format!(
|
||||
"This debug session is configured for `{}`; breakpoints from another source file are not applied",
|
||||
state.source_path
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn restart_requires_whole_process(request: &Value) -> bool {
|
||||
let Some(arguments) = request.get("arguments") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
arguments
|
||||
.get("requiresWholeProcessRestart")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
|| [
|
||||
"compatibility",
|
||||
"sourceCompatibility",
|
||||
"sourceEditCompatibility",
|
||||
"taskCompatibility",
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|field| arguments.get(field).and_then(Value::as_str))
|
||||
.any(is_incompatible_restart)
|
||||
|| arguments
|
||||
.get("sourceEdit")
|
||||
.and_then(|value| value.get("compatibility"))
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(is_incompatible_restart)
|
||||
}
|
||||
|
||||
fn is_incompatible_restart(value: &str) -> bool {
|
||||
value.eq_ignore_ascii_case("incompatible")
|
||||
|| value.eq_ignore_ascii_case("whole-process")
|
||||
|| value.eq_ignore_ascii_case("whole_process")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct FreezeFailure {
|
||||
pub(crate) thread_id: i64,
|
||||
pub(crate) task: TaskInstanceId,
|
||||
}
|
||||
|
||||
impl FreezeFailure {
|
||||
pub(crate) fn message(&self) -> String {
|
||||
format!(
|
||||
"debug all-stop failed: participant `{}` on thread {} could not freeze",
|
||||
self.task, self.thread_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn simulated_freeze_failure_thread(request: &Value) -> Option<i64> {
|
||||
request
|
||||
.get("arguments")
|
||||
.and_then(|arguments| arguments.get("simulateFreezeFailure"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
.then_some(PACKAGE_THREAD)
|
||||
}
|
||||
|
||||
pub(crate) fn freeze_all(
|
||||
state: &mut AdapterState,
|
||||
stopped_thread: i64,
|
||||
forced_failure_thread: Option<i64>,
|
||||
) -> Result<(), FreezeFailure> {
|
||||
let line = state
|
||||
.breakpoints
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|line| thread_for_source_line(state, *line) == stopped_thread)
|
||||
.or_else(|| state.breakpoints.first().copied())
|
||||
.unwrap_or_else(|| state.threads[&stopped_thread].line);
|
||||
freeze_all_at_line(state, stopped_thread, line, forced_failure_thread)
|
||||
}
|
||||
|
||||
pub(crate) fn freeze_all_at_line(
|
||||
state: &mut AdapterState,
|
||||
stopped_thread: i64,
|
||||
line: i64,
|
||||
forced_failure_thread: Option<i64>,
|
||||
) -> Result<(), FreezeFailure> {
|
||||
if let Some(failure) = state.threads.values().find(|thread| {
|
||||
thread.state == DebugRuntimeState::Running
|
||||
&& (!thread.freeze_supported || forced_failure_thread == Some(thread.id))
|
||||
}) {
|
||||
return Err(FreezeFailure {
|
||||
thread_id: failure.id,
|
||||
task: failure.task.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
state.epoch += 1;
|
||||
for thread in state.threads.values_mut() {
|
||||
if thread.state == DebugRuntimeState::Running {
|
||||
thread.state = DebugRuntimeState::Frozen;
|
||||
}
|
||||
}
|
||||
if let Some(thread) = state.threads.get_mut(&stopped_thread) {
|
||||
thread.line = line;
|
||||
thread
|
||||
.recent_output
|
||||
.push(format!("debug epoch {} all-stop", state.epoch));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn stopped_thread_for_breakpoint(state: &AdapterState) -> i64 {
|
||||
if let Some(stopped_task) = state.stopped_task.as_ref() {
|
||||
if let Some(thread) = state
|
||||
.threads
|
||||
.values()
|
||||
.find(|thread| &thread.task == stopped_task)
|
||||
{
|
||||
return thread.id;
|
||||
}
|
||||
}
|
||||
state
|
||||
.breakpoints
|
||||
.first()
|
||||
.map(|line| thread_for_source_line(state, *line))
|
||||
.unwrap_or(MAIN_THREAD)
|
||||
}
|
||||
|
||||
pub(crate) fn position_confirmed_breakpoint_stop(state: &mut AdapterState, stopped_thread: i64) {
|
||||
let line = state
|
||||
.stopped_probe_symbol
|
||||
.as_deref()
|
||||
.and_then(|symbol| symbol.strip_prefix("clusterflux.probe."))
|
||||
.and_then(|function| {
|
||||
state
|
||||
.debug_probes
|
||||
.iter()
|
||||
.find(|probe| probe.function == function)
|
||||
.and_then(|probe| {
|
||||
state.breakpoints.iter().copied().find(|line| {
|
||||
*line >= i64::from(probe.line_start) && *line <= i64::from(probe.line_end)
|
||||
})
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
state
|
||||
.breakpoints
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|line| thread_for_source_line(state, *line) == stopped_thread)
|
||||
})
|
||||
.or_else(|| state.breakpoints.first().copied());
|
||||
if let (Some(line), Some(thread)) = (line, state.threads.get_mut(&stopped_thread)) {
|
||||
thread.line = line;
|
||||
thread.recent_output.push(format!(
|
||||
"debug epoch {} confirmed frozen by all participants",
|
||||
state.epoch
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next_breakpoint_after(state: &AdapterState, thread_id: i64) -> Option<(i64, i64)> {
|
||||
let current_line = state.threads.get(&thread_id)?.line;
|
||||
state
|
||||
.breakpoints
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|line| *line > current_line)
|
||||
.min()
|
||||
.map(|line| (thread_for_source_line(state, line), line))
|
||||
}
|
||||
|
||||
fn thread_for_source_line(state: &AdapterState, line: i64) -> i64 {
|
||||
if let Some(probe) = debug_probe_for_line(state, line) {
|
||||
if state.runtime_backend == crate::virtual_model::RuntimeBackend::Simulated {
|
||||
let function = probe.function.to_ascii_lowercase();
|
||||
if function.contains("linux") {
|
||||
return LINUX_THREAD;
|
||||
}
|
||||
if function.contains("windows") {
|
||||
return WINDOWS_THREAD;
|
||||
}
|
||||
if function.contains("package") {
|
||||
return PACKAGE_THREAD;
|
||||
}
|
||||
return MAIN_THREAD;
|
||||
}
|
||||
return state
|
||||
.stopped_task
|
||||
.as_ref()
|
||||
.and_then(|task| {
|
||||
state
|
||||
.threads
|
||||
.values()
|
||||
.find(|thread| &thread.task == task)
|
||||
.map(|thread| thread.id)
|
||||
})
|
||||
.unwrap_or(MAIN_THREAD);
|
||||
}
|
||||
|
||||
if let Some(function_name) = source_function_name_at_line(state, line) {
|
||||
let lower = function_name.to_ascii_lowercase();
|
||||
if lower.contains("linux") {
|
||||
return LINUX_THREAD;
|
||||
}
|
||||
if lower.contains("windows") {
|
||||
return WINDOWS_THREAD;
|
||||
}
|
||||
if lower.contains("package") {
|
||||
return PACKAGE_THREAD;
|
||||
}
|
||||
return MAIN_THREAD;
|
||||
}
|
||||
|
||||
state
|
||||
.threads
|
||||
.values()
|
||||
.find(|thread| thread.line == line)
|
||||
.map(|thread| thread.id)
|
||||
.unwrap_or(MAIN_THREAD)
|
||||
}
|
||||
|
||||
fn debug_probe_for_line(state: &AdapterState, line: i64) -> Option<&BundleDebugProbe> {
|
||||
let line = u32::try_from(line).ok()?;
|
||||
state
|
||||
.debug_probes
|
||||
.iter()
|
||||
.find(|probe| probe.line_start <= line && line <= probe.line_end)
|
||||
}
|
||||
|
||||
pub(crate) fn source_function_name_at_line(state: &AdapterState, line: i64) -> Option<String> {
|
||||
let source = fs::read_to_string(crate::source::resolve_source_path(
|
||||
&state.project,
|
||||
&state.source_path,
|
||||
))
|
||||
.ok()?;
|
||||
let lines = source.lines().collect::<Vec<_>>();
|
||||
let mut index = usize::try_from(line).ok()?.saturating_sub(1);
|
||||
if index >= lines.len() {
|
||||
index = lines.len().saturating_sub(1);
|
||||
}
|
||||
lines[..=index]
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|line| parse_rust_function_name(line))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_rust_function_name(line: &str) -> Option<String> {
|
||||
let start = line.find("fn ")? + 3;
|
||||
let rest = &line[start..];
|
||||
let name = rest
|
||||
.chars()
|
||||
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
|
||||
.collect::<String>();
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
pub(crate) fn step_thread(state: &mut AdapterState, thread_id: i64, description: &str) {
|
||||
state.epoch += 1;
|
||||
let resolved_thread = if state.threads.contains_key(&thread_id) {
|
||||
thread_id
|
||||
} else {
|
||||
LINUX_THREAD
|
||||
};
|
||||
if let Some(thread) = state.threads.get_mut(&resolved_thread) {
|
||||
thread.state = DebugRuntimeState::Frozen;
|
||||
thread.line += 1;
|
||||
thread
|
||||
.recent_output
|
||||
.push(format!("debug epoch {} {description}", state.epoch));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue