Public release release-09ca780bd67a

Source commit: 09ca780bd67a00467e78139cf38466b8201b66ca

Public tree identity: sha256:2f744c260b5fc2cf074ad9129f97f87f03714902e5b9fe174128afdc342ec2d0
This commit is contained in:
Clusterflux release 2026-07-19 18:38:40 +02:00
commit eb1077f380
221 changed files with 81144 additions and 0 deletions

View 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));
}
}