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 { 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 { 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, ) -> Vec { 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::>(); 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, ) -> Vec { 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 { 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, ) -> 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, ) -> 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 { let source = fs::read_to_string(crate::source::resolve_source_path( &state.project, &state.source_path, )) .ok()?; let lines = source.lines().collect::>(); 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 { let start = line.find("fn ")? + 3; let rest = &line[start..]; let name = rest .chars() .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_') .collect::(); (!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)); } }