clusterflux-public/crates/clusterflux-dap/src/variables.rs
Clusterflux release c1769967c1 Public release release-01875e88a3e2
Source commit: 01875e88a3e25379c309489f9f057dd97c0d37de

Public tree identity: sha256:8f37a1aa0cc8f408975daf9cabbe193f8f4c169c6d25e8795e67a3ed5083c76b
2026-07-17 06:09:42 +02:00

764 lines
26 KiB
Rust

use std::collections::BTreeMap;
use std::fs;
use clusterflux_core::{Digest, TaskInstanceId};
use serde_json::{json, Value};
use crate::breakpoints::parse_rust_function_name;
use crate::demo_backend::{LINUX_THREAD, MAIN_THREAD, PACKAGE_THREAD, WINDOWS_THREAD};
use crate::virtual_model::{AdapterState, VirtualThread};
pub(crate) fn variables_response(state: &AdapterState, reference: i64) -> Value {
let Some(thread) = state.threads.values().find(|thread| {
thread.locals_ref == reference
|| thread.wasm_locals_ref == reference
|| thread.args_ref == reference
|| thread.runtime_ref == reference
|| thread.output_ref == reference
|| thread.target_ref == reference
|| thread.vfs_ref == reference
|| thread.command_ref == reference
}) else {
return json!({ "variables": [] });
};
if thread.locals_ref == reference {
return json!({ "variables": source_locals_variables(state, thread) });
}
if thread.wasm_locals_ref == reference {
return json!({ "variables": wasm_frame_local_variables(state, thread) });
}
if thread.args_ref == reference {
let mut variables = thread
.runtime_task_args
.iter()
.map(|(name, value)| {
json!({
"name": name,
"value": value,
"type": "task-argument",
"variablesReference": 0,
})
})
.chain(thread.runtime_handles.iter().map(|(name, value)| {
json!({
"name": name,
"value": value,
"type": "runtime-handle",
"variablesReference": 0,
})
}))
.collect::<Vec<_>>();
if variables.is_empty() {
variables.push(json!({
"name": "runtime-boundary-diagnostic",
"value": "the frozen runtime participant reported no task arguments or handles at this probe",
"type": "diagnostic",
"variablesReference": 0,
}));
}
return json!({ "variables": variables });
}
if thread.target_ref == reference {
let runtime_backed =
state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated;
return json!({
"variables": [
{
"name": "task",
"value": thread.task.to_string(),
"variablesReference": 0
},
{
"name": "attempt",
"value": thread.attempt_id,
"variablesReference": 0
},
{
"name": "environment",
"value": if runtime_backed { thread.runtime_environment.as_deref().unwrap_or("unknown") } else { task_environment(thread) },
"variablesReference": 0
},
{
"name": "kind",
"value": if thread.id == MAIN_THREAD { "wasm entrypoint" } else { "wasm task" },
"variablesReference": 0
},
{
"name": "required_capability",
"value": if runtime_backed { "not reported by the frozen node participant" } else if thread.id == MAIN_THREAD { "none" } else { "Command" },
"variablesReference": 0
}
]
});
}
if thread.vfs_ref == reference {
if state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated {
return json!({
"variables": [
{
"name": "runtime-vfs-diagnostic",
"value": thread.runtime_vfs_checkpoint.as_deref().unwrap_or("the coordinator has no current VFS checkpoint identity"),
"variablesReference": 0
},
{
"name": "reported_artifact_path",
"value": state.runtime_artifact_path.as_deref().unwrap_or("unavailable"),
"variablesReference": 0
}
]
});
}
return json!({
"variables": [
{
"name": "/vfs/artifacts",
"value": artifact_handle_value(state, thread),
"variablesReference": 0
},
{
"name": "/vfs/sources",
"value": format!("source://local-checkout/{}", project_snapshot_suffix(&state.project)),
"variablesReference": 0
},
{
"name": "/vfs/blobs",
"value": blob_digest(state, thread),
"variablesReference": 0
},
{
"name": "durability",
"value": "best-effort until explicitly exported or stored",
"variablesReference": 0
}
]
});
}
if thread.runtime_ref == reference {
return json!({
"variables": [
{
"name": "virtual_process_id",
"value": state.process.to_string(),
"variablesReference": 0
},
{
"name": "virtual_thread",
"value": thread.task.to_string(),
"variablesReference": 0
},
{
"name": "task_attempt_id",
"value": thread.attempt_id,
"variablesReference": 0
},
{
"name": "node",
"value": thread.runtime_node.as_deref().unwrap_or("unknown"),
"variablesReference": 0
},
{
"name": "restart_compatible",
"value": thread.restart_compatible.map_or("unknown".to_owned(), |value| value.to_string()),
"variablesReference": 0
},
{
"name": "debug_epoch",
"value": state.epoch,
"variablesReference": 0
},
{
"name": "state",
"value": format!("{:?}", thread.state),
"variablesReference": 0
},
{
"name": "runtime_backend",
"value": format!("{:?}", state.runtime_backend),
"variablesReference": 0
},
{
"name": "coordinator_task_events",
"value": state.runtime_event_count,
"variablesReference": 0
},
{
"name": "command_status",
"value": thread.runtime_command_status.as_deref().unwrap_or(&state.command_status),
"variablesReference": 0
},
{
"name": "command_spec",
"value": command_spec_summary(state, thread),
"variablesReference": if thread.runtime_command_status.is_some() { thread.command_ref } else { 0 }
},
{
"name": "stdout_tail",
"value": output_tail_display(&thread.stdout_tail, thread.stdout_bytes, thread.stdout_truncated, "stdout"),
"variablesReference": 0
},
{
"name": "stderr_tail",
"value": output_tail_display(&thread.stderr_tail, thread.stderr_bytes, thread.stderr_truncated, "stderr"),
"variablesReference": 0
}
]
});
}
if thread.command_ref == reference {
if state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated {
return json!({
"variables": [
{
"name": "status",
"value": thread.runtime_command_status.as_deref().unwrap_or("no native command state was reported by this participant"),
"variablesReference": 0
},
{
"name": "command-spec-diagnostic",
"value": "the node debug snapshot does not yet report native command program, arguments, or capability requirements",
"variablesReference": 0
}
]
});
}
return json!({
"variables": [
{
"name": "program",
"value": command_program(thread),
"variablesReference": 0
},
{
"name": "args",
"value": command_args_summary(state, thread),
"variablesReference": 0
},
{
"name": "required_capability",
"value": if thread.id == MAIN_THREAD { "none" } else { "Command" },
"variablesReference": 0
},
{
"name": "status",
"value": state.command_status,
"variablesReference": 0
}
]
});
}
let mut variables = vec![
json!({
"name": "stdout_tail",
"value": output_tail_display(&thread.stdout_tail, thread.stdout_bytes, thread.stdout_truncated, "stdout"),
"variablesReference": 0
}),
json!({
"name": "stderr_tail",
"value": output_tail_display(&thread.stderr_tail, thread.stderr_bytes, thread.stderr_truncated, "stderr"),
"variablesReference": 0
}),
];
variables.extend(
thread
.recent_output
.iter()
.enumerate()
.map(|(index, output)| {
json!({
"name": format!("line_{index}"),
"value": output,
"variablesReference": 0
})
}),
);
json!({ "variables": variables })
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SourceLocal {
name: String,
line: i64,
value: Option<String>,
}
fn source_locals_variables(state: &AdapterState, thread: &VirtualThread) -> Vec<Value> {
let (locals, diagnostic) = source_locals_for_thread(state, thread);
let mut variables = locals
.into_iter()
.map(|local| {
let has_value = local.value.is_some();
let value = local.value.unwrap_or_else(|| {
format!(
"cannot be inspected: source-level local from line {} is known, but DWARF/runtime value snapshot is unavailable",
local.line
)
});
json!({
"name": local.name,
"value": value,
"type": if has_value { "clusterflux-source-local" } else { "unavailable-local" },
"variablesReference": 0
})
})
.collect::<Vec<_>>();
variables.push(json!({
"name": "unavailable-local-diagnostic",
"value": diagnostic,
"type": "unavailable-local",
"variablesReference": 0
}));
variables
}
fn wasm_frame_local_variables(_state: &AdapterState, thread: &VirtualThread) -> Vec<Value> {
if thread.wasm_local_values.is_empty() {
return vec![json!({
"name": "wasm-local-diagnostic",
"value": "the frozen node participant did not report inspectable Wasm frame locals at this safepoint",
"type": "unavailable-local",
"variablesReference": 0
})];
}
thread
.wasm_local_values
.iter()
.map(|(name, value)| {
json!({
"name": name,
"value": value,
"type": "wasm-frame-local",
"variablesReference": 0
})
})
.collect()
}
fn source_locals_for_thread(
state: &AdapterState,
thread: &VirtualThread,
) -> (Vec<SourceLocal>, String) {
if state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated && thread.line <= 0
{
return (
Vec::new(),
"cannot be inspected: the coordinator has no current source location for this attempt"
.to_owned(),
);
}
let source_path = crate::source::resolve_source_path(&state.project, &state.source_path);
let Ok(source) = fs::read_to_string(&source_path) else {
return (
Vec::new(),
format!(
"cannot be inspected: source file `{}` is unavailable; task args and handles remain visible",
source_path.display()
),
);
};
let lines = source.lines().collect::<Vec<_>>();
if lines.is_empty() {
return (
Vec::new(),
"cannot be inspected: source file is empty; task args and handles remain visible"
.to_owned(),
);
}
let current = usize::try_from(thread.line)
.ok()
.and_then(|line| line.checked_sub(1))
.unwrap_or(0)
.min(lines.len() - 1);
let function_start = lines[..=current]
.iter()
.rposition(|line| parse_rust_function_name(line).is_some())
.unwrap_or(0);
let mut locals = Vec::new();
let mut runtime_values = BTreeMap::new();
let mut index = function_start;
while index <= current {
let Some(name) = parse_let_binding_name(lines[index]) else {
index += 1;
continue;
};
let line = (index + 1) as i64;
let mut statement = lines[index].to_owned();
while !statement.contains(';') && index < current {
index += 1;
statement.push('\n');
statement.push_str(lines[index]);
}
let runtime_value =
infer_clusterflux_source_local_value(state, &statement, &runtime_values);
if let Some(value) = runtime_value.clone() {
runtime_values.insert(name.clone(), value);
}
locals.push(SourceLocal {
name,
line,
value: runtime_value.map(|value| value.display(state)),
});
index += 1;
}
let diagnostic = if locals.is_empty() {
"cannot be inspected: no source-level `let` locals are in scope for this frame; task args and handles remain visible"
.to_owned()
} else if locals.iter().any(|local| local.value.is_some()) {
"selected source-level Clusterflux API locals are populated from virtual-thread runtime state; non-Clusterflux locals remain best-effort until DWARF value snapshots are available"
.to_owned()
} else {
"cannot be inspected: selected source-level local names are best-effort until DWARF/runtime value snapshots are available"
.to_owned()
};
(locals, diagnostic)
}
fn parse_let_binding_name(line: &str) -> Option<String> {
let rest = line.trim_start().strip_prefix("let ")?;
let rest = rest
.trim_start()
.strip_prefix("mut ")
.unwrap_or(rest)
.trim_start();
let name = rest
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.collect::<String>();
(!name.is_empty()).then_some(name)
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum SourceLocalRuntimeValue {
SourceSnapshot(String),
TaskHandle {
task: TaskInstanceId,
thread_id: i64,
env: String,
state: String,
},
ThreadId(i64),
Artifact(String),
}
impl SourceLocalRuntimeValue {
fn display(&self, _state: &AdapterState) -> String {
match self {
SourceLocalRuntimeValue::SourceSnapshot(digest) => {
format!("SourceSnapshot {{ digest = \"{digest}\" }}")
}
SourceLocalRuntimeValue::TaskHandle {
task,
thread_id,
env,
state,
} => format!(
"TaskHandle {{ task = \"{task}\", virtual_thread_id = {thread_id}, env = \"{env}\", state = {state} }}"
),
SourceLocalRuntimeValue::ThreadId(thread_id) => thread_id.to_string(),
SourceLocalRuntimeValue::Artifact(artifact) => {
format!("Artifact {{ id = \"{artifact}\" }}")
}
}
}
}
fn infer_clusterflux_source_local_value(
state: &AdapterState,
statement: &str,
runtime_values: &BTreeMap<String, SourceLocalRuntimeValue>,
) -> Option<SourceLocalRuntimeValue> {
if statement.contains("prepare_source_snapshot()") {
return Some(SourceLocalRuntimeValue::SourceSnapshot(
source_snapshot_digest_from_project(state).unwrap_or_else(|| {
format!(
"source://local-checkout/{}",
project_snapshot_suffix(&state.project)
)
}),
));
}
if let Some(task_function) = extract_spawn_task_function(statement) {
let task_name = extract_string_argument(statement, ".name(");
let spawned_thread =
thread_for_spawn_statement(state, &task_function, task_name.as_deref())?;
return Some(SourceLocalRuntimeValue::TaskHandle {
task: spawned_thread.task.clone(),
thread_id: spawned_thread.id,
env: extract_env_name(statement)
.unwrap_or_else(|| task_environment(spawned_thread).to_owned()),
state: format!("{:?}", spawned_thread.state),
});
}
if let Some(receiver) = receiver_before_method(statement, ".virtual_thread_id()") {
if let Some(SourceLocalRuntimeValue::TaskHandle { thread_id, .. }) =
runtime_values.get(&receiver)
{
return Some(SourceLocalRuntimeValue::ThreadId(*thread_id));
}
}
if let Some(receiver) = receiver_before_method(statement, ".join()") {
if let Some(SourceLocalRuntimeValue::TaskHandle { thread_id, .. }) =
runtime_values.get(&receiver)
{
if let Some(thread) = state.threads.get(thread_id) {
return Some(SourceLocalRuntimeValue::Artifact(artifact_handle_value(
state, thread,
)));
}
}
}
None
}
fn extract_spawn_task_function(statement: &str) -> Option<String> {
for marker in [
"clusterflux::spawn::async_task(",
"clusterflux::spawn::task(",
] {
if let Some(args) = extract_call_arguments(statement, marker) {
return args.first().cloned();
}
}
for marker in [
"clusterflux::spawn::async_task_with_arg(",
"clusterflux::spawn::task_with_arg(",
] {
if let Some(args) = extract_call_arguments(statement, marker) {
return args.get(1).cloned();
}
}
None
}
fn source_snapshot_digest_from_project(state: &AdapterState) -> Option<String> {
let source = fs::read_to_string(crate::source::resolve_source_path(
&state.project,
&state.source_path,
))
.ok()?;
let digest_marker = source.find("digest:")?;
let after_digest = &source[digest_marker..];
let first_quote = after_digest.find('"')? + 1;
let rest = &after_digest[first_quote..];
let end_quote = rest.find('"')?;
Some(rest[..end_quote].to_owned())
}
fn extract_call_arguments(statement: &str, marker: &str) -> Option<Vec<String>> {
let start = statement.find(marker)? + marker.len();
let rest = &statement[start..];
let mut args = Vec::new();
let mut current = String::new();
let mut depth = 0_i32;
let mut in_string = false;
let mut escaped = false;
for ch in rest.chars() {
if in_string {
current.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
match ch {
'"' => {
in_string = true;
current.push(ch);
}
'(' | '[' | '{' => {
depth += 1;
current.push(ch);
}
')' => {
if depth == 0 {
let value = current.trim();
if !value.is_empty() {
args.push(value.to_owned());
}
return (!args.is_empty()).then_some(args);
}
depth -= 1;
current.push(ch);
}
']' | '}' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
let value = current.trim();
if !value.is_empty() {
args.push(value.to_owned());
}
current.clear();
}
_ => current.push(ch),
}
}
None
}
fn extract_string_argument(statement: &str, marker: &str) -> Option<String> {
let start = statement.find(marker)? + marker.len();
let rest = &statement[start..];
let quoted = rest.strip_prefix('"')?;
let end = quoted.find('"')?;
Some(quoted[..end].to_owned())
}
fn extract_env_name(statement: &str) -> Option<String> {
if statement.contains("windows_env") || statement.contains("env!(\"windows\")") {
return Some("windows".to_owned());
}
if statement.contains("linux_command_env") || statement.contains("env!(\"linux-command\")") {
return Some("linux-command".to_owned());
}
if statement.contains("linux_env") || statement.contains("env!(\"linux\")") {
return Some("linux".to_owned());
}
if statement.contains("coordinator_env") || statement.contains("env!(\"coordinator\")") {
return Some("coordinator".to_owned());
}
None
}
fn receiver_before_method(statement: &str, method: &str) -> Option<String> {
let before = statement.split(method).next()?.trim_end();
let receiver = before
.chars()
.rev()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.collect::<String>()
.chars()
.rev()
.collect::<String>();
(!receiver.is_empty()).then_some(receiver)
}
fn thread_for_spawn_statement<'a>(
state: &'a AdapterState,
task_function: &str,
task_name: Option<&str>,
) -> Option<&'a VirtualThread> {
if let Some(task_name) = task_name {
if let Some(thread) = state
.threads
.values()
.find(|thread| thread.name == task_name)
{
return Some(thread);
}
}
let normalized = task_function.replace('_', "-");
state.threads.values().find(|thread| {
thread.task.as_str() == normalized
|| (thread.id == PACKAGE_THREAD && normalized == "package-release")
})
}
fn task_environment(thread: &VirtualThread) -> &'static str {
match thread.id {
WINDOWS_THREAD => "windows",
MAIN_THREAD => "coordinator",
PACKAGE_THREAD => "coordinator",
LINUX_THREAD => "linux",
_ => "unconstrained",
}
}
fn artifact_handle_value(state: &AdapterState, thread: &VirtualThread) -> String {
if state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated {
return state.runtime_artifact_path.clone().unwrap_or_else(|| {
"unavailable: runtime participant did not report an artifact handle".to_owned()
});
}
if thread.id == LINUX_THREAD {
return state.runtime_artifact_path.clone().unwrap_or_else(|| {
format!("artifact://{}/{}/app.tar.zst", state.process, thread.task)
});
}
if thread.id == PACKAGE_THREAD {
return "artifact://package/release.tar.zst".to_owned();
}
format!("artifact://{}/{}/app.tar.zst", state.process, thread.task)
}
fn blob_digest(state: &AdapterState, thread: &VirtualThread) -> String {
Digest::from_parts([
b"blob:v1".as_slice(),
state.project.as_bytes(),
thread.task.as_str().as_bytes(),
])
.as_str()
.to_owned()
}
fn command_spec_summary(state: &AdapterState, thread: &VirtualThread) -> String {
if state.runtime_backend != crate::virtual_model::RuntimeBackend::Simulated {
return thread
.runtime_command_status
.clone()
.unwrap_or_else(|| "no native command state reported".to_owned());
}
format!(
"{} {} ({})",
command_program(thread),
command_args_summary(state, thread),
state.command_status
)
}
fn command_program(thread: &VirtualThread) -> &'static str {
if thread.id == MAIN_THREAD {
"coordinator-side wasm"
} else {
"cargo"
}
}
fn command_args_summary(state: &AdapterState, thread: &VirtualThread) -> String {
if thread.id == MAIN_THREAD {
return format!("entry={}", state.entry);
}
format!("test --quiet --manifest-path {}/Cargo.toml", state.project)
}
fn output_tail_display(tail: &str, bytes: u64, truncated: bool, stream: &str) -> String {
if tail.is_empty() {
return format!("no {stream} captured ({bytes} bytes)");
}
if truncated {
format!("{tail}\n... truncated")
} else {
tail.to_owned()
}
}
fn project_snapshot_suffix(project: &str) -> String {
Digest::from_parts([b"source-snapshot:v1".as_slice(), project.as_bytes()])
.as_str()
.trim_start_matches("sha256:")
.chars()
.take(12)
.collect()
}