use std::io::{self, BufReader}; use anyhow::Result; use disasmer_core::DebugRuntimeState; use serde_json::{json, Value}; use crate::breakpoints::{ freeze_all, freeze_all_at_line, next_breakpoint_after, position_confirmed_breakpoint_stop, request_thread, request_thread_id, resolve_breakpoints_for_source, restart_requires_whole_process, simulated_freeze_failure_thread, step_thread, stopped_thread_for_breakpoint, }; use crate::dap_protocol::{initialize_capabilities, read_message, DapWriter}; use crate::demo_backend::{ is_explicit_demo_backend, start_simulated_backend, LINUX_THREAD, MAIN_THREAD, }; use crate::runtime_client::{ attach_services_runtime, create_debug_epoch, relaunch_services_main_runtime, restart_task, resume_debug_epoch, run_live_services_runtime, run_local_services_runtime, wait_for_debug_epoch_frozen, wait_for_debug_epoch_resumed, wait_for_services_runtime_outcome, RuntimeContinuationOutcome, }; use crate::variables::variables_response; use crate::virtual_model::{AdapterState, DapSessionMode, RuntimeBackend}; pub(crate) fn run_adapter() -> Result<()> { let mut writer = DapWriter::new(); let mut reader = BufReader::new(io::stdin()); let mut state = AdapterState::default(); let mut _local_runtime_session = None; while let Some(request) = read_message(&mut reader)? { let command = request .get("command") .and_then(Value::as_str) .unwrap_or_default(); match command { "initialize" => writer.response(&request, true, initialize_capabilities())?, "launch" | "attach" => { let args = request .get("arguments") .cloned() .unwrap_or_else(|| json!({})); state.entry = args .get("entry") .and_then(Value::as_str) .unwrap_or("build") .to_owned(); let project = args .get("project") .and_then(Value::as_str) .unwrap_or(".") .to_owned(); let runtime_backend = match runtime_backend_from_launch_arg( args.get("runtimeBackend").and_then(Value::as_str), ) { Ok(runtime_backend) => runtime_backend, Err(message) => { writer.error_response(&request, message)?; continue; } }; let coordinator_endpoint = args .get("coordinatorEndpoint") .and_then(Value::as_str) .map(str::to_owned); let source_path = args .get("sourcePath") .and_then(Value::as_str) .map(str::to_owned); if let Err(err) = state.configure_launch( project, state.entry.clone(), runtime_backend, coordinator_endpoint, source_path, ) { writer.error_response(&request, err.to_string())?; continue; } state.restart_existing = args .get("restartExisting") .and_then(Value::as_bool) .unwrap_or(false); if let Some(process_id) = args.get("processId").and_then(Value::as_str) { state.process = disasmer_core::ProcessId::new(process_id); } if command == "attach" { state.session_mode = DapSessionMode::Attach; state.command_status = format!("attached to existing virtual process {}", state.process); } writer.response(&request, true, json!({}))?; writer.event("initialized", json!({}))?; } "setBreakpoints" => { let requested_source_path = request .get("arguments") .and_then(|value| value.get("source")) .and_then(|value| value.get("path")) .and_then(Value::as_str); let requested_lines = request .get("arguments") .and_then(|value| value.get("breakpoints")) .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(|item| item.get("line").and_then(Value::as_i64)) .collect::>() }) .unwrap_or_default(); let response = resolve_breakpoints_for_source( &mut state, requested_source_path, requested_lines, ) .iter() .map(|breakpoint| breakpoint.to_dap()) .collect::>(); writer.response(&request, true, json!({ "breakpoints": response }))?; } "setExceptionBreakpoints" => { writer.response(&request, true, json!({ "breakpoints": [] }))?; } "configurationDone" => { if state.session_mode == DapSessionMode::Attach { match state.runtime_backend { RuntimeBackend::Simulated => { state.command_status = format!("attached to existing virtual process {}", state.process); } RuntimeBackend::LocalServices | RuntimeBackend::LiveServices => { match attach_services_runtime(&state) { Ok(record) => state.apply_attach_record(record), Err(err) => { writer.error_response( &request, format!("services runtime attach failed: {err:#}"), )?; continue; } } } } } else { match state.runtime_backend { RuntimeBackend::LocalServices => match run_local_services_runtime(&state) { Ok((record, session)) => { state.apply_runtime_record(record); _local_runtime_session = Some(session); } Err(err) => { writer.error_response( &request, format!("local services runtime launch failed: {err:#}"), )?; continue; } }, RuntimeBackend::LiveServices => match run_live_services_runtime(&state) { Ok(record) => { state.apply_runtime_record(record); } Err(err) => { writer.error_response( &request, format!("live services runtime launch failed: {err:#}"), )?; continue; } }, RuntimeBackend::Simulated => { start_simulated_backend(&mut state); } } } writer.response(&request, true, json!({}))?; let session_message = if state.session_mode == DapSessionMode::Attach { format!( "Attached to Disasmer virtual process {} with {:?} runtime\n", state.process, state.runtime_backend ) } else { format!( "Disasmer bundle refreshed for entry `{}`; starting virtual process {} with {:?} runtime\n", state.entry, state.process, state.runtime_backend ) }; writer.output("console", session_message)?; if !state.breakpoints.is_empty() { let stopped_thread = stopped_thread_for_breakpoint(&state); if matches!( state.runtime_backend, RuntimeBackend::LocalServices | RuntimeBackend::LiveServices ) && state.coordinator_debug_epoch.is_some() { position_confirmed_breakpoint_stop(&mut state, stopped_thread); writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Disasmer Debug Epoch all-stop confirmed by every active participant", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; continue; } match freeze_all(&mut state, stopped_thread, None) { Ok(()) => { if let Err(err) = record_coordinator_debug_epoch( &mut state, stopped_thread, "breakpoint", ) { let message = format!("coordinator debug epoch failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; continue; } writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Disasmer Debug Epoch all-stop", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Err(failure) => { let message = failure.message(); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; } } } else { writer.event("terminated", json!({}))?; } } "threads" => { let threads = state .threads .values() .map(|thread| { json!({ "id": thread.id, "name": format!("{}/{}", state.process, thread.name), }) }) .collect::>(); writer.response(&request, true, json!({ "threads": threads }))?; } "stackTrace" => { let thread = request_thread(&request, &state); let source_path = crate::source::stack_source_path(&state); let frame_name = thread .runtime_stack_frames .first() .cloned() .unwrap_or_else(|| format!("{}::run", thread.name)); writer.response( &request, true, json!({ "stackFrames": [{ "id": thread.frame_id, "name": frame_name, "line": thread.line, "column": 1, "source": { "name": crate::source::source_name(&source_path), "path": source_path, "presentationHint": "normal" } }], "totalFrames": 1 }), )?; } "source" => match crate::source::source_response(&state, &request) { Ok(body) => writer.response(&request, true, body)?, Err(err) => writer.error_response(&request, err.to_string())?, }, "scopes" => { let frame_id = request .get("arguments") .and_then(|value| value.get("frameId")) .and_then(Value::as_i64) .unwrap_or(1000 + crate::demo_backend::MAIN_THREAD); let thread = state .threads .values() .find(|thread| thread.frame_id == frame_id) .cloned() .unwrap_or_else(|| state.threads[&crate::demo_backend::MAIN_THREAD].clone()); writer.response( &request, true, json!({ "scopes": [ { "name": "Source Locals", "variablesReference": thread.locals_ref, "expensive": false, "presentationHint": "locals" }, { "name": "Wasm Frame Locals", "variablesReference": thread.wasm_locals_ref, "expensive": false, "presentationHint": "locals" }, { "name": "Task Args and Handles", "variablesReference": thread.args_ref, "expensive": false, "presentationHint": "arguments" }, { "name": "Disasmer Runtime", "variablesReference": thread.runtime_ref, "expensive": false }, { "name": "Recent Output", "variablesReference": thread.output_ref, "expensive": false } ] }), )?; } "variables" => { let reference = request .get("arguments") .and_then(|value| value.get("variablesReference")) .and_then(Value::as_i64) .unwrap_or(0); writer.response(&request, true, variables_response(&state, reference))?; } "evaluate" => { let expression = request .get("arguments") .and_then(|value| value.get("expression")) .and_then(Value::as_str) .unwrap_or_default(); let result = match expression { "virtual_process_id" => state.process.to_string(), "debug_epoch" => state.epoch.to_string(), "command_status" => state.command_status.clone(), _ => "not available".to_owned(), }; writer.response( &request, true, json!({ "result": result, "variablesReference": 0 }), )?; } "pause" => { let stopped_thread = default_thread_id(&state); match freeze_all( &mut state, stopped_thread, simulated_freeze_failure_thread(&request), ) { Ok(()) => { if let Err(err) = record_coordinator_debug_epoch(&mut state, stopped_thread, "pause") { let message = format!("coordinator debug epoch failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; continue; } writer.response(&request, true, json!({}))?; writer.event( "stopped", json!({ "reason": "pause", "description": "Disasmer Debug Epoch pause", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Err(failure) => { let message = failure.message(); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; } } } "continue" => { let thread_id = request_thread_id(&request).unwrap_or_else(|| default_thread_id(&state)); let next_breakpoint = next_breakpoint_after(&state, thread_id); let previous_debug_epoch = state.coordinator_debug_epoch.unwrap_or(state.epoch); if let Err(err) = resume_coordinator_epoch(&mut state) { let message = format!("coordinator debug epoch resume failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; continue; } for thread in state.threads.values_mut() { if thread.state == DebugRuntimeState::Frozen { thread.state = DebugRuntimeState::Running; } } writer.response(&request, true, json!({ "allThreadsContinued": true }))?; writer.event( "continued", json!({ "threadId": thread_id, "allThreadsContinued": true, }), )?; if matches!( state.runtime_backend, RuntimeBackend::LocalServices | RuntimeBackend::LiveServices ) { match wait_for_services_runtime_outcome(&state, previous_debug_epoch) { Ok(RuntimeContinuationOutcome::Breakpoint(record)) => { state.apply_runtime_record(record); let stopped_thread = stopped_thread_for_breakpoint(&state); position_confirmed_breakpoint_stop(&mut state, stopped_thread); writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Disasmer Debug Epoch all-stop confirmed by every active participant", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Ok(RuntimeContinuationOutcome::Terminal(record)) => { state.apply_runtime_record(record); if state.last_task_failed { writer.output("stderr", format!("{}\n", state.command_status))?; } writer.event("terminated", json!({}))?; } Err(err) => { let message = format!("continued runtime observation failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.event( "stopped", json!({ "reason": "exception", "description": message, "threadId": thread_id, "allThreadsStopped": false, }), )?; } } continue; } if let Some((stopped_thread, line)) = next_breakpoint { match freeze_all_at_line(&mut state, stopped_thread, line, None) { Ok(()) => { if let Err(err) = record_coordinator_debug_epoch( &mut state, stopped_thread, "breakpoint", ) { let message = format!("coordinator debug epoch failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; continue; } writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Disasmer Debug Epoch all-stop", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Err(failure) => { let message = failure.message(); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; } } } else { writer.event("terminated", json!({}))?; } } "next" | "stepIn" | "stepOut" => { if state.runtime_backend != RuntimeBackend::Simulated { writer.error_response( &request, "source stepping is not yet available from the executing node; use continue or pause instead of a synthetic step", )?; continue; } let thread_id = request_thread_id(&request).unwrap_or(LINUX_THREAD); let description = match command { "next" => "step over", "stepIn" => "step in", "stepOut" => "step out", _ => "step", }; step_thread(&mut state, thread_id, description); writer.response(&request, true, json!({}))?; writer.event( "stopped", json!({ "reason": "step", "description": format!("Disasmer Debug Epoch {description}"), "threadId": thread_id, "allThreadsStopped": true, }), )?; } "restart" | "restartFrame" => { let thread_id = request_thread_id(&request) .or_else(|| { request .get("arguments") .and_then(|arguments| arguments.get("frameId")) .and_then(Value::as_i64) .and_then(|frame_id| { state .threads .values() .find(|thread| thread.frame_id == frame_id) .map(|thread| thread.id) }) }) .unwrap_or_else(|| default_thread_id(&state)); if restart_requires_whole_process(&request) { let message = format!( "Incompatible source edit requires whole virtual-process restart for {}", state.process ); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; continue; } let restarting_failed_task = state .threads .get(&thread_id) .is_some_and(|thread| matches!(thread.state, DebugRuntimeState::Failed(_))); if state.runtime_backend != RuntimeBackend::Simulated { if thread_id == MAIN_THREAD && restarting_failed_task { match relaunch_services_main_runtime(&state) { Ok(record) => { state.apply_runtime_record(record); let stopped_thread = stopped_thread_for_breakpoint(&state); position_confirmed_breakpoint_stop(&mut state, stopped_thread); writer.response(&request, true, json!({}))?; writer.output( "console", "Rebuilt the current bundle and restarted the failed coordinator main from its entry boundary\n", )?; writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Restarted main from the rebuilt bundle; every active participant confirmed all-stop", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Err(err) => { let message = format!("coordinator main restart failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; } } continue; } match restart_task_through_coordinator(&mut state, thread_id) { Ok(message) => { let previous_debug_epoch = state.coordinator_debug_epoch.unwrap_or(state.epoch); if let Some(thread) = state.threads.get_mut(&thread_id) { thread.state = DebugRuntimeState::Running; thread.recent_output.push(message.clone()); } state.last_task_failed = false; state.command_status = message.clone(); writer.response(&request, true, json!({}))?; writer.output("console", format!("{message}\n"))?; if matches!( state.runtime_backend, RuntimeBackend::LocalServices | RuntimeBackend::LiveServices ) { match wait_for_services_runtime_outcome( &state, previous_debug_epoch, ) { Ok(RuntimeContinuationOutcome::Breakpoint(record)) => { state.apply_runtime_record(record); let stopped_thread = stopped_thread_for_breakpoint(&state); position_confirmed_breakpoint_stop( &mut state, stopped_thread, ); writer.event( "stopped", json!({ "reason": "breakpoint", "description": "Restarted task reached a Wasm probe and every active participant confirmed all-stop", "threadId": stopped_thread, "allThreadsStopped": true, }), )?; } Ok(RuntimeContinuationOutcome::Terminal(record)) => { state.apply_runtime_record(record); writer.event("terminated", json!({}))?; } Err(err) => { writer.event( "stopped", json!({ "reason": "exception", "description": format!("restarted runtime observation failed: {err:#}"), "threadId": thread_id, "allThreadsStopped": false, }), )?; } } } } Err(err) => { let message = format!("coordinator task restart failed: {err:#}"); writer.output("stderr", format!("{message}\n"))?; writer.error_response(&request, message)?; } } continue; } if let Some(thread) = state.threads.get_mut(&thread_id) { thread.state = DebugRuntimeState::Running; thread .recent_output .push("task restarted from VFS checkpoint".to_owned()); } if restarting_failed_task { state.last_task_failed = false; } state.command_status = format!( "{} task restarted from compatible VFS checkpoint", if restarting_failed_task { "failed" } else { "selected" } ); writer.response(&request, true, json!({}))?; writer.output( "console", format!( "Restarted {} task from compatible VFS checkpoint on thread {thread_id}\n", if restarting_failed_task { "failed" } else { "selected" } ), )?; } "disconnect" | "terminate" => { writer.response(&request, true, json!({}))?; writer.event("terminated", json!({}))?; break; } _ => writer.error_response(&request, format!("unsupported DAP command: {command}"))?, } } Ok(()) } fn restart_task_through_coordinator(state: &mut AdapterState, thread_id: i64) -> Result { let task = state .threads .get(&thread_id) .or_else(|| state.threads.get(&default_thread_id(state))) .map(|thread| thread.task.clone()) .ok_or_else(|| anyhow::anyhow!("selected debug thread does not map to a virtual task"))?; let record = restart_task(state, &task)?; if !record.accepted { let mut message = format!( "coordinator refused task restart for `{task}`: {}", record.message ); if record.requires_whole_process_restart { message.push_str("; whole virtual-process restart required"); } if record.active_task { message.push_str("; selected task is still active"); } if record.completed_event_observed { message.push_str("; only terminal task-event metadata is available"); } return Err(anyhow::anyhow!(message)); } if !record.clean_boundary_available { return Err(anyhow::anyhow!( "coordinator accepted task restart for `{task}` without a clean checkpoint boundary" )); } let restarted_task = record.restarted_task_instance.ok_or_else(|| { anyhow::anyhow!("coordinator accepted task restart without a new task-instance ID") })?; if let Some(thread) = state.threads.get_mut(&thread_id) { thread.task = restarted_task.clone(); } state.debug_probes = crate::breakpoints::load_bundle_debug_probes(&state.project, &state.source_path); Ok(format!( "Coordinator restarted task `{task}` as `{restarted_task}` from the rebuilt bundle and a clean runtime checkpoint boundary" )) } fn record_coordinator_debug_epoch( state: &mut AdapterState, stopped_thread: i64, reason: &str, ) -> Result<()> { if state.runtime_backend == RuntimeBackend::Simulated { return Ok(()); } let stopped_task = state .threads .get(&stopped_thread) .map(|thread| thread.task.clone()) .unwrap_or_else(|| state.threads[&default_thread_id(state)].task.clone()); let record = create_debug_epoch(state, &stopped_task, reason)?; let status = wait_for_debug_epoch_frozen(state, record.epoch)?; state.epoch = record.epoch; state.coordinator_debug_epoch = Some(record.epoch); let previous_status = state.command_status.clone(); state.command_status = format!( "{previous_status}; debug epoch {} {} after {}/{} signed participant freeze acknowledgements", status.epoch, status.command, status.acknowledgements.len(), status.expected_tasks ); if let Some(thread) = state.threads.get_mut(&stopped_thread) { thread.recent_output.push(format!( "coordinator debug epoch {} reached all-stop after {}/{} signed participant acknowledgements", status.epoch, status.acknowledgements.len(), status.expected_tasks )); } Ok(()) } fn default_thread_id(state: &AdapterState) -> i64 { if state.runtime_backend == RuntimeBackend::Simulated && state.threads.contains_key(&LINUX_THREAD) { LINUX_THREAD } else { MAIN_THREAD } } fn resume_coordinator_epoch(state: &mut AdapterState) -> Result<()> { let Some(epoch) = state.coordinator_debug_epoch else { return Ok(()); }; if state.runtime_backend == RuntimeBackend::Simulated { return Ok(()); } let record = resume_debug_epoch(state, epoch)?; let status = wait_for_debug_epoch_resumed(state, epoch)?; state.coordinator_debug_epoch = None; state.command_status = format!( "debug epoch {} resumed after {}/{} signed participant acknowledgements", status.epoch, status.acknowledgements.len(), status.expected_tasks ); let status_thread = default_thread_id(state); if let Some(thread) = state.threads.get_mut(&status_thread) { thread.recent_output.push(format!( "coordinator debug epoch {} resumed with {} after {}/{} acknowledgements ({} affected task(s))", status.epoch, status.command, status.acknowledgements.len(), status.expected_tasks, record.affected_tasks )); } Ok(()) } pub(crate) fn runtime_backend_from_launch_arg( value: Option<&str>, ) -> Result { match value { None | Some("local-services") => Ok(RuntimeBackend::LocalServices), Some("live-services") => Ok(RuntimeBackend::LiveServices), Some(value) if is_explicit_demo_backend(value) => Ok(RuntimeBackend::Simulated), Some(value) => Err(format!( "unsupported Disasmer runtimeBackend `{value}`; use local-services, live-services, or explicit demo" )), } }