Source commit: 224e0a3c717e58cdb7bb61dcebbf5dadc4225d3a Public tree identity: sha256:2ea0b60d516db02a3ecdeb49982be1782cc3e7364a0e0eadc541c2e095d8b126
1133 lines
37 KiB
Rust
1133 lines
37 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::io::{self, BufRead, BufReader, Write};
|
|
use std::net::TcpStream;
|
|
use std::process::{Child, Command, Stdio};
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use disasmer_core::{DebugRuntimeState, Digest, ProcessId, TaskId};
|
|
use serde_json::{json, Value};
|
|
|
|
const MAIN_THREAD: i64 = 1;
|
|
const LINUX_THREAD: i64 = 2;
|
|
const WINDOWS_THREAD: i64 = 3;
|
|
const PACKAGE_THREAD: i64 = 4;
|
|
|
|
#[derive(Clone)]
|
|
struct DapWriter {
|
|
seq: i64,
|
|
}
|
|
|
|
impl DapWriter {
|
|
fn new() -> Self {
|
|
Self { seq: 1 }
|
|
}
|
|
|
|
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,
|
|
}))
|
|
}
|
|
|
|
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(),
|
|
}))
|
|
}
|
|
|
|
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,
|
|
}))
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct VirtualThread {
|
|
id: i64,
|
|
frame_id: i64,
|
|
args_ref: i64,
|
|
runtime_ref: i64,
|
|
output_ref: i64,
|
|
task: TaskId,
|
|
name: String,
|
|
line: i64,
|
|
state: DebugRuntimeState,
|
|
freeze_supported: bool,
|
|
recent_output: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AdapterState {
|
|
process: ProcessId,
|
|
epoch: u64,
|
|
entry: String,
|
|
project: String,
|
|
runtime_backend: RuntimeBackend,
|
|
command_status: String,
|
|
last_task_failed: bool,
|
|
runtime_artifact_path: Option<String>,
|
|
runtime_event_count: usize,
|
|
breakpoints: Vec<i64>,
|
|
threads: BTreeMap<i64, VirtualThread>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
enum RuntimeBackend {
|
|
Simulated,
|
|
LocalServices,
|
|
}
|
|
|
|
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,
|
|
runtime_backend: RuntimeBackend::Simulated,
|
|
command_status: "waiting for launch".to_owned(),
|
|
last_task_failed: false,
|
|
runtime_artifact_path: None,
|
|
runtime_event_count: 0,
|
|
breakpoints: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AdapterState {
|
|
fn configure_launch(
|
|
&mut self,
|
|
project: String,
|
|
entry: String,
|
|
runtime_backend: RuntimeBackend,
|
|
) {
|
|
self.process = process_id(&project, &entry);
|
|
self.entry = entry;
|
|
self.project = project;
|
|
self.runtime_backend = runtime_backend;
|
|
self.command_status = "waiting for configurationDone".to_owned();
|
|
self.last_task_failed = false;
|
|
self.runtime_artifact_path = None;
|
|
self.runtime_event_count = 0;
|
|
self.epoch = 0;
|
|
self.breakpoints.clear();
|
|
self.threads = launch_threads(&self.entry);
|
|
}
|
|
|
|
fn apply_runtime_record(&mut self, record: RuntimeLaunchRecord) {
|
|
let task_failed = record.status_code != Some(0);
|
|
self.command_status = format!(
|
|
"{} through local services with status {:?}",
|
|
if task_failed { "failed" } else { "completed" },
|
|
record.status_code
|
|
);
|
|
self.last_task_failed = task_failed;
|
|
self.runtime_artifact_path = record.artifact_path;
|
|
self.runtime_event_count = record.event_count;
|
|
if let Some(thread) = self.threads.get_mut(&LINUX_THREAD) {
|
|
thread.recent_output.push(format!(
|
|
"local node {} task with {} stdout bytes and {} stderr bytes",
|
|
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
|
|
));
|
|
if task_failed {
|
|
thread.state = DebugRuntimeState::Failed(format!(
|
|
"local services task exited with status {:?}",
|
|
record.status_code
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct RuntimeLaunchRecord {
|
|
status_code: Option<i32>,
|
|
stdout_bytes: u64,
|
|
stderr_bytes: u64,
|
|
artifact_path: Option<String>,
|
|
event_count: usize,
|
|
}
|
|
|
|
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", "package artifacts", 64),
|
|
]
|
|
.into_iter()
|
|
.map(|thread| (thread.id, thread))
|
|
.collect()
|
|
}
|
|
|
|
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::<String>();
|
|
ProcessId::new(format!("vp-{suffix}"))
|
|
}
|
|
|
|
fn thread(id: i64, task: &str, name: &str, line: i64) -> VirtualThread {
|
|
VirtualThread {
|
|
id,
|
|
frame_id: 1000 + id,
|
|
args_ref: 2000 + id,
|
|
runtime_ref: 3000 + id,
|
|
output_ref: 4000 + id,
|
|
task: TaskId::from(task),
|
|
name: name.to_owned(),
|
|
line,
|
|
state: DebugRuntimeState::Running,
|
|
freeze_supported: true,
|
|
recent_output: vec![format!("{name}: waiting")],
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let mut writer = DapWriter::new();
|
|
let mut reader = BufReader::new(io::stdin());
|
|
let mut state = AdapterState::default();
|
|
|
|
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" => {
|
|
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 args.get("runtimeBackend").and_then(Value::as_str) {
|
|
Some("local-services") => RuntimeBackend::LocalServices,
|
|
_ => RuntimeBackend::Simulated,
|
|
};
|
|
state.configure_launch(project, state.entry.clone(), runtime_backend);
|
|
writer.response(&request, true, json!({}))?;
|
|
writer.event("initialized", json!({}))?;
|
|
}
|
|
"setBreakpoints" => {
|
|
state.breakpoints = 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::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
let response = state
|
|
.breakpoints
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, line)| {
|
|
json!({
|
|
"id": index + 1,
|
|
"verified": true,
|
|
"line": line,
|
|
"message": "Mapped to Disasmer virtual source location",
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
writer.response(&request, true, json!({ "breakpoints": response }))?;
|
|
}
|
|
"setExceptionBreakpoints" => {
|
|
writer.response(&request, true, json!({ "breakpoints": [] }))?;
|
|
}
|
|
"configurationDone" => {
|
|
if state.runtime_backend == RuntimeBackend::LocalServices {
|
|
match run_local_services_runtime(&state) {
|
|
Ok(record) => {
|
|
state.apply_runtime_record(record);
|
|
}
|
|
Err(err) => {
|
|
writer.error_response(
|
|
&request,
|
|
format!("local services runtime launch failed: {err:#}"),
|
|
)?;
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
state.command_status = "running under simulated debug adapter state".to_owned();
|
|
}
|
|
writer.response(&request, true, json!({}))?;
|
|
writer.output(
|
|
"console",
|
|
format!(
|
|
"Disasmer bundle refreshed for entry `{}`; starting virtual process {} with {:?} runtime\n",
|
|
state.entry, state.process, state.runtime_backend
|
|
),
|
|
)?;
|
|
if !state.breakpoints.is_empty() {
|
|
let stopped_thread = stopped_thread_for_breakpoint(&state);
|
|
match freeze_all(&mut state, stopped_thread, None) {
|
|
Ok(()) => {
|
|
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::<Vec<_>>();
|
|
writer.response(&request, true, json!({ "threads": threads }))?;
|
|
}
|
|
"stackTrace" => {
|
|
let thread = request_thread(&request, &state);
|
|
writer.response(
|
|
&request,
|
|
true,
|
|
json!({
|
|
"stackFrames": [{
|
|
"id": thread.frame_id,
|
|
"name": format!("{}::run", thread.name),
|
|
"line": thread.line,
|
|
"column": 1,
|
|
"source": {
|
|
"name": "build.rs",
|
|
"path": "src/build.rs"
|
|
}
|
|
}],
|
|
"totalFrames": 1
|
|
}),
|
|
)?;
|
|
}
|
|
"scopes" => {
|
|
let frame_id = request
|
|
.get("arguments")
|
|
.and_then(|value| value.get("frameId"))
|
|
.and_then(Value::as_i64)
|
|
.unwrap_or(1000 + LINUX_THREAD);
|
|
let thread = state
|
|
.threads
|
|
.values()
|
|
.find(|thread| thread.frame_id == frame_id)
|
|
.cloned()
|
|
.unwrap_or_else(|| state.threads[&LINUX_THREAD].clone());
|
|
writer.response(
|
|
&request,
|
|
true,
|
|
json!({
|
|
"scopes": [
|
|
{
|
|
"name": "Task Args and Handles",
|
|
"variablesReference": thread.args_ref,
|
|
"expensive": false,
|
|
"presentationHint": "locals"
|
|
},
|
|
{
|
|
"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" => {
|
|
match freeze_all(
|
|
&mut state,
|
|
LINUX_THREAD,
|
|
simulated_freeze_failure_thread(&request),
|
|
) {
|
|
Ok(()) => {
|
|
writer.response(&request, true, json!({}))?;
|
|
writer.event(
|
|
"stopped",
|
|
json!({
|
|
"reason": "pause",
|
|
"description": "Disasmer Debug Epoch pause",
|
|
"threadId": LINUX_THREAD,
|
|
"allThreadsStopped": true,
|
|
}),
|
|
)?;
|
|
}
|
|
Err(failure) => {
|
|
let message = failure.message();
|
|
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;
|
|
}
|
|
}
|
|
let thread_id = request_thread_id(&request).unwrap_or(LINUX_THREAD);
|
|
writer.response(&request, true, json!({ "allThreadsContinued": true }))?;
|
|
writer.event(
|
|
"continued",
|
|
json!({
|
|
"threadId": thread_id,
|
|
"allThreadsContinued": true,
|
|
}),
|
|
)?;
|
|
}
|
|
"restart" | "restartFrame" => {
|
|
let thread_id = request_thread_id(&request).unwrap_or(LINUX_THREAD);
|
|
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 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 request_thread<'a>(request: &Value, state: &'a AdapterState) -> &'a VirtualThread {
|
|
let thread_id = request_thread_id(request).unwrap_or(LINUX_THREAD);
|
|
state
|
|
.threads
|
|
.get(&thread_id)
|
|
.unwrap_or_else(|| &state.threads[&LINUX_THREAD])
|
|
}
|
|
|
|
fn request_thread_id(request: &Value) -> Option<i64> {
|
|
request
|
|
.get("arguments")
|
|
.and_then(|value| value.get("threadId"))
|
|
.and_then(Value::as_i64)
|
|
}
|
|
|
|
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)]
|
|
struct FreezeFailure {
|
|
thread_id: i64,
|
|
task: TaskId,
|
|
}
|
|
|
|
impl FreezeFailure {
|
|
fn message(&self) -> String {
|
|
format!(
|
|
"debug all-stop failed: participant `{}` on thread {} could not freeze",
|
|
self.task, self.thread_id
|
|
)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
fn freeze_all(
|
|
state: &mut AdapterState,
|
|
stopped_thread: 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) {
|
|
if let Some(line) = state.breakpoints.first().copied() {
|
|
thread.line = line;
|
|
}
|
|
thread
|
|
.recent_output
|
|
.push(format!("debug epoch {} all-stop", state.epoch));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn stopped_thread_for_breakpoint(state: &AdapterState) -> i64 {
|
|
state
|
|
.breakpoints
|
|
.first()
|
|
.and_then(|line| {
|
|
state
|
|
.threads
|
|
.values()
|
|
.find(|thread| thread.line == *line)
|
|
.map(|thread| thread.id)
|
|
})
|
|
.unwrap_or(LINUX_THREAD)
|
|
}
|
|
|
|
fn variables_response(state: &AdapterState, reference: i64) -> Value {
|
|
let Some(thread) = state.threads.values().find(|thread| {
|
|
thread.args_ref == reference
|
|
|| thread.runtime_ref == reference
|
|
|| thread.output_ref == reference
|
|
}) else {
|
|
return json!({ "variables": [] });
|
|
};
|
|
|
|
if thread.args_ref == reference {
|
|
return json!({
|
|
"variables": [
|
|
{
|
|
"name": "target",
|
|
"value": thread.task.to_string(),
|
|
"variablesReference": 0
|
|
},
|
|
{
|
|
"name": "artifact",
|
|
"value": state.runtime_artifact_path.clone().unwrap_or_else(|| {
|
|
format!("artifact://{}/{}/app.tar.zst", state.process, thread.task)
|
|
}),
|
|
"variablesReference": 0
|
|
},
|
|
{
|
|
"name": "source_snapshot",
|
|
"value": format!("source://local-checkout/{}", project_snapshot_suffix(&state.project)),
|
|
"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.name,
|
|
"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": state.command_status,
|
|
"variablesReference": 0
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
json!({
|
|
"variables": thread
|
|
.recent_output
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, output)| {
|
|
json!({
|
|
"name": format!("line_{index}"),
|
|
"value": output,
|
|
"variablesReference": 0
|
|
})
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
}
|
|
|
|
fn run_local_services_runtime(state: &AdapterState) -> Result<RuntimeLaunchRecord> {
|
|
let repo = std::env::current_dir()?;
|
|
let mut coordinator = Command::new("cargo")
|
|
.args([
|
|
"run",
|
|
"-q",
|
|
"-p",
|
|
"disasmer-coordinator",
|
|
"--bin",
|
|
"disasmer-coordinator",
|
|
"--",
|
|
"--listen",
|
|
"127.0.0.1:0",
|
|
])
|
|
.current_dir(&repo)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()?;
|
|
|
|
let result = run_with_coordinator(state, &repo, &mut coordinator);
|
|
let _ = coordinator.kill();
|
|
let _ = coordinator.wait();
|
|
result
|
|
}
|
|
|
|
fn run_with_coordinator(
|
|
state: &AdapterState,
|
|
repo: &std::path::Path,
|
|
coordinator: &mut Child,
|
|
) -> Result<RuntimeLaunchRecord> {
|
|
let stdout = coordinator
|
|
.stdout
|
|
.take()
|
|
.ok_or_else(|| anyhow!("coordinator stdout was not captured"))?;
|
|
let mut ready_line = String::new();
|
|
BufReader::new(stdout).read_line(&mut ready_line)?;
|
|
let ready: Value = serde_json::from_str(&ready_line)?;
|
|
let listen = ready
|
|
.get("listen")
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| anyhow!("coordinator did not report a listen address"))?;
|
|
|
|
let node_output = Command::new("cargo")
|
|
.args([
|
|
"run",
|
|
"-q",
|
|
"-p",
|
|
"disasmer-node",
|
|
"--bin",
|
|
"disasmer-node",
|
|
"--",
|
|
"--coordinator",
|
|
listen,
|
|
"--tenant",
|
|
"tenant",
|
|
"--project-id",
|
|
"project",
|
|
"--node",
|
|
"dap-node",
|
|
"--process",
|
|
state.process.as_str(),
|
|
"--task",
|
|
"compile-linux",
|
|
"--project",
|
|
&state.project,
|
|
"--artifact",
|
|
"/vfs/artifacts/dap-output.txt",
|
|
])
|
|
.current_dir(repo)
|
|
.output()?;
|
|
if !node_output.status.success() {
|
|
return Err(anyhow!(
|
|
"node runtime failed: {}",
|
|
String::from_utf8_lossy(&node_output.stderr)
|
|
));
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&node_output.stdout);
|
|
let report_line = stdout
|
|
.lines()
|
|
.last()
|
|
.ok_or_else(|| anyhow!("node runtime did not emit JSON"))?;
|
|
let report: Value = serde_json::from_str(report_line)?;
|
|
let events = coordinator_request(
|
|
listen,
|
|
json!({
|
|
"type": "list_task_events",
|
|
"tenant": "tenant",
|
|
"project": "project",
|
|
"actor_user": "dap",
|
|
"process": state.process.as_str(),
|
|
}),
|
|
)?;
|
|
let event_count = events
|
|
.get("events")
|
|
.and_then(Value::as_array)
|
|
.map(Vec::len)
|
|
.unwrap_or(0);
|
|
|
|
Ok(RuntimeLaunchRecord {
|
|
status_code: report
|
|
.get("status_code")
|
|
.and_then(Value::as_i64)
|
|
.map(|value| value as i32),
|
|
stdout_bytes: report
|
|
.get("stdout_bytes")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0),
|
|
stderr_bytes: report
|
|
.get("stderr_bytes")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0),
|
|
artifact_path: report
|
|
.get("staged_artifact")
|
|
.and_then(|artifact| artifact.get("path"))
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned),
|
|
event_count,
|
|
})
|
|
}
|
|
|
|
fn coordinator_request(addr: &str, request: Value) -> Result<Value> {
|
|
let mut stream = TcpStream::connect(addr)?;
|
|
serde_json::to_writer(&mut stream, &request)?;
|
|
stream.write_all(b"\n")?;
|
|
stream.flush()?;
|
|
|
|
let mut line = String::new();
|
|
BufReader::new(stream).read_line(&mut line)?;
|
|
Ok(serde_json::from_str(&line)?)
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
fn initialize_capabilities() -> Value {
|
|
json!({
|
|
"supportsConfigurationDoneRequest": true,
|
|
"supportsTerminateRequest": true,
|
|
"supportsRestartRequest": true,
|
|
"supportsRestartFrame": true,
|
|
"supportsEvaluateForHovers": true,
|
|
"supportsStepBack": false,
|
|
})
|
|
}
|
|
|
|
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)?))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
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("disasmer")));
|
|
}
|
|
|
|
#[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 nonzero_local_runtime_record_marks_linux_task_failed() {
|
|
let mut state = AdapterState::default();
|
|
|
|
state.apply_runtime_record(RuntimeLaunchRecord {
|
|
status_code: Some(1),
|
|
stdout_bytes: 0,
|
|
stderr_bytes: 12,
|
|
artifact_path: None,
|
|
event_count: 1,
|
|
});
|
|
|
|
let linux = state
|
|
.threads
|
|
.get(&LINUX_THREAD)
|
|
.expect("linux task should exist");
|
|
|
|
assert!(state.last_task_failed);
|
|
assert!(state
|
|
.command_status
|
|
.contains("failed through local services"));
|
|
assert!(matches!(linux.state, DebugRuntimeState::Failed(_)));
|
|
assert!(linux
|
|
.recent_output
|
|
.iter()
|
|
.any(|line| line.contains("local node failed task")));
|
|
}
|
|
|
|
#[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, TaskId::from("package"));
|
|
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 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, TaskId::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 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"
|
|
}));
|
|
}
|
|
}
|