use std::collections::BTreeMap; use std::path::Path; use anyhow::{anyhow, Result}; use clusterflux_core::{ BundleDebugProbe, DebugRuntimeState, Digest, ProcessId, ProjectId, TaskDefinitionId, TaskInstanceId, TenantId, UserId, }; use serde_json::Value; use crate::demo_backend::{launch_threads, LINUX_THREAD, MAIN_THREAD}; #[derive(Clone)] pub(crate) struct VirtualThread { pub(crate) id: i64, pub(crate) frame_id: i64, pub(crate) locals_ref: i64, pub(crate) wasm_locals_ref: i64, pub(crate) args_ref: i64, pub(crate) runtime_ref: i64, pub(crate) output_ref: i64, pub(crate) target_ref: i64, pub(crate) vfs_ref: i64, pub(crate) command_ref: i64, pub(crate) task: TaskInstanceId, pub(crate) attempt_id: String, pub(crate) task_definition: TaskDefinitionId, pub(crate) name: String, pub(crate) line: i64, pub(crate) state: DebugRuntimeState, pub(crate) freeze_supported: bool, pub(crate) recent_output: Vec, pub(crate) stdout_bytes: u64, pub(crate) stderr_bytes: u64, pub(crate) stdout_tail: String, pub(crate) stderr_tail: String, pub(crate) stdout_truncated: bool, pub(crate) stderr_truncated: bool, pub(crate) runtime_stack_frames: Vec, pub(crate) wasm_local_values: Vec<(String, String)>, pub(crate) runtime_task_args: Vec<(String, String)>, pub(crate) runtime_handles: Vec<(String, String)>, pub(crate) runtime_command_status: Option, pub(crate) runtime_node: Option, pub(crate) runtime_environment: Option, pub(crate) runtime_vfs_checkpoint: Option, pub(crate) restart_compatible: Option, } #[derive(Clone)] pub(crate) struct AdapterState { pub(crate) process: ProcessId, pub(crate) epoch: u64, pub(crate) entry: String, pub(crate) project: String, pub(crate) source_path: String, pub(crate) runtime_backend: RuntimeBackend, pub(crate) coordinator_endpoint: String, pub(crate) tenant: TenantId, pub(crate) project_id: ProjectId, pub(crate) actor_user: UserId, pub(crate) client_session_secret: Option, pub(crate) session_mode: DapSessionMode, pub(crate) restart_existing: bool, pub(crate) command_status: String, pub(crate) last_task_failed: bool, pub(crate) runtime_artifact_path: Option, pub(crate) runtime_event_count: usize, pub(crate) coordinator_debug_epoch: Option, pub(crate) debug_all_threads_stopped: bool, pub(crate) stopped_task: Option, pub(crate) stopped_probe_symbol: Option, pub(crate) debug_probes: Vec, pub(crate) breakpoints: Vec, pub(crate) threads: BTreeMap, } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum RuntimeBackend { Simulated, LocalServices, LiveServices, } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum DapSessionMode { Launch, Attach, } impl RuntimeBackend { pub(crate) fn description(&self) -> &'static str { match self { RuntimeBackend::Simulated => "simulated debug adapter state", RuntimeBackend::LocalServices => "local services", RuntimeBackend::LiveServices => "live services", } } } impl Default for AdapterState { fn default() -> Self { let entry = "build".to_owned(); let project = ".".to_owned(); Self { process: process_id(&project, &entry), epoch: 0, threads: launch_threads(&entry), entry, project, source_path: "src/build.rs".to_owned(), runtime_backend: RuntimeBackend::Simulated, coordinator_endpoint: "https://clusterflux.michelpaulissen.com".to_owned(), tenant: TenantId::from("tenant"), project_id: ProjectId::from("project"), actor_user: UserId::from("dap"), client_session_secret: None, session_mode: DapSessionMode::Launch, restart_existing: false, command_status: "waiting for launch".to_owned(), last_task_failed: false, runtime_artifact_path: None, runtime_event_count: 0, coordinator_debug_epoch: None, debug_all_threads_stopped: true, stopped_task: None, stopped_probe_symbol: None, debug_probes: Vec::new(), breakpoints: Vec::new(), } } } impl AdapterState { pub(crate) fn requested_probe_symbols(&self) -> Vec { let mut symbols = self .breakpoints .iter() .filter_map(|line| { self.debug_probes.iter().find(|probe| { probe.source_path == self.source_path && *line >= i64::from(probe.line_start) && *line <= i64::from(probe.line_end) }) }) .map(|probe| format!("clusterflux.probe.{}", probe.function)) .collect::>(); symbols.sort(); symbols.dedup(); symbols } pub(crate) fn configure_launch( &mut self, project: String, entry: String, runtime_backend: RuntimeBackend, coordinator_endpoint: Option, source_path: Option, ) -> Result<()> { self.process = process_id(&project, &entry); self.entry = entry; self.source_path = source_path.unwrap_or_else(|| crate::source::infer_source_path(&project)); self.project = project; self.runtime_backend = runtime_backend; let project_scope = load_project_scope(&self.project); if self.runtime_backend == RuntimeBackend::LiveServices { let session = load_client_session(&self.project).ok_or_else(|| { anyhow!( "no authenticated CLI session was found for this workspace; run `clusterflux login --browser` from {}", self.project ) })?; if let Some(configured) = project_scope.as_ref() { let mismatches = scope_mismatches(configured, &session); if !mismatches.is_empty() { return Err(anyhow!( "the stored CLI session does not match this workspace ({mismatches}); run `clusterflux login --browser` from {}", self.project )); } } if let Some(requested) = coordinator_endpoint.as_deref() { if crate::view_state::normalize_coordinator_endpoint(requested) != crate::view_state::normalize_coordinator_endpoint(&session.coordinator) { return Err(anyhow!( "the debug coordinator does not match the authenticated CLI session coordinator" )); } } self.coordinator_endpoint = crate::view_state::normalize_coordinator_endpoint(&session.coordinator); self.tenant = TenantId::new(session.tenant); self.project_id = ProjectId::new(session.project); self.actor_user = UserId::new(session.user); self.client_session_secret = Some(session.session_secret); } else { self.coordinator_endpoint = crate::view_state::normalize_coordinator_endpoint( coordinator_endpoint.as_deref().unwrap_or("127.0.0.1:0"), ); if let Some(scope) = project_scope { self.tenant = TenantId::new(scope.tenant); self.project_id = ProjectId::new(scope.project); self.actor_user = UserId::new(scope.user); } self.client_session_secret = None; } self.session_mode = DapSessionMode::Launch; self.restart_existing = false; self.command_status = "waiting for configurationDone".to_owned(); self.last_task_failed = false; self.runtime_artifact_path = None; self.runtime_event_count = 0; self.coordinator_debug_epoch = None; self.debug_all_threads_stopped = true; self.stopped_task = None; self.stopped_probe_symbol = None; self.debug_probes = crate::breakpoints::load_bundle_debug_probes(&self.project, &self.source_path); self.epoch = 0; self.breakpoints.clear(); self.threads = if self.runtime_backend == RuntimeBackend::Simulated { launch_threads(&self.entry) } else { BTreeMap::new() }; Ok(()) } pub(crate) fn apply_runtime_record(&mut self, record: RuntimeLaunchRecord) { self.coordinator_endpoint = record.coordinator.clone(); if self.runtime_backend != RuntimeBackend::Simulated { if let Some(snapshots) = record.node_report.get("task_snapshots") { self.threads = coordinator_threads_from_snapshots( &self.entry, snapshots, record.node_report.get("process_status"), ); } } if !record.placed_task_launched { self.command_status = format!( "virtual process started through {}; no runtime task event observed yet", self.runtime_backend.description() ); self.last_task_failed = false; self.runtime_artifact_path = None; self.runtime_event_count = record.event_count; if let Some(thread) = self.threads.get_mut(&MAIN_THREAD) { thread.recent_output.push(format!( "coordinator-side process started through {}", self.runtime_backend.description() )); thread .recent_output .push("an attached worker will be required when the process reaches work that must be placed on a node".to_owned()); } let _ = crate::view_state::write_view_state(self, &record); return; } let task_failed = record.status_code.is_some_and(|status| status != 0); self.command_status = if record.debug_epoch.is_some() { format!( "{} through {} at executing Wasm probe {} with debug epoch {}", if record.all_participants_frozen { "fully frozen" } else { "partially frozen" }, self.runtime_backend.description(), record.stopped_probe_symbol.as_deref().unwrap_or("unknown"), record.debug_epoch.unwrap_or_default() ) } else if let Some(status) = record.status_code { format!( "{} through {} with status {status}", if task_failed { "failed" } else { "completed" }, self.runtime_backend.description() ) } else { format!("running through {}", self.runtime_backend.description()) }; self.last_task_failed = task_failed; self.runtime_artifact_path = record.artifact_path.clone(); self.runtime_event_count = record.event_count; if record.debug_epoch.is_some() { self.debug_all_threads_stopped = record.all_participants_frozen; self.epoch = record.debug_epoch.unwrap_or(self.epoch); self.coordinator_debug_epoch = record.debug_epoch; self.stopped_task = record.stopped_task.as_deref().map(TaskInstanceId::new); self.stopped_probe_symbol = record.stopped_probe_symbol.clone(); if let Some(acknowledgements) = record .node_report .pointer("/debug_epoch/acknowledgements") .and_then(Value::as_array) { for acknowledgement in acknowledgements { let Some(task) = acknowledgement.get("task").and_then(Value::as_str) else { continue; }; let Some(task_definition) = acknowledgement .get("task_definition") .and_then(Value::as_str) else { continue; }; let coordinator_main = acknowledgement.get("node").and_then(Value::as_str) == Some("coordinator-main"); let thread_id = if coordinator_main { MAIN_THREAD } else { self.threads .values() .find(|thread| thread.task.as_str() == task) .map(|thread| thread.id) .unwrap_or_else(|| self.insert_runtime_thread(task, task_definition)) }; let thread = self .threads .get_mut(&thread_id) .expect("runtime thread was found or inserted"); thread.task = TaskInstanceId::new(task); thread.task_definition = TaskDefinitionId::new(task_definition); thread.runtime_stack_frames = acknowledgement .get("stack_frames") .and_then(Value::as_array) .into_iter() .flatten() .filter_map(Value::as_str) .map(str::to_owned) .collect(); thread.wasm_local_values = value_string_pairs(acknowledgement.get("local_values")); thread.runtime_task_args = value_string_pairs(acknowledgement.get("task_args")); thread.runtime_handles = value_string_pairs(acknowledgement.get("handles")); thread.runtime_command_status = acknowledgement .get("command_status") .and_then(Value::as_str) .map(str::to_owned); thread.state = match acknowledgement.get("state").and_then(Value::as_str) { Some("frozen") => DebugRuntimeState::Frozen, Some("failed") => DebugRuntimeState::Failed( acknowledgement .get("message") .and_then(Value::as_str) .unwrap_or("participant failed to freeze") .to_owned(), ), _ => thread.state.clone(), }; } } if let Some(thread) = self.threads.get_mut(&MAIN_THREAD) { thread.recent_output.push(format!( "executing task {} reported the breakpoint hit", record.stopped_task.as_deref().unwrap_or("unknown") )); } } if let Some(terminal_event) = record.node_report.get("terminal_event") { let terminal_task = terminal_event .get("task") .and_then(Value::as_str) .unwrap_or(self.entry.as_str()) .to_owned(); let terminal_task_definition = terminal_event .get("task_definition") .and_then(Value::as_str) .unwrap_or(self.entry.as_str()) .to_owned(); let terminal_thread_id = self .threads .values() .find(|thread| thread.task.as_str() == terminal_task.as_str()) .map(|thread| thread.id) .unwrap_or_else(|| { self.insert_runtime_thread(&terminal_task, &terminal_task_definition) }); if let Some(thread) = self.threads.get_mut(&terminal_thread_id) { thread.stdout_bytes = record.stdout_bytes; thread.stderr_bytes = record.stderr_bytes; thread.stdout_tail = record.stdout_tail.clone(); thread.stderr_tail = record.stderr_tail.clone(); thread.stdout_truncated = record.stdout_truncated; thread.stderr_truncated = record.stderr_truncated; thread.recent_output.push(format!( "{} task {} with {} stdout bytes and {} stderr bytes", terminal_event .get("executor") .and_then(Value::as_str) .unwrap_or("runtime"), if task_failed { "failed" } else { "completed" }, record.stdout_bytes, record.stderr_bytes )); thread.recent_output.push(format!( "coordinator recorded {} task event(s)", record.event_count )); thread.state = if task_failed { DebugRuntimeState::Failed(format!( "local services task exited with status {:?}", record.status_code )) } else { DebugRuntimeState::Completed }; } } let _ = crate::view_state::write_view_state(self, &record); } fn insert_runtime_thread(&mut self, task: &str, task_definition: &str) -> i64 { let id = self .threads .keys() .next_back() .copied() .unwrap_or(MAIN_THREAD) + 1; let line = self .debug_probes .iter() .find(|probe| { probe.task.as_str() == task_definition || probe.function == task_definition }) .map(|probe| i64::from(probe.line_start)) .unwrap_or(1); let definition_name = task_definition.replace(['_', '-'], " "); let name = if task == task_definition { definition_name } else { format!("{definition_name} ({task})") }; self.threads.insert( id, 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::new(task), attempt_id: "unknown-attempt".to_owned(), task_definition: TaskDefinitionId::new(task_definition), name, line, state: DebugRuntimeState::Running, freeze_supported: true, recent_output: vec!["runtime participant reported by the node".to_owned()], 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: None, runtime_environment: None, runtime_vfs_checkpoint: None, restart_compatible: None, }, ); id } pub(crate) fn apply_attach_record(&mut self, record: RuntimeLaunchRecord) { self.coordinator_endpoint = record.coordinator.clone(); self.command_status = format!( "attached to existing virtual process through {}; coordinator task event(s): {}", self.runtime_backend.description(), record.event_count ); self.last_task_failed = false; self.runtime_artifact_path = None; self.runtime_event_count = record.event_count; self.threads = coordinator_threads_from_snapshots( &self.entry, record .node_report .get("task_snapshots") .unwrap_or(&Value::Null), record.node_report.get("process_status"), ); if let Some(thread) = self.threads.get_mut(&MAIN_THREAD) { thread.recent_output.push(format!( "attached to existing virtual process through {}", self.runtime_backend.description() )); thread.recent_output.push(format!( "coordinator returned {} task event(s) for this process", record.event_count )); } let _ = crate::view_state::write_view_state(self, &record); } } fn value_string_pairs(value: Option<&Value>) -> Vec<(String, String)> { value .and_then(Value::as_array) .into_iter() .flatten() .filter_map(|pair| { let pair = pair.as_array()?; Some(( pair.first()?.as_str()?.to_owned(), pair.get(1)?.as_str()?.to_owned(), )) }) .collect() } #[derive(Clone, Debug)] struct ProjectScope { coordinator: Option, tenant: String, project: String, user: String, } #[derive(Clone, Debug)] struct ClientSession { coordinator: String, tenant: String, project: String, user: String, session_secret: String, } fn load_project_scope(project: &str) -> Option { for ancestor in Path::new(project).ancestors() { let file = ancestor.join(".clusterflux/project.json"); let Ok(bytes) = std::fs::read(file) else { continue; }; let Ok(config) = serde_json::from_slice::(&bytes) else { continue; }; return Some(ProjectScope { coordinator: config .get("coordinator") .and_then(Value::as_str) .map(str::to_owned), tenant: config.get("tenant")?.as_str()?.to_owned(), project: config.get("project")?.as_str()?.to_owned(), user: config.get("user")?.as_str()?.to_owned(), }); } None } fn load_client_session(project: &str) -> Option { for ancestor in Path::new(project).ancestors() { let file = ancestor.join(".clusterflux/session.json"); let Ok(bytes) = std::fs::read(file) else { continue; }; let Ok(session) = serde_json::from_slice::(&bytes) else { continue; }; let secret = session .get("session_secret") .and_then(Value::as_str) .filter(|secret| !secret.trim().is_empty())?; return Some(ClientSession { coordinator: session.get("coordinator")?.as_str()?.to_owned(), tenant: session.get("tenant")?.as_str()?.to_owned(), project: session.get("project")?.as_str()?.to_owned(), user: session.get("user")?.as_str()?.to_owned(), session_secret: secret.to_owned(), }); } None } fn scope_mismatches(project: &ProjectScope, session: &ClientSession) -> String { let mut fields = Vec::new(); if project.coordinator.as_deref().is_some_and(|coordinator| { crate::view_state::normalize_coordinator_endpoint(coordinator) != crate::view_state::normalize_coordinator_endpoint(&session.coordinator) }) { fields.push("coordinator"); } if project.tenant != session.tenant { fields.push("tenant"); } if project.project != session.project { fields.push("project"); } if project.user != session.user { fields.push("user"); } fields.join(", ") } #[derive(Clone, Debug)] pub(crate) struct RuntimeLaunchRecord { pub(crate) coordinator: String, pub(crate) node: String, pub(crate) node_report: Value, pub(crate) task_events: Value, pub(crate) placed_task_launched: bool, pub(crate) status_code: Option, pub(crate) stdout_bytes: u64, pub(crate) stderr_bytes: u64, pub(crate) stdout_tail: String, pub(crate) stderr_tail: String, pub(crate) stdout_truncated: bool, pub(crate) stderr_truncated: bool, pub(crate) artifact_path: Option, pub(crate) event_count: usize, pub(crate) debug_epoch: Option, pub(crate) stopped_task: Option, pub(crate) stopped_probe_symbol: Option, pub(crate) all_participants_frozen: bool, } pub(crate) fn process_id(project: &str, entry: &str) -> ProcessId { let digest = Digest::from_parts([ b"dap-process:v1".as_slice(), project.as_bytes(), entry.as_bytes(), ]); let suffix = digest .as_str() .trim_start_matches("sha256:") .chars() .take(12) .collect::(); ProcessId::new(format!("vp-{suffix}")) } fn coordinator_main_thread(entry: &str, task: &str, task_definition: &str) -> VirtualThread { VirtualThread { id: MAIN_THREAD, frame_id: 1_001, locals_ref: 2_001, wasm_locals_ref: 2_501, args_ref: 3_001, runtime_ref: 3_501, output_ref: 4_001, target_ref: 4_501, vfs_ref: 5_001, command_ref: 5_501, task: TaskInstanceId::from(task), attempt_id: "main:unknown".to_owned(), task_definition: TaskDefinitionId::from(task_definition), name: format!("{entry} coordinator main ({task})"), line: 0, state: DebugRuntimeState::Running, freeze_supported: true, recent_output: Vec::new(), 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("coordinator-main".to_owned()), runtime_environment: None, runtime_vfs_checkpoint: None, restart_compatible: None, } } #[cfg(test)] #[allow(dead_code)] fn coordinator_threads_from_events( entry: &str, task_events: &Value, process_status: Option<&Value>, ) -> BTreeMap { let mut threads = BTreeMap::new(); let mut main = launch_threads(entry) .remove(&MAIN_THREAD) .expect("launch thread model should include main"); if let Some(status) = process_status { if let Some(task) = status.get("main_task_instance").and_then(Value::as_str) { main.task = TaskInstanceId::from(task); } if let Some(definition) = status.get("main_task_definition").and_then(Value::as_str) { main.task_definition = TaskDefinitionId::from(definition); } main.name = format!("{entry} coordinator main ({})", main.task); main.state = if status .get("main_debug_epoch") .and_then(Value::as_u64) .is_some() { DebugRuntimeState::Frozen } else { DebugRuntimeState::Running }; main.recent_output = vec![format!( "coordinator reports main state {}", status .get("main_state") .and_then(Value::as_str) .unwrap_or("running") )]; } threads.insert(MAIN_THREAD, main); let Some(events) = task_events.get("events").and_then(Value::as_array) else { return threads; }; let mut next_worker_thread = LINUX_THREAD; for (index, event) in events.iter().enumerate() { let coordinator_main = event.get("executor").and_then(Value::as_str) == Some("coordinator_main"); let id = if coordinator_main { MAIN_THREAD } else { let id = next_worker_thread; next_worker_thread += 1; id }; let Some(mut thread) = coordinator_event_thread(id, index, event) else { continue; }; if coordinator_main { thread.name = format!("{entry} coordinator main ({})", thread.task); thread.recent_output.push( "terminal state was recorded by the coordinator-hosted main runtime".to_owned(), ); } threads.insert(id, thread); } threads } fn coordinator_threads_from_snapshots( entry: &str, task_snapshots: &Value, process_status: Option<&Value>, ) -> BTreeMap { let mut threads = BTreeMap::new(); if let Some(status) = process_status { if let Some(task) = status.get("main_task_instance").and_then(Value::as_str) { let task_definition = status .get("main_task_definition") .and_then(Value::as_str) .unwrap_or(entry); let mut main = coordinator_main_thread(entry, task, task_definition); main.attempt_id = format!( "main:{}", status .get("coordinator_epoch") .and_then(Value::as_u64) .unwrap_or_default() ); main.line = 0; main.runtime_stack_frames.clear(); main.state = if status .get("main_debug_epoch") .and_then(Value::as_u64) .is_some() { DebugRuntimeState::Frozen } else { DebugRuntimeState::Running }; main.runtime_node = Some("coordinator-main".to_owned()); main.runtime_environment = Some("coordinator-capless".to_owned()); main.runtime_vfs_checkpoint = status .get("coordinator_epoch") .and_then(Value::as_u64) .map(|epoch| format!("vfs-epoch:{epoch}")); main.restart_compatible = Some(false); main.recent_output.clear(); threads.insert(MAIN_THREAD, main); } } let Some(snapshots) = task_snapshots.get("snapshots").and_then(Value::as_array) else { return threads; }; let mut next_id = if threads.contains_key(&MAIN_THREAD) { LINUX_THREAD } else { MAIN_THREAD }; for snapshot in snapshots { if snapshot.get("current").and_then(Value::as_bool) != Some(true) || !matches!( snapshot.get("state").and_then(Value::as_str), Some("queued" | "running" | "failed_awaiting_action") ) { continue; } let Some(task) = snapshot.get("task").and_then(Value::as_str) else { continue; }; let Some(task_definition) = snapshot.get("task_definition").and_then(Value::as_str) else { continue; }; let attempt_id = snapshot .get("attempt_id") .and_then(Value::as_str) .unwrap_or("unknown-attempt") .to_owned(); let state = match snapshot.get("state").and_then(Value::as_str) { Some("queued" | "running") => DebugRuntimeState::Running, Some("failed_awaiting_action") => { DebugRuntimeState::Failed("failed awaiting operator action".to_owned()) } _ => continue, }; let argument_summary = snapshot .get("argument_summary") .and_then(Value::as_array) .into_iter() .flatten() .enumerate() .filter_map(|(index, value)| Some((format!("arg_{index}"), value.as_str()?.to_owned()))) .collect(); let handle_summary = snapshot .get("handle_summary") .and_then(Value::as_array) .into_iter() .flatten() .enumerate() .filter_map(|(index, value)| { Some((format!("handle_{index}"), value.as_str()?.to_owned())) }) .collect(); let display_name = snapshot .get("display_name") .and_then(Value::as_str) .unwrap_or(task_definition); let short_attempt = attempt_id.chars().take(16).collect::(); let id = next_id; next_id += 1; threads.insert( id, 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, task_definition: TaskDefinitionId::from(task_definition), name: format!("{display_name} ({task}, attempt {short_attempt})"), line: snapshot .get("source_line") .and_then(Value::as_i64) .unwrap_or(0), state, freeze_supported: true, recent_output: Vec::new(), 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: argument_summary, runtime_handles: handle_summary, runtime_command_status: snapshot .get("command_state") .and_then(Value::as_str) .map(str::to_owned), runtime_node: snapshot .get("node") .and_then(Value::as_str) .map(str::to_owned), runtime_environment: snapshot .get("environment_id") .and_then(Value::as_str) .map(str::to_owned), runtime_vfs_checkpoint: snapshot .get("vfs_checkpoint") .and_then(Value::as_str) .map(str::to_owned), restart_compatible: snapshot.get("restart_compatible").and_then(Value::as_bool), }, ); } threads } #[cfg(test)] #[allow(dead_code)] fn coordinator_event_thread(id: i64, _event_index: usize, event: &Value) -> Option { let task = event.get("task").and_then(Value::as_str)?; let task_definition = event.get("task_definition").and_then(Value::as_str)?; let definition_name = task_definition.replace(['_', '-'], " "); let name = if task == task_definition { definition_name } else { format!("{definition_name} ({task})") }; let stdout_tail = event .get("stdout_tail") .and_then(Value::as_str) .unwrap_or_default() .to_owned(); let stderr_tail = event .get("stderr_tail") .and_then(Value::as_str) .unwrap_or_default() .to_owned(); let stdout_bytes = event .get("stdout_bytes") .and_then(Value::as_u64) .unwrap_or(0); let stderr_bytes = event .get("stderr_bytes") .and_then(Value::as_u64) .unwrap_or(0); let status_code = event.get("status_code").and_then(Value::as_i64); let state = match event.get("terminal_state").and_then(Value::as_str) { Some("completed") => DebugRuntimeState::Completed, Some("cancelled") => DebugRuntimeState::Failed(format!("task {task} was cancelled")), Some("failed") => DebugRuntimeState::Failed(format!("task {task} failed")), _ if status_code == Some(0) => DebugRuntimeState::Completed, _ => { DebugRuntimeState::Failed(format!("task {task} completed with status {status_code:?}")) } }; Some(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: event .get("attempt_id") .and_then(Value::as_str) .unwrap_or("unknown-attempt") .to_owned(), task_definition: TaskDefinitionId::from(task_definition), name, line: 0, state, freeze_supported: true, recent_output: vec![format!( "coordinator task event from {} {}", event .get("executor") .and_then(Value::as_str) .unwrap_or("node"), event .get("node") .and_then(Value::as_str) .unwrap_or("unknown") )], stdout_bytes, stderr_bytes, stdout_tail, stderr_tail, stdout_truncated: event .get("stdout_truncated") .and_then(Value::as_bool) .unwrap_or(false), stderr_truncated: event .get("stderr_truncated") .and_then(Value::as_bool) .unwrap_or(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: event.get("node").and_then(Value::as_str).map(str::to_owned), runtime_environment: None, runtime_vfs_checkpoint: None, restart_compatible: None, }) }