Public release release-6d1a121b0a8a

Source commit: 6d1a121b0a8a636aac95998fe5d15c24c78eb7d0

Public tree identity: sha256:2bc22807f5b838286678b65d3f550b8ae4714ae673622041d4c2516a7c5b7926
This commit is contained in:
Clusterflux release 2026-07-17 04:42:17 +02:00
commit 21f097d9a8
210 changed files with 78649 additions and 0 deletions

File diff suppressed because it is too large Load diff

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

View file

@ -0,0 +1,129 @@
use std::io::{self, BufRead, Write};
use anyhow::{anyhow, Result};
use serde_json::{json, Value};
#[derive(Clone)]
pub(crate) struct DapWriter {
seq: i64,
}
impl DapWriter {
pub(crate) fn new() -> Self {
Self { seq: 1 }
}
pub(crate) fn response(
&mut self,
request: &Value,
success: bool,
body: Value,
) -> io::Result<()> {
let command = request
.get("command")
.and_then(Value::as_str)
.unwrap_or("<unknown>");
let request_seq = request.get("seq").and_then(Value::as_i64).unwrap_or(0);
let seq = self.next_seq();
self.write(json!({
"seq": seq,
"type": "response",
"request_seq": request_seq,
"success": success,
"command": command,
"body": body,
}))
}
pub(crate) fn error_response(
&mut self,
request: &Value,
message: impl Into<String>,
) -> io::Result<()> {
let command = request
.get("command")
.and_then(Value::as_str)
.unwrap_or("<unknown>");
let request_seq = request.get("seq").and_then(Value::as_i64).unwrap_or(0);
let seq = self.next_seq();
self.write(json!({
"seq": seq,
"type": "response",
"request_seq": request_seq,
"success": false,
"command": command,
"message": message.into(),
}))
}
pub(crate) fn event(&mut self, event: &str, body: Value) -> io::Result<()> {
let seq = self.next_seq();
self.write(json!({
"seq": seq,
"type": "event",
"event": event,
"body": body,
}))
}
pub(crate) fn output(&mut self, category: &str, output: impl Into<String>) -> io::Result<()> {
self.event(
"output",
json!({
"category": category,
"output": output.into(),
}),
)
}
fn next_seq(&mut self) -> i64 {
let seq = self.seq;
self.seq += 1;
seq
}
fn write(&mut self, message: Value) -> io::Result<()> {
let payload = serde_json::to_vec(&message)?;
let mut out = io::stdout().lock();
write!(out, "Content-Length: {}\r\n\r\n", payload.len())?;
out.write_all(&payload)?;
out.flush()
}
}
pub(crate) fn initialize_capabilities() -> Value {
json!({
"supportsConfigurationDoneRequest": true,
"supportsTerminateRequest": true,
"supportsRestartRequest": true,
"supportsRestartFrame": true,
"supportsEvaluateForHovers": true,
"supportsStepBack": false,
})
}
pub(crate) fn read_message<R: BufRead>(reader: &mut R) -> Result<Option<Value>> {
let mut content_length = None;
loop {
let mut line = String::new();
let bytes = reader.read_line(&mut line)?;
if bytes == 0 {
return Ok(None);
}
let line = line.trim_end_matches(['\r', '\n']);
if line.is_empty() {
break;
}
if let Some(value) = line.strip_prefix("Content-Length:") {
content_length = Some(value.trim().parse::<usize>()?);
}
}
let length = content_length.ok_or_else(|| anyhow!("DAP message missing Content-Length"))?;
let mut payload = vec![0; length];
reader.read_exact(&mut payload)?;
Ok(Some(serde_json::from_slice(&payload)?))
}

View file

@ -0,0 +1,68 @@
use std::collections::BTreeMap;
use clusterflux_core::{DebugRuntimeState, TaskInstanceId};
use crate::virtual_model::{AdapterState, VirtualThread};
pub(crate) const MAIN_THREAD: i64 = 1;
pub(crate) const LINUX_THREAD: i64 = 2;
pub(crate) const WINDOWS_THREAD: i64 = 3;
pub(crate) const PACKAGE_THREAD: i64 = 4;
pub(crate) fn is_explicit_demo_backend(value: &str) -> bool {
value == "simulated" || value == "demo"
}
pub(crate) fn launch_threads(entry: &str) -> BTreeMap<i64, VirtualThread> {
[
thread(MAIN_THREAD, "main", &format!("{entry} virtual process"), 12),
thread(LINUX_THREAD, "compile-linux", "compile linux", 42),
thread(WINDOWS_THREAD, "compile-windows", "compile windows", 52),
thread(PACKAGE_THREAD, "package-release", "package artifacts", 64),
]
.into_iter()
.map(|thread| (thread.id, thread))
.collect()
}
pub(crate) fn start_simulated_backend(state: &mut AdapterState) {
state.command_status = "running under simulated debug adapter state".to_owned();
}
fn thread(id: i64, task: &str, name: &str, line: i64) -> VirtualThread {
VirtualThread {
id,
frame_id: 1000 + id,
locals_ref: 5000 + id,
wasm_locals_ref: 9000 + id,
args_ref: 2000 + id,
runtime_ref: 3000 + id,
output_ref: 4000 + id,
target_ref: 6000 + id,
vfs_ref: 7000 + id,
command_ref: 8000 + id,
task: TaskInstanceId::from(task),
attempt_id: format!("demo-attempt-{id}"),
task_definition: clusterflux_core::TaskDefinitionId::from(task),
name: name.to_owned(),
line,
state: DebugRuntimeState::Running,
freeze_supported: true,
recent_output: vec![format!("{name}: waiting")],
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
runtime_stack_frames: Vec::new(),
wasm_local_values: Vec::new(),
runtime_task_args: Vec::new(),
runtime_handles: Vec::new(),
runtime_command_status: None,
runtime_node: Some("demo-node".to_owned()),
runtime_environment: Some("demo".to_owned()),
runtime_vfs_checkpoint: Some("demo-vfs".to_owned()),
restart_compatible: Some(true),
}
}

View file

@ -0,0 +1,43 @@
use anyhow::Result;
#[cfg(test)]
use std::path::Path;
mod adapter;
mod breakpoints;
mod dap_protocol;
mod demo_backend;
mod runtime_client;
mod source;
mod variables;
mod view_state;
mod virtual_model;
#[cfg(test)]
use adapter::runtime_backend_from_launch_arg;
#[cfg(test)]
use breakpoints::{
freeze_all, resolve_breakpoints, resolve_breakpoints_for_source,
restart_requires_whole_process, stopped_thread_for_breakpoint,
};
#[cfg(test)]
use dap_protocol::{initialize_capabilities, read_message};
#[cfg(test)]
use runtime_client::{client_user_request, parse_task_restart_response};
#[cfg(test)]
use variables::variables_response;
#[cfg(test)]
use virtual_model::{AdapterState, RuntimeLaunchRecord};
#[cfg(test)]
use clusterflux_core::{BundleDebugProbe, DebugRuntimeState, TaskInstanceId};
#[cfg(test)]
use demo_backend::{LINUX_THREAD, MAIN_THREAD, PACKAGE_THREAD, WINDOWS_THREAD};
#[cfg(test)]
use virtual_model::{process_id, RuntimeBackend};
fn main() -> Result<()> {
adapter::run_adapter()
}
#[cfg(test)]
mod tests;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,243 @@
use std::time::{Duration, Instant};
use anyhow::{anyhow, Result};
use clusterflux_core::TaskInstanceId;
use serde_json::{json, Value};
use crate::virtual_model::AdapterState;
use super::{
client_user_request, coordinator_request, DebugEpochRecord, DebugEpochStatusRecord,
TaskRestartRecord,
};
pub(super) fn coordinator_debug_epoch_request(
state: &AdapterState,
payload: Value,
) -> Result<Value> {
let coordinator =
crate::view_state::normalize_coordinator_endpoint(&state.coordinator_endpoint);
coordinator_request(&coordinator, payload)
}
pub(super) fn parse_debug_epoch_response(response: Value) -> Result<DebugEpochRecord> {
let epoch = response
.get("epoch")
.and_then(Value::as_u64)
.ok_or_else(|| anyhow!("coordinator debug epoch response did not include an epoch"))?;
let command = response
.get("command")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("coordinator debug epoch response did not include a command"))?
.to_owned();
let affected_tasks = response
.get("affected_tasks")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
Ok(DebugEpochRecord {
epoch,
command,
affected_tasks,
})
}
pub(super) fn wait_for_debug_epoch_state(
state: &AdapterState,
epoch: u64,
frozen: bool,
) -> Result<DebugEpochStatusRecord> {
let deadline = Instant::now() + Duration::from_secs(60);
loop {
let response = match coordinator_debug_epoch_request(
state,
client_user_request(
state,
json!({
"type": "inspect_debug_epoch",
"tenant": state.tenant,
"project": state.project_id,
"actor_user": state.actor_user,
"process": state.process.as_str(),
"epoch": epoch,
}),
),
) {
Ok(response) => response,
Err(error) if !frozen && debug_epoch_was_released(&error, state, epoch) => {
return Ok(DebugEpochStatusRecord {
epoch,
command: "resume".to_owned(),
expected_tasks: 0,
acknowledgements: Vec::new(),
fully_frozen: false,
partially_frozen: false,
fully_resumed: true,
failed: false,
failure_messages: Vec::new(),
});
}
Err(error) => return Err(error),
};
let status = parse_debug_epoch_status(response)?;
if frozen && (status.fully_frozen || status.partially_frozen) {
return Ok(status);
}
if status.failed {
return Err(anyhow!(
"debug epoch {epoch} participant failed: {}",
status.failure_messages.join("; ")
));
}
if !frozen && status.fully_resumed {
return Ok(status);
}
if Instant::now() >= deadline {
return Err(anyhow!(
"debug epoch {epoch} did not receive {}/{} signed participant acknowledgements for {} within 60 seconds",
status.acknowledgements.len(),
status.expected_tasks,
if frozen { "frozen state" } else { "resumed state" }
));
}
std::thread::sleep(Duration::from_millis(100));
}
}
fn debug_epoch_was_released(error: &anyhow::Error, state: &AdapterState, epoch: u64) -> bool {
format!("{error:#}").contains(&format!(
"debug epoch {epoch} is not active for {}",
state.process
))
}
fn parse_debug_epoch_status(response: Value) -> Result<DebugEpochStatusRecord> {
let epoch = response
.get("epoch")
.and_then(Value::as_u64)
.ok_or_else(|| anyhow!("coordinator debug epoch status omitted epoch"))?;
let command = response
.get("command")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("coordinator debug epoch status omitted command"))?
.to_owned();
let expected_tasks = response
.get("expected_tasks")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
let acknowledgements = response
.get("acknowledgements")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let failure_messages = response
.get("failure_messages")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect();
Ok(DebugEpochStatusRecord {
epoch,
command,
expected_tasks,
acknowledgements,
fully_frozen: response
.get("fully_frozen")
.and_then(Value::as_bool)
.unwrap_or(false),
partially_frozen: response
.get("partially_frozen")
.and_then(Value::as_bool)
.unwrap_or(false),
fully_resumed: response
.get("fully_resumed")
.and_then(Value::as_bool)
.unwrap_or(false),
failed: response
.get("failed")
.and_then(Value::as_bool)
.unwrap_or(false),
failure_messages,
})
}
pub(crate) fn parse_task_restart_response(response: Value) -> Result<TaskRestartRecord> {
Ok(TaskRestartRecord {
accepted: response
.get("accepted")
.and_then(Value::as_bool)
.ok_or_else(|| anyhow!("coordinator task restart response did not include accepted"))?,
restarted_task_instance: response
.get("restarted_task_instance")
.and_then(Value::as_str)
.map(TaskInstanceId::new),
restarted_attempt_id: response
.get("restarted_attempt_id")
.and_then(Value::as_str)
.map(str::to_owned),
clean_boundary_available: response
.get("clean_boundary_available")
.and_then(Value::as_bool)
.ok_or_else(|| {
anyhow!("coordinator task restart response did not include clean_boundary_available")
})?,
requires_whole_process_restart: response
.get("requires_whole_process_restart")
.and_then(Value::as_bool)
.ok_or_else(|| {
anyhow!(
"coordinator task restart response did not include requires_whole_process_restart"
)
})?,
active_task: response
.get("active_task")
.and_then(Value::as_bool)
.ok_or_else(|| anyhow!("coordinator task restart response did not include active_task"))?,
completed_event_observed: response
.get("completed_event_observed")
.and_then(Value::as_bool)
.ok_or_else(|| {
anyhow!("coordinator task restart response did not include completed_event_observed")
})?,
message: response
.get("message")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("coordinator task restart response did not include message"))?
.to_owned(),
})
}
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use super::debug_epoch_was_released;
use crate::virtual_model::AdapterState;
#[test]
fn accepts_epoch_release_after_an_accepted_resume() {
let state = AdapterState {
process: "vp-completed".into(),
..AdapterState::default()
};
assert!(debug_epoch_was_released(
&anyhow!("coordinator protocol error: debug epoch 3 is not active for vp-completed"),
&state,
3,
));
assert!(!debug_epoch_was_released(
&anyhow!("coordinator protocol error: debug epoch 4 is not active for vp-completed"),
&state,
3,
));
assert!(!debug_epoch_was_released(
&anyhow!("coordinator protocol error: debug epoch 3 is not active for vp-other"),
&state,
3,
));
}
}

View file

@ -0,0 +1,63 @@
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
pub(super) fn child_stderr_suffix(child: &mut Child) -> String {
let mut stderr = String::new();
if let Some(mut stream) = child.stderr.take() {
let _ = stream.read_to_string(&mut stderr);
}
let stderr = stderr.trim();
if stderr.is_empty() {
String::new()
} else {
format!("; worker stderr: {stderr}")
}
}
pub(super) fn local_tool_command(
env_var: &str,
binary: &str,
package: &str,
repo: &Path,
) -> Command {
if let Some(path) = std::env::var_os(env_var) {
return Command::new(path);
}
let current_exe = std::env::current_exe().ok();
if !workspace_development_executable(current_exe.as_deref(), repo) {
if let Some(path) = sibling_tool_path(current_exe.as_deref(), binary) {
return Command::new(path);
}
}
// A workspace-built adapter must ask Cargo for the matching service binary:
// a sibling executable can be older than the adapter even though both happen
// to live in target/debug. Installed releases take the sibling path above.
let mut command = Command::new("cargo");
command.args(["run", "-q", "-p", package, "--bin", binary, "--"]);
command
}
fn sibling_tool_path(current_exe: Option<&Path>, binary: &str) -> Option<PathBuf> {
let mut sibling = current_exe?.to_path_buf();
sibling.set_file_name(format!("{binary}{}", std::env::consts::EXE_SUFFIX));
sibling.is_file().then_some(sibling)
}
fn workspace_development_executable(current_exe: Option<&Path>, repo: &Path) -> bool {
let Some(current_exe) = current_exe else {
return false;
};
let target = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.map(|path| {
if path.is_absolute() {
path
} else {
repo.join(path)
}
})
.unwrap_or_else(|| repo.join("target"));
current_exe.starts_with(target)
}

View file

@ -0,0 +1,38 @@
use anyhow::{anyhow, Result};
use clusterflux_control::ControlSession;
use clusterflux_core::coordinator_wire_request;
use serde_json::{json, Value};
use crate::virtual_model::AdapterState;
pub(crate) fn client_user_request(state: &AdapterState, mut request: Value) -> Value {
let Some(session_secret) = state.client_session_secret.as_deref() else {
return request;
};
if let Value::Object(fields) = &mut request {
fields.remove("tenant");
fields.remove("project");
fields.remove("actor_user");
}
json!({
"type": "authenticated",
"session_secret": session_secret,
"request": request,
})
}
pub(super) fn coordinator_request(addr: &str, request: Value) -> Result<Value> {
let mut session = ControlSession::connect(addr)?;
let wire_request = coordinator_wire_request("dap-1", request);
let response = session.request(&wire_request)?;
if response.get("type").and_then(Value::as_str) == Some("error") {
return Err(anyhow!(
"{}",
response
.get("message")
.and_then(Value::as_str)
.unwrap_or("coordinator returned an error")
));
}
Ok(response)
}

View file

@ -0,0 +1,71 @@
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde_json::{json, Value};
use crate::virtual_model::AdapterState;
pub(crate) fn infer_source_path(project: &str) -> String {
if Path::new(project).join("src/build.rs").is_file() {
"src/build.rs".to_owned()
} else if Path::new(project).join("src/main.rs").is_file() {
"src/main.rs".to_owned()
} else if Path::new(project).join("src/lib.rs").is_file() {
"src/lib.rs".to_owned()
} else {
"src/main.rs".to_owned()
}
}
pub(crate) fn source_name(source_path: &str) -> &str {
Path::new(source_path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(source_path)
}
pub(crate) fn stack_source_path(state: &AdapterState) -> String {
normalized_source_path(&state.project, &state.source_path)
.to_string_lossy()
.into_owned()
}
pub(crate) fn source_paths_match(project: &str, configured: &str, requested: &str) -> bool {
normalized_source_path(project, configured) == normalized_source_path(project, requested)
}
pub(crate) fn source_response(state: &AdapterState, request: &Value) -> Result<Value> {
let source_path = request
.get("arguments")
.and_then(|arguments| arguments.get("source"))
.and_then(|source| source.get("path"))
.and_then(Value::as_str)
.unwrap_or(&state.source_path);
let content = fs::read_to_string(resolve_source_path(&state.project, source_path))?;
Ok(json!({
"content": content,
"mimeType": "text/x-rustsrc",
}))
}
pub(crate) fn resolve_source_path(project: &str, source_path: &str) -> PathBuf {
let path = Path::new(source_path);
if path.is_absolute() {
path.to_path_buf()
} else {
Path::new(project).join(path)
}
}
fn normalized_source_path(project: &str, source_path: &str) -> PathBuf {
let resolved = resolve_source_path(project, source_path);
let absolute = if resolved.is_absolute() {
resolved
} else {
std::env::current_dir()
.unwrap_or_else(|_| Path::new(".").to_path_buf())
.join(resolved)
};
absolute.canonicalize().unwrap_or(absolute)
}

View file

@ -0,0 +1,873 @@
#![allow(clippy::field_reassign_with_default)]
use std::fs;
use std::io::Cursor;
use serde_json::json;
use super::*;
#[test]
fn reads_standard_dap_content_length_frame_from_generic_client() {
let payload = json!({
"seq": 7,
"type": "request",
"command": "threads"
})
.to_string();
let frame = format!("Content-Length: {}\r\n\r\n{payload}", payload.len());
let mut reader = Cursor::new(frame.into_bytes());
let message = read_message(&mut reader)
.unwrap()
.expect("DAP frame should contain a request");
assert_eq!(message["seq"], 7);
assert_eq!(message["type"], "request");
assert_eq!(message["command"], "threads");
}
#[test]
fn initialize_capabilities_use_standard_dap_flags() {
let capabilities = initialize_capabilities();
let capabilities = capabilities
.as_object()
.expect("initialize capabilities should be an object");
assert_eq!(
capabilities.get("supportsConfigurationDoneRequest"),
Some(&json!(true))
);
assert_eq!(capabilities.get("supportsRestartFrame"), Some(&json!(true)));
assert_eq!(capabilities.get("supportsStepBack"), Some(&json!(false)));
assert!(capabilities
.keys()
.all(|key| key.starts_with("supports") && !key.contains("clusterflux")));
}
#[test]
fn runtime_backend_defaults_to_local_services_and_requires_explicit_demo() {
assert_eq!(
runtime_backend_from_launch_arg(None).unwrap(),
RuntimeBackend::LocalServices
);
assert_eq!(
runtime_backend_from_launch_arg(Some("simulated")).unwrap(),
RuntimeBackend::Simulated
);
assert_eq!(
runtime_backend_from_launch_arg(Some("demo")).unwrap(),
RuntimeBackend::Simulated
);
assert!(runtime_backend_from_launch_arg(Some("unknown")).is_err());
}
#[test]
fn live_dap_uses_matching_stored_cli_session_and_omits_claimed_scope() {
let temp = tempfile::tempdir().unwrap();
let project = temp.path().join("examples/demo");
fs::create_dir_all(temp.path().join(".clusterflux")).unwrap();
fs::create_dir_all(&project).unwrap();
fs::write(
temp.path().join(".clusterflux/session.json"),
serde_json::to_vec(&json!({
"coordinator": "https://hosted.example.test",
"tenant": "tenant-session",
"project": "project-session",
"user": "user-session",
"session_secret": "dap-session-secret"
}))
.unwrap(),
)
.unwrap();
let mut state = AdapterState::default();
state
.configure_launch(
project.to_string_lossy().into_owned(),
"build".to_owned(),
RuntimeBackend::LiveServices,
Some("https://hosted.example.test/".to_owned()),
None,
)
.unwrap();
assert_eq!(state.tenant.as_str(), "tenant-session");
assert_eq!(state.project_id.as_str(), "project-session");
assert_eq!(state.actor_user.as_str(), "user-session");
assert_eq!(
state.client_session_secret.as_deref(),
Some("dap-session-secret")
);
let request = client_user_request(
&state,
json!({
"type": "debug_attach",
"tenant": "forged-tenant",
"project": "forged-project",
"actor_user": "forged-user",
"process": "vp"
}),
);
assert_eq!(request["type"], "authenticated");
assert_eq!(request["session_secret"], "dap-session-secret");
assert_eq!(request["request"]["type"], "debug_attach");
assert!(request["request"].get("tenant").is_none());
assert!(request["request"].get("project").is_none());
assert!(request["request"].get("actor_user").is_none());
let mismatch = state.configure_launch(
project.to_string_lossy().into_owned(),
"build".to_owned(),
RuntimeBackend::LiveServices,
Some("https://different.example.test".to_owned()),
None,
);
assert!(mismatch
.unwrap_err()
.to_string()
.contains("does not match the authenticated CLI session"));
}
#[test]
fn compatible_restart_frame_does_not_require_whole_process_restart() {
let request = json!({
"command": "restartFrame",
"arguments": {
"frameId": 1,
"sourceEdit": {
"compatibility": "compatible"
}
}
});
assert!(!restart_requires_whole_process(&request));
}
#[test]
fn incompatible_source_edit_requires_whole_process_restart() {
for arguments in [
json!({ "sourceCompatibility": "incompatible" }),
json!({ "sourceEdit": { "compatibility": "whole-process" } }),
json!({ "requiresWholeProcessRestart": true }),
] {
let request = json!({
"command": "restartFrame",
"arguments": arguments,
});
assert!(restart_requires_whole_process(&request));
}
}
#[test]
fn task_restart_response_preserves_coordinator_boundary_decision() {
let record = parse_task_restart_response(json!({
"type": "task_restart",
"accepted": false,
"clean_boundary_available": false,
"requires_whole_process_restart": true,
"active_task": false,
"completed_event_observed": true,
"message": "selected task has only terminal event metadata; restart requires a captured checkpoint boundary"
}))
.unwrap();
assert!(!record.accepted);
assert!(!record.clean_boundary_available);
assert!(record.requires_whole_process_restart);
assert!(!record.active_task);
assert!(record.completed_event_observed);
assert!(record.message.contains("checkpoint boundary"));
}
#[test]
fn nonzero_local_runtime_record_marks_linux_task_failed() {
let mut state = AdapterState::default();
state.runtime_backend = RuntimeBackend::LocalServices;
state.apply_runtime_record(RuntimeLaunchRecord {
coordinator: "127.0.0.1:7999".to_owned(),
node: "node".to_owned(),
node_report: json!({ "terminal_event": {
"task": "compile-linux-1",
"task_definition": "compile-linux"
} }),
task_events: json!({ "events": [] }),
placed_task_launched: true,
status_code: Some(1),
stdout_bytes: 0,
stderr_bytes: 12,
stdout_tail: String::new(),
stderr_tail: "failed".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
event_count: 1,
debug_epoch: None,
stopped_task: None,
stopped_probe_symbol: None,
all_participants_frozen: false,
});
let exact_runtime_thread = state
.threads
.values()
.find(|thread| thread.task == TaskInstanceId::from("compile-linux-1"))
.expect("the exact terminal task instance should have its own thread");
assert!(state.last_task_failed);
assert!(state
.command_status
.contains("failed through local services"));
assert!(matches!(
exact_runtime_thread.state,
DebugRuntimeState::Failed(_)
));
assert!(exact_runtime_thread
.recent_output
.iter()
.any(|line| line.contains("task failed")));
assert_eq!(
state.threads[&LINUX_THREAD].state,
DebugRuntimeState::Running
);
}
#[test]
fn freeze_all_reports_failure_without_claiming_all_stop() {
let mut state = AdapterState::default();
state
.threads
.get_mut(&PACKAGE_THREAD)
.expect("package thread should exist")
.freeze_supported = false;
let error = freeze_all(&mut state, LINUX_THREAD, None).unwrap_err();
assert_eq!(error.thread_id, PACKAGE_THREAD);
assert_eq!(error.task, TaskInstanceId::from("package-release"));
assert!(error.message().contains("could not freeze"));
assert_eq!(state.epoch, 0);
assert!(state
.threads
.values()
.all(|thread| thread.state == DebugRuntimeState::Running));
}
#[test]
fn freeze_all_success_freezes_every_running_participant() {
let mut state = AdapterState::default();
freeze_all(&mut state, LINUX_THREAD, None).unwrap();
assert_eq!(state.epoch, 1);
assert!(state
.threads
.values()
.all(|thread| thread.state == DebugRuntimeState::Frozen));
}
#[test]
fn breakpoint_line_selects_main_or_spawned_virtual_thread() {
let mut state = AdapterState::default();
state.breakpoints = vec![12];
assert_eq!(stopped_thread_for_breakpoint(&state), MAIN_THREAD);
state.breakpoints = vec![42];
assert_eq!(stopped_thread_for_breakpoint(&state), LINUX_THREAD);
}
#[test]
fn breakpoint_resolution_uses_bundle_debug_probe_metadata() {
let mut state = AdapterState::default();
state.debug_probes = vec![BundleDebugProbe {
id: "probe-linux".to_owned(),
source_path: "src/build.rs".to_owned(),
line_start: 20,
line_end: 30,
function: "compile_linux".to_owned(),
task: clusterflux_core::TaskDefinitionId::from("compile-linux"),
}];
let resolved = resolve_breakpoints(&mut state, vec![22, 80]);
assert_eq!(resolved.len(), 2);
assert!(resolved[0].verified);
assert!(resolved[0].message.contains("probe-linux"));
assert!(!resolved[1].verified);
assert!(resolved[1].message.contains("No Clusterflux debug probe"));
assert_eq!(state.breakpoints, vec![22]);
assert_eq!(stopped_thread_for_breakpoint(&state), LINUX_THREAD);
}
#[test]
fn breakpoint_updates_from_another_source_do_not_replace_configured_breakpoints() {
let project = tempfile::tempdir().unwrap();
let source_dir = project.path().join("src");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("build.rs"), "pub fn build() {}\n").unwrap();
fs::write(source_dir.join("main.rs"), "fn main() {}\n").unwrap();
let mut state = AdapterState::default();
state.project = project.path().to_string_lossy().into_owned();
state.source_path = "src/build.rs".to_owned();
state.breakpoints = vec![1];
let requested_source = source_dir.join("main.rs");
let resolved = resolve_breakpoints_for_source(&mut state, requested_source.to_str(), vec![1]);
assert_eq!(resolved.len(), 1);
assert!(!resolved[0].verified);
assert!(resolved[0]
.message
.contains("configured for `src/build.rs`"));
assert_eq!(state.breakpoints, vec![1]);
}
#[test]
fn launch_threads_include_windows_task_in_same_virtual_process() {
let state = AdapterState::default();
let windows = state
.threads
.get(&WINDOWS_THREAD)
.expect("windows task should be debugger-visible");
assert_eq!(state.threads.len(), 4);
assert_eq!(windows.id, WINDOWS_THREAD);
assert_eq!(windows.task, TaskInstanceId::from("compile-windows"));
assert_eq!(windows.name, "compile windows");
assert_eq!(windows.line, 52);
assert_eq!(process_id(&state.project, &state.entry), state.process);
}
#[test]
fn vscode_and_dap_share_stable_workspace_process_identity() {
assert_eq!(
process_id("/workspace/app", "build"),
clusterflux_core::ProcessId::from("vp-e4bd6ef50539")
);
}
#[test]
fn windows_thread_runtime_variables_share_virtual_process() {
let state = AdapterState::default();
let windows = state
.threads
.get(&WINDOWS_THREAD)
.expect("windows task should be debugger-visible");
let variables = variables_response(&state, windows.runtime_ref);
let variables = variables["variables"]
.as_array()
.expect("variables response should be an array");
assert!(variables.iter().any(|variable| {
variable["name"] == "virtual_process_id" && variable["value"] == state.process.to_string()
}));
assert!(variables.iter().any(|variable| {
variable["name"] == "virtual_thread" && variable["value"] == "compile-windows"
}));
assert!(variables.iter().any(|variable| {
variable["name"] == "task_attempt_id"
&& variable["value"] == format!("demo-attempt-{WINDOWS_THREAD}")
}));
}
#[test]
fn variables_expose_mvp_runtime_handles_and_output_tails() {
let mut state = AdapterState::default();
state.runtime_backend = RuntimeBackend::LocalServices;
state.apply_runtime_record(RuntimeLaunchRecord {
coordinator: "127.0.0.1:7999".to_owned(),
node: "node".to_owned(),
node_report: json!({ "terminal_event": {
"task": "compile-linux-1",
"task_definition": "compile-linux"
} }),
task_events: json!({ "events": [] }),
placed_task_launched: true,
status_code: Some(0),
stdout_bytes: 11,
stderr_bytes: 7,
stdout_tail: "hello tail\n".to_owned(),
stderr_tail: "warn\n".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/dap-output.txt".to_owned()),
event_count: 1,
debug_epoch: None,
stopped_task: None,
stopped_probe_symbol: None,
all_participants_frozen: false,
});
let exact_thread_id = state
.threads
.values()
.find(|thread| thread.task == TaskInstanceId::from("compile-linux-1"))
.map(|thread| thread.id)
.expect("the exact terminal task instance should have its own thread");
{
let runtime_thread = state.threads.get_mut(&exact_thread_id).unwrap();
runtime_thread.runtime_task_args =
vec![("source".to_owned(), "source://checkout".to_owned())];
runtime_thread.runtime_handles = vec![(
"artifact".to_owned(),
"artifact://runtime/output".to_owned(),
)];
runtime_thread.runtime_command_status = Some("frozen native command pid 42".to_owned());
}
let runtime_thread = state
.threads
.get(&exact_thread_id)
.expect("exact runtime thread should exist");
let args = variables_response(&state, runtime_thread.args_ref);
let args = args["variables"]
.as_array()
.expect("variables response should be an array");
assert!(args.iter().any(|variable| {
variable["name"] == "source"
&& variable["value"] == "source://checkout"
&& variable["type"] == "task-argument"
}));
assert!(args.iter().any(|variable| {
variable["name"] == "artifact"
&& variable["value"] == "artifact://runtime/output"
&& variable["type"] == "runtime-handle"
}));
let runtime = variables_response(&state, runtime_thread.runtime_ref);
let runtime = runtime["variables"].as_array().unwrap();
let command_ref = runtime
.iter()
.find(|variable| variable["name"] == "command_spec")
.and_then(|variable| variable["variablesReference"].as_i64())
.expect("command spec should have child variables");
assert!(runtime
.iter()
.any(|variable| variable["name"] == "stdout_tail" && variable["value"] == "hello tail\n"));
assert!(runtime
.iter()
.any(|variable| variable["name"] == "stderr_tail" && variable["value"] == "warn\n"));
let command = variables_response(&state, command_ref);
let command = command["variables"].as_array().unwrap();
assert!(command.iter().any(|variable| {
variable["name"] == "status" && variable["value"] == "frozen native command pid 42"
}));
assert!(!command.iter().any(|variable| {
variable["name"] == "program" || variable["name"] == "required_capability"
}));
}
#[test]
fn attach_record_replaces_demo_threads_with_authoritative_task_snapshots() {
let mut state = AdapterState::default();
state.runtime_backend = RuntimeBackend::LiveServices;
state.apply_attach_record(RuntimeLaunchRecord {
coordinator: "127.0.0.1:9443".to_owned(),
node: "coordinator-debug-attach".to_owned(),
node_report: json!({
"debug_attach": {
"authorization": {
"allowed": true,
"reason": "debug attach allowed"
}
},
"process_status": {
"main_task_definition": "sha256:main-definition",
"main_task_instance": format!("ti:{}:main", state.process),
"main_debug_epoch": null,
"coordinator_epoch": 1
},
"task_snapshots": {
"snapshots": [{
"task": "compile-linux-1",
"task_definition": "compile-linux",
"attempt_id": "attempt-compile-linux-1",
"display_name": "compile linux",
"current": true,
"state": "running",
"node": "worker-linux"
}]
}
}),
task_events: json!({
"events": [{
"tenant": "tenant",
"project": "project",
"process": state.process.to_string(),
"node": "worker-linux",
"executor": "node",
"task_definition": "compile-linux",
"task": "compile-linux-1",
"terminal_state": "completed",
"status_code": 0,
"stdout_bytes": 4,
"stderr_bytes": 0,
"stdout_tail": "done",
"stderr_tail": "",
"stdout_truncated": false,
"stderr_truncated": false,
"artifact_path": "/vfs/artifacts/from-attach.txt",
"artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"artifact_size_bytes": 4
}]
}),
placed_task_launched: true,
status_code: Some(0),
stdout_bytes: 4,
stderr_bytes: 0,
stdout_tail: "done".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/from-attach.txt".to_owned()),
event_count: 1,
debug_epoch: None,
stopped_task: None,
stopped_probe_symbol: None,
all_participants_frozen: false,
});
assert_eq!(state.threads.len(), 2);
assert!(state.threads.contains_key(&MAIN_THREAD));
assert!(!state.threads.contains_key(&WINDOWS_THREAD));
let linux = state
.threads
.get(&LINUX_THREAD)
.expect("current coordinator task snapshot should become debugger thread");
assert_eq!(linux.task, TaskInstanceId::from("compile-linux-1"));
assert_eq!(
linux.task_definition,
clusterflux_core::TaskDefinitionId::from("compile-linux")
);
assert!(linux.stdout_tail.is_empty());
assert_eq!(linux.state, DebugRuntimeState::Running);
assert_eq!(state.runtime_artifact_path, None);
assert!(state
.command_status
.contains("attached to existing virtual process"));
assert!(state.command_status.contains("coordinator task event"));
}
#[test]
fn attach_projects_active_coordinator_main_without_inventing_a_worker_task() {
let mut state = AdapterState::default();
state.runtime_backend = RuntimeBackend::LiveServices;
let main_instance = format!("ti:{}:main", state.process);
state.apply_attach_record(RuntimeLaunchRecord {
coordinator: "127.0.0.1:9443".to_owned(),
node: "coordinator-main".to_owned(),
node_report: json!({
"debug_attach": { "authorization": { "allowed": true } },
"process_status": {
"process": state.process.to_string(),
"state": "running",
"main_task_definition": "sha256:main-definition",
"main_task_instance": main_instance,
"main_state": "running",
"main_debug_epoch": null,
"connected_nodes": [],
"coordinator_epoch": 1
}
}),
task_events: json!({ "events": [] }),
placed_task_launched: true,
status_code: None,
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
event_count: 0,
debug_epoch: None,
stopped_task: None,
stopped_probe_symbol: None,
all_participants_frozen: false,
});
assert_eq!(state.threads.len(), 1);
let main = state.threads.get(&MAIN_THREAD).unwrap();
assert_eq!(main.task, TaskInstanceId::from(main_instance.as_str()));
assert_eq!(
main.task_definition,
clusterflux_core::TaskDefinitionId::from("sha256:main-definition")
);
assert_eq!(main.state, DebugRuntimeState::Running);
assert!(main.name.contains("coordinator main"));
}
#[test]
fn terminal_events_do_not_resurrect_threads_absent_from_current_snapshots() {
let mut state = AdapterState::default();
state.runtime_backend = RuntimeBackend::LiveServices;
let main_instance = format!("ti:{}:main", state.process);
state.apply_attach_record(RuntimeLaunchRecord {
coordinator: "127.0.0.1:9443".to_owned(),
node: "coordinator-main".to_owned(),
node_report: json!({
"debug_attach": { "authorization": { "allowed": true } },
"task_snapshots": { "snapshots": [] }
}),
task_events: json!({
"events": [
{
"node": "worker-linux",
"executor": "node",
"task_definition": "compile_linux",
"task": "ti:child:1",
"terminal_state": "completed",
"status_code": 0
},
{
"node": "coordinator-main",
"executor": "coordinator_main",
"task_definition": "sha256:main-definition",
"task": main_instance,
"terminal_state": "completed",
"status_code": 0
}
]
}),
placed_task_launched: true,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
event_count: 2,
debug_epoch: None,
stopped_task: None,
stopped_probe_symbol: None,
all_participants_frozen: false,
});
assert!(state.threads.is_empty());
}
#[test]
fn source_locals_infer_clusterflux_api_values_from_runtime_state() {
let mut state = AdapterState::default();
let project = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/launch-build-demo")
.canonicalize()
.unwrap();
state.project = project.to_string_lossy().into_owned();
state.source_path = "src/build.rs".to_owned();
let source = fs::read_to_string(project.join(&state.source_path)).unwrap();
let inspection_line = source
.lines()
.position(|line| line.contains("let package_artifact = package.join()"))
.map(|index| index as i64 + 1)
.expect("flagship source must expose the post-join locals inspection point");
state.threads.get_mut(&MAIN_THREAD).unwrap().line = inspection_line;
let thread = state.threads[&MAIN_THREAD].clone();
let locals = variables_response(&state, thread.locals_ref);
let locals = locals["variables"].as_array().unwrap();
assert!(locals.iter().any(|variable| {
variable["name"] == "linux"
&& variable["value"].as_str().is_some_and(|value| {
value.contains("TaskHandle")
&& value.contains("compile-linux")
&& value.contains("virtual_thread_id = 2")
})
}));
assert!(locals
.iter()
.any(|variable| { variable["name"] == "linux_thread" && variable["value"] == "2" }));
assert!(locals.iter().any(|variable| {
variable["name"] == "linux_artifact"
&& variable["value"]
.as_str()
.is_some_and(|value| value.contains("Artifact"))
}));
assert!(locals.iter().any(|variable| {
variable["name"] == "unavailable-local-diagnostic"
&& variable["type"] == "unavailable-local"
}));
}
#[test]
fn source_locals_infer_task_with_arg_values_from_runtime_state() {
let project = std::env::temp_dir().join(format!(
"clusterflux-dap-task-with-arg-{}",
std::process::id()
));
let src = project.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("main.rs"),
r#"use clusterflux::{Artifact, EnvRef, SourceSnapshot};
async fn build_release() -> Result<(), clusterflux::TaskArgError> {
let source = prepare_source_snapshot();
let compile = clusterflux::spawn::task_with_arg(source.clone(), compile_linux)
.name("compile linux")
.env(linux_command_env())
.start()
.await?;
let compile_thread = compile.virtual_thread_id();
let linux_artifact = compile.join().await;
let package = clusterflux::spawn::task_with_arg(vec![linux_artifact.clone()], package_release)
.name("package release")
.env(coordinator_env())
.start()
.await?;
let package_thread = package.virtual_thread_id();
let release_artifact = package.join().await;
Ok(())
}
fn prepare_source_snapshot() -> SourceSnapshot {
SourceSnapshot {
digest: "source://coordinator/quick-test-checkout".to_owned(),
}
}
fn linux_command_env() -> EnvRef {
clusterflux::env!("linux-command")
}
fn coordinator_env() -> EnvRef {
clusterflux::env!("coordinator")
}
fn compile_linux(source: SourceSnapshot) -> Artifact {
Artifact { id: source.digest }
}
fn package_release(inputs: Vec<Artifact>) -> Artifact {
inputs.into_iter().next().unwrap()
}
"#,
)
.unwrap();
let mut state = AdapterState::default();
state.project = project.to_string_lossy().into_owned();
state.source_path = "src/main.rs".to_owned();
state.threads.get_mut(&MAIN_THREAD).unwrap().line = 23;
let thread = state.threads[&MAIN_THREAD].clone();
let locals = variables_response(&state, thread.locals_ref);
let locals = locals["variables"].as_array().unwrap();
assert!(locals.iter().any(|variable| {
variable["name"] == "source"
&& variable["value"]
.as_str()
.is_some_and(|value| value.contains("source://coordinator/quick-test-checkout"))
}));
assert!(locals.iter().any(|variable| {
variable["name"] == "compile"
&& variable["value"].as_str().is_some_and(|value| {
value.contains("TaskHandle")
&& value.contains("compile-linux")
&& value.contains("virtual_thread_id = 2")
&& value.contains("linux-command")
})
}));
assert!(locals
.iter()
.any(|variable| variable["name"] == "compile_thread" && variable["value"] == "2"));
assert!(locals.iter().any(|variable| {
variable["name"] == "linux_artifact"
&& variable["value"]
.as_str()
.is_some_and(|value| value.contains("Artifact"))
}));
assert!(locals.iter().any(|variable| {
variable["name"] == "package"
&& variable["value"].as_str().is_some_and(|value| {
value.contains("TaskHandle")
&& value.contains("package-release")
&& value.contains("virtual_thread_id = 4")
&& value.contains("coordinator")
})
}));
assert!(locals
.iter()
.any(|variable| variable["name"] == "package_thread" && variable["value"] == "4"));
assert!(locals.iter().any(|variable| {
variable["name"] == "release_artifact"
&& variable["value"]
.as_str()
.is_some_and(|value| value.contains("Artifact"))
}));
let _ = fs::remove_dir_all(project);
}
#[test]
fn wasm_frame_locals_expose_only_values_from_the_node_snapshot() {
let mut state = AdapterState::default();
state.project = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/launch-build-demo")
.to_string_lossy()
.into_owned();
state.source_path = "src/build.rs".to_owned();
let source = fs::read_to_string(Path::new(&state.project).join(&state.source_path)).unwrap();
state.threads.get_mut(&MAIN_THREAD).unwrap().line = source
.lines()
.position(|line| line.contains("input + 1"))
.map(|line| line as i64 + 1)
.expect("task_add_one source line");
state
.threads
.get_mut(&MAIN_THREAD)
.unwrap()
.wasm_local_values = vec![("wasm_local_0".to_owned(), "I32(41)".to_owned())];
let thread = state.threads[&MAIN_THREAD].clone();
let locals = variables_response(&state, thread.wasm_locals_ref);
let locals = locals["variables"].as_array().unwrap();
assert!(locals.iter().any(|variable| {
variable["name"] == "wasm_local_0"
&& variable["type"] == "wasm-frame-local"
&& variable["value"]
.as_str()
.is_some_and(|value| value.contains("41"))
}));
assert!(!locals
.iter()
.any(|variable| variable["name"] == "wasm-local-diagnostic"));
}
#[test]
fn detects_current_failed_attempt_awaiting_operator_action() {
let snapshots = json!({
"snapshots": [
{
"task": "task-stale",
"attempt_id": "attempt-stale",
"state": "failed_awaiting_action",
"current": false
},
{
"task": "task-current",
"attempt_id": "attempt-current",
"state": "failed_awaiting_action",
"current": true
}
]
});
assert_eq!(
runtime_client::failed_awaiting_action_snapshot(&snapshots),
Some(("task-current", "attempt-current"))
);
}

View file

@ -0,0 +1,764 @@
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()
}

View file

@ -0,0 +1,89 @@
use std::fs;
use std::path::Path;
use anyhow::Result;
use serde_json::json;
use crate::virtual_model::{AdapterState, RuntimeLaunchRecord};
pub(crate) fn normalize_coordinator_endpoint(endpoint: &str) -> String {
endpoint.trim().trim_end_matches('/').to_owned()
}
pub(crate) fn write_view_state(state: &AdapterState, record: &RuntimeLaunchRecord) -> Result<()> {
let dir = Path::new(&state.project).join(".clusterflux");
fs::create_dir_all(&dir)?;
let node_status = if record.placed_task_launched {
if record.status_code == Some(0) {
"completed"
} else {
"failed"
}
} else {
"not-required-yet"
};
let process_status = if record.placed_task_launched {
if record.status_code == Some(0) {
"completed"
} else {
"failed"
}
} else {
"running"
};
let log_message = if record.placed_task_launched {
"node task output was staged as a VFS artifact"
} else {
"coordinator-side virtual process started; capability task not launched"
};
let artifact_status = if record.placed_task_launched {
"best-effort retained on the attached node"
} else {
"not produced until a capability task is placed"
};
let view_state = json!({
"nodes": [{
"id": record.node,
"status": node_status,
"capabilities": format!("connected to {}", record.coordinator),
}],
"processes": [{
"id": state.process.as_str(),
"status": process_status,
"entry": state.entry,
}],
"logs": [{
"task": "compile-linux",
"message": log_message,
"bytes": format!("stdout={} stderr={}", record.stdout_bytes, record.stderr_bytes),
}],
"artifacts": [{
"path": record.artifact_path.clone().unwrap_or_else(|| "/vfs/artifacts/dap-output.txt".to_owned()),
"status": artifact_status,
"size": "reported by node",
}],
"inspector": [
{
"label": "Runtime backend",
"value": state.runtime_backend.description(),
},
{
"label": "Coordinator",
"value": record.coordinator,
},
{
"label": "Node report",
"value": record.node_report.to_string(),
},
{
"label": "Task events",
"value": record.task_events.to_string(),
}
]
});
fs::write(
dir.join("views.json"),
serde_json::to_string_pretty(&view_state)?,
)?;
Ok(())
}

File diff suppressed because it is too large Load diff