дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Stop flipped the session back to Run as soon as the direct child exited, but a shell, Maven or the debug adapter can exit while the application it started still holds its port, so the next Run failed with "port already in use". The console also went quiet on a debug Stop because the DAP session was disconnected before java-debug drained the program's output, and closing a window killed running programs with SIGKILL at once. Run and Debug sessions now stay in a stopping state from the Stop press until the whole process group (or the debuggee's pid tree) is gone. As in IntelliJ IDEA, the first Stop only interrupts and waits, and a second Stop kills; the Stop button turns into Kill process meanwhile. A rerun or restart waits for the old process without a time limit, and the termination time limit setting now only bounds a configuration's stop command. Debug launches record the program pid from the adapter's process/processid event, signal its pid tree and keep the adapter attached until the program exits; closing a project also stops debug sessions under its root. Closing a window with running programs shows a Terminating panel with Kill process and closes once they are gone. Co-authored-by: misswell <[email protected]>
1674 рядки
71 KiB
Rust
1674 рядки
71 KiB
Rust
//! Debug Adapter Protocol client.
|
|
//!
|
|
//! [`DebugManager`] owns every debug session plus the per-file breakpoint
|
|
//! store (independent of sessions; sent to each session during configuration
|
|
//! and updated live). Sessions emit `IdeEvent::Debug`; the UI then pulls
|
|
//! threads, frames, scopes and variables on demand.
|
|
//!
|
|
//! All lines and columns in this API are 0-based; the client tells adapters
|
|
//! `linesStartAt1`/`columnsStartAt1` and converts at this edge.
|
|
|
|
pub mod adapter;
|
|
pub mod attach;
|
|
mod cargo;
|
|
pub mod connection;
|
|
pub mod jdwp;
|
|
pub mod kotlin;
|
|
mod kotlin_jdwp;
|
|
mod session;
|
|
|
|
use adapter::{AdapterLauncher, AdapterSpec, ProcessLauncher};
|
|
use connection::{Connection, DapError};
|
|
use futures::future::join_all;
|
|
use ide_core::{DebugSessionId, IdeError, IdeResult, SharedSink};
|
|
use ide_language::LanguageRegistry;
|
|
use parking_lot::Mutex;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{Value, json};
|
|
use session::Session;
|
|
use specta::Type;
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::{Arc, Weak};
|
|
use std::time::Duration;
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LaunchConfig {
|
|
pub name: String,
|
|
/// DAP adapter name from `languages/*.toml` (or a built-in: codelldb, debugpy, js-debug, delve, netcoredbg).
|
|
pub adapter: String,
|
|
/// `launch` or `attach`.
|
|
pub request: String,
|
|
pub program: Option<String>,
|
|
pub args: Vec<String>,
|
|
pub cwd: Option<ide_core::IpcPath>,
|
|
pub env: BTreeMap<String, String>,
|
|
/// Adapter specific extra arguments as JSON text, merged into the launch request.
|
|
/// A `"cargo": {"args": [...], "filter": {"name", "kind"}}` entry builds with cargo
|
|
/// and launches the produced executable; `"debugServer": <port>` connects to an
|
|
/// adapter already listening on that local port (e.g. java-debug inside jdtls)
|
|
/// instead of starting one, `"debugServer": "host:port"` to one on another machine
|
|
/// (`dlv --headless`, `debugpy --listen`).
|
|
pub extra_json: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Breakpoint {
|
|
pub line: u32,
|
|
pub condition: Option<String>,
|
|
pub hit_condition: Option<String>,
|
|
/// Logpoint message; when set the breakpoint logs instead of suspending.
|
|
pub log_message: Option<String>,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
/// A method breakpoint (DAP function breakpoint). `name` uses the adapter's syntax:
|
|
/// `pkg.Class#method` for java-debug, `pkg.Func` / `pkg.(*T).Method` for Delve, a plain
|
|
/// function name for debugpy and LLDB.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FunctionBreakpoint {
|
|
pub name: String,
|
|
pub condition: Option<String>,
|
|
pub hit_condition: Option<String>,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
/// A field watchpoint (DAP data breakpoint) on a `dataId` from [`DebugManager::data_breakpoint_info`]
|
|
/// (java-debug ids, `Class#field`, stay valid across sessions).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DataBreakpoint {
|
|
pub data_id: String,
|
|
/// `read`, `write` or `readWrite` (adapter default when unset).
|
|
pub access_type: Option<String>,
|
|
pub condition: Option<String>,
|
|
pub hit_condition: Option<String>,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
/// Answer of `dataBreakpointInfo`: `data_id` is `None` when the value cannot be watched.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DataBreakpointInfo {
|
|
pub data_id: Option<String>,
|
|
pub description: String,
|
|
pub access_types: Vec<String>,
|
|
}
|
|
|
|
/// A call on the current line that Smart Step Into can enter (`stepInTargets`).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct StepInTarget {
|
|
pub id: i64,
|
|
pub label: String,
|
|
/// 0-based, when the adapter reports where the call is.
|
|
pub line: Option<u32>,
|
|
pub column: Option<u32>,
|
|
pub end_column: Option<u32>,
|
|
}
|
|
|
|
/// What a session's adapter supports, for enabling actions.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DebugCapabilities {
|
|
pub restart_frame: bool,
|
|
pub step_in_targets: bool,
|
|
pub function_breakpoints: bool,
|
|
pub data_breakpoints: bool,
|
|
/// `setVariable` or `setExpression`.
|
|
pub set_value: bool,
|
|
pub conditional_breakpoints: bool,
|
|
pub hit_conditional_breakpoints: bool,
|
|
pub log_points: bool,
|
|
/// HotSwap ("Reload Changed Classes"): java-debug's `redefineClasses` request.
|
|
pub hot_code_replace: bool,
|
|
}
|
|
|
|
/// Result of [`DebugManager::redefine_classes`].
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HotSwapResult {
|
|
/// Fully qualified names of the classes reloaded (empty: nothing had changed).
|
|
pub changed_classes: Vec<String>,
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
/// Adapters that implement java-debug's hot code replace protocol.
|
|
pub const HOT_CODE_REPLACE_ADAPTERS: &[&str] = &["java-debug"];
|
|
|
|
/// java-debug's `HotCodeReplaceEvent.changeType` (an enum name, or its numeric code).
|
|
pub(crate) fn hot_code_replace_type(v: Option<&Value>) -> String {
|
|
match v {
|
|
Some(Value::String(s)) => s.clone(),
|
|
Some(Value::Number(n)) => match n.as_i64() {
|
|
Some(-1) => "ERROR",
|
|
Some(-2) => "WARNING",
|
|
Some(1) => "STARTING",
|
|
Some(2) => "END",
|
|
Some(3) => "BUILD_COMPLETE",
|
|
_ => "UNKNOWN",
|
|
}
|
|
.to_owned(),
|
|
_ => "UNKNOWN".to_owned(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn hot_swap_result(body: &Value) -> HotSwapResult {
|
|
HotSwapResult {
|
|
changed_classes: body
|
|
.get("changedClasses")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|c| c.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default(),
|
|
error_message: body.get("errorMessage").and_then(Value::as_str).filter(|m| !m.is_empty()).map(str::to_owned),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BreakpointState {
|
|
pub line: u32,
|
|
pub verified: bool,
|
|
pub message: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
pub struct ThreadInfo {
|
|
pub id: i64,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct StackFrame {
|
|
pub id: i64,
|
|
pub name: String,
|
|
pub path: Option<ide_core::IpcPath>,
|
|
pub line: u32,
|
|
pub column: u32,
|
|
/// `subtle` frames (library/generated) are collapsed in the UI.
|
|
pub presentation_hint: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Scope {
|
|
pub name: String,
|
|
pub variables_reference: i64,
|
|
pub expensive: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Variable {
|
|
pub name: String,
|
|
pub value: String,
|
|
pub type_name: Option<String>,
|
|
/// > 0 when expandable.
|
|
pub variables_reference: i64,
|
|
pub evaluate_name: Option<String>,
|
|
}
|
|
|
|
/// A completion the adapter offers for an expression (DAP `completions`).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DebugCompletion {
|
|
pub label: String,
|
|
/// What to insert, when it differs from `label`.
|
|
pub text: Option<String>,
|
|
/// DAP item type: `method`, `field`, `variable`, `class`, `keyword`, …
|
|
pub kind: Option<String>,
|
|
pub detail: Option<String>,
|
|
pub sort_text: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum StepKind {
|
|
Over,
|
|
Into,
|
|
Out,
|
|
}
|
|
|
|
/// An exception breakpoint filter offered by an adapter (`exceptionBreakpointFilters`).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExceptionFilter {
|
|
pub filter: String,
|
|
pub label: String,
|
|
pub description: Option<String>,
|
|
/// Enabled unless the user chose otherwise.
|
|
pub default: bool,
|
|
pub supports_condition: bool,
|
|
}
|
|
|
|
/// Exception filters of one session (see [`DebugManager::exception_filters`]).
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SessionExceptionFilters {
|
|
pub session: DebugSessionId,
|
|
pub adapter: String,
|
|
pub filters: Vec<ExceptionFilter>,
|
|
}
|
|
|
|
/// A live session, as listed by [`DebugManager::sessions`].
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SessionInfo {
|
|
pub id: DebugSessionId,
|
|
pub name: String,
|
|
pub adapter: String,
|
|
/// Set for child sessions started by the adapter (`startDebugging`).
|
|
pub parent: Option<DebugSessionId>,
|
|
}
|
|
|
|
/// Who started a session. Every event of the session, and of the child sessions
|
|
/// its adapter starts, goes to `sink` only; `key` (e.g. a window label) scopes
|
|
/// listings such as [`DebugManager::sessions`].
|
|
#[derive(Clone)]
|
|
pub struct SessionOwner {
|
|
pub key: String,
|
|
pub sink: SharedSink,
|
|
}
|
|
|
|
impl SessionOwner {
|
|
pub fn new(key: impl Into<String>, sink: SharedSink) -> Self {
|
|
Self { key: key.into(), sink }
|
|
}
|
|
}
|
|
|
|
const INIT_TIMEOUT: Duration = Duration::from_secs(20);
|
|
const LAUNCH_TIMEOUT: Duration = Duration::from_secs(60);
|
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
|
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
|
|
/// Redefining many classes in a large JVM can take a while.
|
|
const HOT_SWAP_TIMEOUT: Duration = Duration::from_secs(120);
|
|
|
|
pub struct DebugManager {
|
|
languages: Arc<LanguageRegistry>,
|
|
launcher: Arc<dyn AdapterLauncher>,
|
|
me: Weak<DebugManager>,
|
|
sessions: Mutex<HashMap<DebugSessionId, Arc<Session>>>,
|
|
/// Session start order, for "the first session" semantics.
|
|
order: Mutex<Vec<DebugSessionId>>,
|
|
breakpoints: Mutex<BTreeMap<PathBuf, Vec<Breakpoint>>>,
|
|
states: Mutex<HashMap<PathBuf, Vec<BreakpointState>>>,
|
|
/// Explicit exception filter choices (filter id → enabled); unset filters use the adapter default.
|
|
exception_choices: Mutex<BTreeMap<String, bool>>,
|
|
function_breakpoints: Mutex<Vec<FunctionBreakpoint>>,
|
|
data_breakpoints: Mutex<Vec<DataBreakpoint>>,
|
|
/// In-flight stops, by session: a second Stop sets `stop_force`, the waiting one kills.
|
|
stopping: Mutex<HashMap<DebugSessionId, Arc<Session>>>,
|
|
}
|
|
|
|
/// Whether the debugged project depends on the Flutter SDK, so it must go
|
|
/// through `flutter debug_adapter` instead of the plain Dart one.
|
|
///
|
|
/// `dart debug_adapter` cannot start a program that imports `dart:ui`, and only
|
|
/// the Flutter adapter answers `hotReload` / `hotRestart`. `dir` is the launch
|
|
/// `cwd`; the program's own directory is tried first because a module inside a
|
|
/// larger repository has its `pubspec.yaml` above the program, not above `cwd`.
|
|
fn is_flutter_project(dir: &Path, program: Option<&Path>) -> bool {
|
|
let mut start: Vec<&Path> = program.and_then(Path::parent).into_iter().collect();
|
|
start.push(dir);
|
|
start
|
|
.iter()
|
|
.filter_map(|d| d.ancestors().take(6).find(|a| a.join("pubspec.yaml").is_file()))
|
|
.filter_map(|root| std::fs::read_to_string(root.join("pubspec.yaml")).ok())
|
|
.any(|s| s.contains("sdk: flutter"))
|
|
}
|
|
|
|
fn no_session(s: DebugSessionId) -> IdeError {
|
|
IdeError::not_found(format!("no debug session {}", s.0))
|
|
}
|
|
|
|
/// Builds the `launch`/`attach` arguments: our fields first, then `extra_json` merged on top.
|
|
pub fn launch_arguments(config: &LaunchConfig) -> IdeResult<Value> {
|
|
// VS Code puts `type`, `request` and `name` in the arguments; js-debug picks launch
|
|
// or attach from `request` there, not from the DAP command.
|
|
let mut args = json!({ "name": config.name, "request": config.request });
|
|
if let Some(t) = adapter::default_type(&config.adapter) {
|
|
args["type"] = t.into();
|
|
}
|
|
if let Some(p) = &config.program {
|
|
args["program"] = p.as_str().into();
|
|
}
|
|
if !config.args.is_empty() {
|
|
args["args"] = json!(config.args);
|
|
}
|
|
if let Some(cwd) = &config.cwd {
|
|
args["cwd"] = json!(cwd);
|
|
}
|
|
if !config.env.is_empty() {
|
|
args["env"] = json!(config.env);
|
|
}
|
|
if let Some(extra) = config.extra_json.as_deref().filter(|e| !e.trim().is_empty()) {
|
|
let extra: Value =
|
|
serde_json::from_str(extra).map_err(|e| IdeError::invalid(format!("invalid launch JSON: {e}")))?;
|
|
let Value::Object(extra) = extra else {
|
|
return Err(IdeError::invalid("launch JSON must be an object"));
|
|
};
|
|
if let Some(obj) = args.as_object_mut() {
|
|
obj.extend(extra);
|
|
}
|
|
}
|
|
Ok(args)
|
|
}
|
|
|
|
/// Placeholder in launch arguments for a free local port, the same one everywhere in a
|
|
/// launch: Deno's `--inspect-wait=127.0.0.1:${freePort}` and js-debug's
|
|
/// `"attachSimplePort": "${freePort}"` (a `port` / `…Port` attribute that is only the
|
|
/// placeholder becomes a number; everything else stays a string, e.g. `env` values).
|
|
pub const FREE_PORT_PLACEHOLDER: &str = "${freePort}";
|
|
|
|
/// Replaces [`FREE_PORT_PLACEHOLDER`] in every string of `value` with `port()` (called once, if used).
|
|
pub(crate) fn substitute_free_port(value: &mut Value, port: &mut impl FnMut() -> IdeResult<u16>) -> IdeResult<()> {
|
|
fn walk(v: &mut Value, key: &str, port: &mut dyn FnMut() -> IdeResult<u16>) -> IdeResult<()> {
|
|
match v {
|
|
Value::String(s) if s.contains(FREE_PORT_PLACEHOLDER) => {
|
|
let p = port()?;
|
|
*v = if s == FREE_PORT_PLACEHOLDER && (key == "port" || key.ends_with("Port")) {
|
|
p.into()
|
|
} else {
|
|
s.replace(FREE_PORT_PLACEHOLDER, &p.to_string()).into()
|
|
};
|
|
}
|
|
Value::Array(a) => a.iter_mut().try_for_each(|x| walk(x, key, port))?,
|
|
Value::Object(o) => o.iter_mut().try_for_each(|(k, x)| walk(x, k, port))?,
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
let mut chosen = None;
|
|
walk(value, "", &mut || match chosen {
|
|
Some(p) => Ok(p),
|
|
None => {
|
|
let p = port()?;
|
|
chosen = Some(p);
|
|
Ok(p)
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Where a running adapter listens: `debugServer` of the launch arguments.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct DebugServer {
|
|
/// `None` for the loopback interface (an adapter inside a local process such as jdtls).
|
|
pub host: Option<String>,
|
|
pub port: u16,
|
|
}
|
|
|
|
/// Removes `debugServer` (VS Code's "connect to a running adapter" attribute) from the
|
|
/// launch arguments: a port (`4711`, `"4711"`) on this machine, or `"host:port"` /
|
|
/// `"[v6]:port"` of a remote one (`dlv --headless`, `debugpy --listen`).
|
|
pub(crate) fn take_debug_server(arguments: &mut Value) -> IdeResult<Option<DebugServer>> {
|
|
let Some(v) = arguments.as_object_mut().and_then(|o| o.remove("debugServer")) else { return Ok(None) };
|
|
let invalid = || IdeError::invalid(format!("invalid debugServer address: {v}"));
|
|
let port_of = |p: u64| u16::try_from(p).ok().filter(|p| *p != 0);
|
|
if let Some(p) = v.as_u64() {
|
|
return port_of(p).map(|port| Some(DebugServer { host: None, port })).ok_or_else(invalid);
|
|
}
|
|
let s = v.as_str().map(str::trim).ok_or_else(invalid)?;
|
|
let (host, port) = match s.rsplit_once(':') {
|
|
Some((host, port)) => {
|
|
let host = host.trim().trim_start_matches('[').trim_end_matches(']');
|
|
(Some(host).filter(|h| !h.is_empty()).map(str::to_owned), port)
|
|
}
|
|
None => (None, s),
|
|
};
|
|
let port = port.trim().parse::<u64>().ok().and_then(port_of).ok_or_else(invalid)?;
|
|
Ok(Some(DebugServer { host, port }))
|
|
}
|
|
|
|
/// DAP stack frame → [`StackFrame`] (1-based → 0-based).
|
|
pub(crate) fn frame_from(f: &Value) -> StackFrame {
|
|
let source = f.get("source");
|
|
let source_hint = source.and_then(|s| s.get("presentationHint")).and_then(Value::as_str);
|
|
let hint = f
|
|
.get("presentationHint")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.or_else(|| (source_hint == Some("deemphasize")).then(|| "subtle".to_owned()));
|
|
StackFrame {
|
|
id: f.get("id").and_then(Value::as_i64).unwrap_or_default(),
|
|
name: f.get("name").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
path: source.and_then(|s| s.get("path")).and_then(Value::as_str).map(Into::into),
|
|
line: (f.get("line").and_then(Value::as_u64).unwrap_or(0) as u32).saturating_sub(1),
|
|
column: (f.get("column").and_then(Value::as_u64).unwrap_or(0) as u32).saturating_sub(1),
|
|
presentation_hint: hint,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn exception_filters_from(caps: &Value) -> Vec<ExceptionFilter> {
|
|
caps.get("exceptionBreakpointFilters")
|
|
.and_then(Value::as_array)
|
|
.map(|a| {
|
|
a.iter()
|
|
.filter_map(|f| {
|
|
let filter = f.get("filter").and_then(Value::as_str)?.to_owned();
|
|
Some(ExceptionFilter {
|
|
label: f
|
|
.get("label")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.unwrap_or_else(|| filter.clone()),
|
|
description: f.get("description").and_then(Value::as_str).map(str::to_owned),
|
|
default: f.get("default").and_then(Value::as_bool).unwrap_or(false),
|
|
supports_condition: f.get("supportsCondition").and_then(Value::as_bool).unwrap_or(false),
|
|
filter,
|
|
})
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Filter ids to enable: explicit choices win (`adapter:filter`, then the bare filter id,
|
|
/// since adapters reuse ids such as `uncaught`), otherwise the adapter's default.
|
|
pub(crate) fn enabled_filters(
|
|
filters: &[ExceptionFilter],
|
|
choices: &BTreeMap<String, bool>,
|
|
adapter: &str,
|
|
) -> Vec<String> {
|
|
filters
|
|
.iter()
|
|
.filter(|f| {
|
|
choices
|
|
.get(&format!("{adapter}:{}", f.filter))
|
|
.or_else(|| choices.get(&f.filter))
|
|
.copied()
|
|
.unwrap_or(f.default)
|
|
})
|
|
.map(|f| f.filter.clone())
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn step_in_target_from(t: &Value) -> StepInTarget {
|
|
let num = |k: &str| t.get(k).and_then(Value::as_u64).map(|n| (n as u32).saturating_sub(1));
|
|
StepInTarget {
|
|
id: t.get("id").and_then(Value::as_i64).unwrap_or_default(),
|
|
label: t.get("label").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
line: num("line"),
|
|
column: num("column"),
|
|
end_column: num("endColumn"),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn capabilities_from(caps: &Value) -> DebugCapabilities {
|
|
let has = |k: &str| caps.get(k).and_then(Value::as_bool).unwrap_or(false);
|
|
DebugCapabilities {
|
|
restart_frame: has("supportsRestartFrame"),
|
|
step_in_targets: has("supportsStepInTargetsRequest"),
|
|
function_breakpoints: has("supportsFunctionBreakpoints"),
|
|
data_breakpoints: has("supportsDataBreakpoints"),
|
|
set_value: has("supportsSetVariable") || has("supportsSetExpression"),
|
|
conditional_breakpoints: has("supportsConditionalBreakpoints"),
|
|
hit_conditional_breakpoints: has("supportsHitConditionalBreakpoints"),
|
|
log_points: has("supportsLogPoints"),
|
|
hot_code_replace: false,
|
|
}
|
|
}
|
|
|
|
/// States of a file's line breakpoints that no session has been sent.
|
|
fn unsent(breakpoints: &[Breakpoint]) -> Vec<BreakpointState> {
|
|
breakpoints
|
|
.iter()
|
|
.map(|b| BreakpointState {
|
|
line: b.line,
|
|
verified: false,
|
|
message: (!b.enabled).then(|| "Disabled".to_owned()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// States for breakpoints no session has verified yet.
|
|
fn unverified(enabled: impl Iterator<Item = bool>) -> Vec<BreakpointState> {
|
|
enabled
|
|
.map(|e| BreakpointState { line: 0, verified: false, message: (!e).then(|| "Disabled".to_owned()) })
|
|
.collect()
|
|
}
|
|
|
|
/// Delve compiles the debuggee to `__debug_bin` in the session's cwd (on Windows
|
|
/// `__debug_bin.exe` plus `__debug_bin.exe<digits>` in DAP mode) and removes it on a
|
|
/// graceful exit — a shutdown it doesn't always reach, so the files pile up. Sweep
|
|
/// the leftovers once the session is down. Runs after `session_closed`, so Delve
|
|
/// has already exited or been killed and holds no file locks; a leftover of a
|
|
/// still-building run is removed on that run's own shutdown.
|
|
fn remove_delve_binaries(cwd: &Path) {
|
|
let Ok(entries) = std::fs::read_dir(cwd) else { return };
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name();
|
|
let Some(name) = name.to_str() else { continue };
|
|
// Windows writes `__debug_bin.exe` (and `.exe<digits>` in DAP mode); other
|
|
// platforms keep the bare name. Suffixes like `.go` are user files.
|
|
let suffix = name.strip_prefix("__debug_bin");
|
|
let leftover = suffix.is_some_and(|s| {
|
|
s.is_empty()
|
|
|| s == ".exe"
|
|
|| (s.starts_with(".exe") && s[".exe".len()..].chars().all(|c| c.is_ascii_digit()))
|
|
});
|
|
if leftover
|
|
&& entry.file_type().is_ok_and(|t| t.is_file())
|
|
&& let Err(e) = std::fs::remove_file(entry.path())
|
|
{
|
|
tracing::debug!("could not remove Delve leftover {}: {e}", entry.path().display());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn variable_from(v: &Value) -> Variable {
|
|
let s = |k: &str| v.get(k).and_then(Value::as_str).map(str::to_owned);
|
|
Variable {
|
|
name: s("name").unwrap_or_default(),
|
|
value: s("value").unwrap_or_default(),
|
|
type_name: s("type").filter(|t| !t.is_empty()),
|
|
variables_reference: v.get("variablesReference").and_then(Value::as_i64).unwrap_or(0),
|
|
evaluate_name: s("evaluateName"),
|
|
}
|
|
}
|
|
|
|
impl DebugManager {
|
|
pub fn new(languages: Arc<LanguageRegistry>) -> Arc<Self> {
|
|
Self::with_launcher(languages, Arc::new(ProcessLauncher))
|
|
}
|
|
|
|
/// Like [`new`](Self::new) with a custom way to start adapters (tests use in-memory adapters).
|
|
pub fn with_launcher(languages: Arc<LanguageRegistry>, launcher: Arc<dyn AdapterLauncher>) -> Arc<Self> {
|
|
Arc::new_cyclic(|me| Self {
|
|
languages,
|
|
launcher,
|
|
me: me.clone(),
|
|
sessions: Mutex::default(),
|
|
order: Mutex::default(),
|
|
breakpoints: Mutex::default(),
|
|
states: Mutex::default(),
|
|
exception_choices: Mutex::default(),
|
|
function_breakpoints: Mutex::default(),
|
|
data_breakpoints: Mutex::default(),
|
|
stopping: Mutex::default(),
|
|
})
|
|
}
|
|
|
|
fn session(&self, s: DebugSessionId) -> IdeResult<Arc<Session>> {
|
|
self.sessions.lock().get(&s).cloned().ok_or_else(|| no_session(s))
|
|
}
|
|
|
|
fn live_sessions(&self) -> Vec<Arc<Session>> {
|
|
let sessions = self.sessions.lock();
|
|
self.order.lock().iter().filter_map(|id| sessions.get(id).cloned()).collect()
|
|
}
|
|
|
|
pub(crate) fn stored_breakpoints(&self, path: &Path) -> Vec<Breakpoint> {
|
|
self.breakpoints.lock().get(path).cloned().unwrap_or_default()
|
|
}
|
|
|
|
/// Live sessions of `owner` (all owners when `None`).
|
|
pub fn sessions(&self, owner: Option<&str>) -> Vec<SessionInfo> {
|
|
self.live_sessions()
|
|
.iter()
|
|
.filter(|s| owner.is_none_or(|o| s.owner.key == o))
|
|
.map(|s| SessionInfo { id: s.id, name: s.name.clone(), adapter: s.adapter.clone(), parent: s.parent })
|
|
.collect()
|
|
}
|
|
|
|
/// Starts a session: spawns the adapter, `initialize`, `launch`/`attach`,
|
|
/// waits for `initialized`, sends breakpoints, then `configurationDone`.
|
|
/// Events of the session (including cargo build output) go to `owner` only.
|
|
pub async fn start(&self, config: LaunchConfig, owner: SessionOwner) -> IdeResult<DebugSessionId> {
|
|
self.start_with(config, owner, |_| None).await
|
|
}
|
|
|
|
/// [`start`](Self::start) with the adapter executable chosen by `custom_command`
|
|
/// (the configured command → a user-chosen executable, from settings).
|
|
pub async fn start_with(
|
|
&self,
|
|
config: LaunchConfig,
|
|
owner: SessionOwner,
|
|
custom_command: impl FnOnce(&str) -> Option<String>,
|
|
) -> IdeResult<DebugSessionId> {
|
|
let mut arguments = launch_arguments(&config)?;
|
|
// An IDE-side marker (never sent to the adapter): the session is a Flutter
|
|
// one regardless of what the working directory holds.
|
|
let force_flutter =
|
|
arguments.as_object_mut().and_then(|o| o.remove("__flutter")).and_then(|v| v.as_bool()).unwrap_or(false);
|
|
let cwd = config
|
|
.cwd
|
|
.clone()
|
|
.map(Into::into)
|
|
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into()));
|
|
let dbg = adapter::find_config(&self.languages, &config.adapter)
|
|
.ok_or_else(|| IdeError::unavailable(format!("unknown debug adapter `{}`", config.adapter)))?;
|
|
let mut spec = adapter::resolve(&dbg, cwd.clone());
|
|
if config.adapter == kotlin::ADAPTER
|
|
&& let Some(home) = arguments
|
|
.as_object_mut()
|
|
.and_then(|o| o.remove("__adapterJavaHome"))
|
|
.and_then(|v| v.as_str().map(str::to_owned))
|
|
{
|
|
spec.env.push(("JAVA_HOME".into(), home));
|
|
}
|
|
if let Some(command) = custom_command(&spec.command) {
|
|
spec.command = command;
|
|
}
|
|
// debugpy: run the adapter with the debuggee's interpreter (venvs carry their own debugpy),
|
|
// or with the launcher's when the debuggee runs through a manager (`uv run python`).
|
|
if config.adapter == "debugpy" {
|
|
let python = match (arguments.get("debugLauncherPython"), arguments.get("python")) {
|
|
(Some(Value::String(p)), _) | (_, Some(Value::String(p))) => Some(p.clone()),
|
|
(_, Some(Value::Array(a))) => a.first().and_then(Value::as_str).map(str::to_owned),
|
|
_ => None,
|
|
};
|
|
if let Some(python) = python {
|
|
spec.command = python;
|
|
}
|
|
}
|
|
// Flutter: a project depending on the Flutter SDK needs `flutter debug_adapter`
|
|
// (the Dart adapter cannot start a program importing `dart:ui`, and hot reload
|
|
// is Flutter-only). Plain Dart projects keep the configured `dart`.
|
|
if config.adapter == "dart" && spec.command == "dart" {
|
|
let program = arguments.get("program").and_then(Value::as_str).map(Path::new);
|
|
if force_flutter || is_flutter_project(&cwd, program) {
|
|
spec.command = "flutter".into();
|
|
}
|
|
}
|
|
let id = DebugSessionId::next();
|
|
let debug_server = take_debug_server(&mut arguments)?;
|
|
if let Some(server) = &debug_server {
|
|
spec.server_host = server.host.clone();
|
|
}
|
|
if let Some(cargo) = arguments.as_object_mut().and_then(|o| o.remove("cargo")) {
|
|
let cargo_args: Vec<String> = cargo
|
|
.get("args")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_else(|| vec!["build".into()]);
|
|
let filter = cargo::CargoFilter::from_json(cargo.get("filter"));
|
|
let sink = owner.sink.clone();
|
|
let program = cargo::build(&cwd, &cargo_args, &filter, move |line| {
|
|
sink.emit(ide_core::IdeEvent::Debug {
|
|
session: id,
|
|
event: ide_core::event::DebugEvent::Output { category: "console".into(), output: line },
|
|
});
|
|
})
|
|
.await?;
|
|
arguments["program"] = json!(program);
|
|
}
|
|
// Attach to a Node process started without `--inspect`: open its inspector first.
|
|
if config.adapter == "js-debug" && config.request == "attach" && arguments.get("port").is_none() {
|
|
let pid = arguments.get("processId").and_then(|p| match p {
|
|
Value::String(s) => s.split(':').next().and_then(|p| p.trim().parse::<u32>().ok()),
|
|
other => other.as_u64().and_then(|p| u32::try_from(p).ok()),
|
|
});
|
|
if let Some(pid) = pid {
|
|
let port = attach::enable_node_inspector(pid).await?;
|
|
if let Some(obj) = arguments.as_object_mut() {
|
|
obj.remove("processId");
|
|
obj.insert("port".into(), port.into());
|
|
obj.entry("address").or_insert_with(|| "127.0.0.1".into());
|
|
}
|
|
}
|
|
}
|
|
substitute_free_port(&mut arguments, &mut || adapter::free_port().map_err(IdeError::from))?;
|
|
// VS Code passes the workspace folder: js-debug's default `outFiles` (where it looks
|
|
// for compiled code of a TypeScript breakpoint before it loads) is relative to it.
|
|
if config.adapter == "js-debug" && arguments.get("__workspaceFolder").is_none() {
|
|
arguments["__workspaceFolder"] = json!(cwd);
|
|
}
|
|
// Godot compares `project` with its own (resolved) project path: `/tmp/game` is
|
|
// rejected by an editor that opened `/private/tmp/game`.
|
|
if config.adapter == "godot"
|
|
&& let Some(project) = arguments.get("project").and_then(Value::as_str)
|
|
&& let Ok(real) = std::fs::canonicalize(project)
|
|
{
|
|
arguments["project"] = json!(real);
|
|
}
|
|
if config.adapter == "codelldb" {
|
|
if arguments.get("sourceLanguages").is_none() {
|
|
arguments["sourceLanguages"] = json!(["rust"]);
|
|
}
|
|
// The Debug Console and Evaluate Expression take expressions, as with every other
|
|
// debugger; LLDB commands still run with a leading backtick.
|
|
match arguments.get_mut("_adapterSettings") {
|
|
None => arguments["_adapterSettings"] = json!({"consoleMode": "evaluate"}),
|
|
Some(Value::Object(settings)) => {
|
|
settings.entry("consoleMode").or_insert_with(|| json!("evaluate"));
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
let port = debug_server.map(|s| s.port);
|
|
self.start_session(id, owner, config.name, config.adapter, None, config.request, arguments, spec, port).await
|
|
}
|
|
|
|
/// `startDebugging` reverse request: a child session on the same adapter.
|
|
pub(crate) async fn start_child(
|
|
&self,
|
|
parent: &Arc<Session>,
|
|
request: String,
|
|
configuration: Value,
|
|
) -> IdeResult<()> {
|
|
let name =
|
|
configuration.get("name").and_then(Value::as_str).map(str::to_owned).unwrap_or_else(|| parent.name.clone());
|
|
let mut configuration = configuration;
|
|
if let Some(obj) = configuration.as_object_mut() {
|
|
obj.entry("request").or_insert_with(|| request.clone().into());
|
|
}
|
|
self.start_session(
|
|
DebugSessionId::next(),
|
|
parent.owner.clone(),
|
|
name,
|
|
parent.adapter.clone(),
|
|
Some(parent.id),
|
|
request,
|
|
configuration,
|
|
parent.spec.clone(),
|
|
parent.port,
|
|
)
|
|
.await
|
|
.map(|_| ())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn start_session(
|
|
&self,
|
|
id: DebugSessionId,
|
|
owner: SessionOwner,
|
|
name: String,
|
|
adapter_name: String,
|
|
parent: Option<DebugSessionId>,
|
|
request: String,
|
|
arguments: Value,
|
|
spec: AdapterSpec,
|
|
reuse_port: Option<u16>,
|
|
) -> IdeResult<DebugSessionId> {
|
|
if request != "launch" && request != "attach" {
|
|
return Err(IdeError::invalid(format!("request must be `launch` or `attach`, got `{request}`")));
|
|
}
|
|
let io = self.launcher.launch(spec.clone(), reuse_port).await?;
|
|
let stderr = io.stderr.clone();
|
|
let (conn, incoming) = {
|
|
let stderr = stderr.clone();
|
|
Connection::start_with_output(io.reader, io.writer, Box::new(move |line| adapter::push_line(&stderr, line)))
|
|
};
|
|
let session =
|
|
Arc::new(Session::new(id, owner, name, adapter_name, parent, request, conn, spec, io.port, io.child));
|
|
if let Some(mut output) = io.program_output {
|
|
let session = session.clone();
|
|
tokio::spawn(async move {
|
|
while let Some((category, output)) = output.recv().await {
|
|
session.emit(ide_core::event::DebugEvent::Output { category: category.into(), output });
|
|
}
|
|
});
|
|
}
|
|
self.sessions.lock().insert(id, session.clone());
|
|
self.order.lock().push(id);
|
|
// Announced before the event loop runs, so it precedes every adapter event.
|
|
session.emit(ide_core::event::DebugEvent::Started {
|
|
name: session.name.clone(),
|
|
adapter: session.adapter.clone(),
|
|
parent,
|
|
});
|
|
tokio::spawn(session::event_loop(self.me.clone(), session.clone(), incoming));
|
|
|
|
match self.configure(&session, arguments).await {
|
|
Ok(()) => Ok(id),
|
|
Err(e) => {
|
|
let e = if session.conn.is_closed() {
|
|
let mut msg = format!("debug adapter `{}` exited during startup", session.adapter);
|
|
let tail = stderr.lock().join("\n");
|
|
if !tail.is_empty() {
|
|
msg.push_str(":\n");
|
|
msg.push_str(&tail);
|
|
}
|
|
if let Some(hint) = &session.spec.install_hint {
|
|
msg.push('\n');
|
|
msg.push_str(hint);
|
|
}
|
|
IdeError::unavailable(msg)
|
|
} else {
|
|
e
|
|
};
|
|
session.terminated.store(true, Ordering::Release);
|
|
self.shutdown(&session, true).await;
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn configure(&self, s: &Arc<Session>, mut arguments: Value) -> IdeResult<()> {
|
|
if s.adapter == kotlin::ADAPTER {
|
|
let project = arguments
|
|
.get("projectRoot")
|
|
.and_then(Value::as_str)
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| s.spec.cwd.clone());
|
|
let mirror = tokio::task::spawn_blocking(move || kotlin::SourceMirror::create(&project))
|
|
.await
|
|
.map_err(|e| IdeError::internal(e.to_string()))??;
|
|
arguments["projectRoot"] = json!(mirror.root());
|
|
*s.kotlin_mirror.lock() = Some(mirror);
|
|
}
|
|
let caps = s
|
|
.conn
|
|
.request_timeout(
|
|
"initialize",
|
|
json!({
|
|
"clientID": "openidea",
|
|
"clientName": "OpenIDEA",
|
|
"adapterID": s.adapter,
|
|
"locale": "en",
|
|
"linesStartAt1": true,
|
|
"columnsStartAt1": true,
|
|
"pathFormat": "path",
|
|
"supportsVariableType": true,
|
|
"supportsVariablePaging": false,
|
|
"supportsRunInTerminalRequest": true,
|
|
"supportsStartDebuggingRequest": true,
|
|
"supportsProgressReporting": true,
|
|
"supportsInvalidatedEvent": false,
|
|
"supportsMemoryReferences": false,
|
|
"supportsArgsCanBeInterpretedByShell": false,
|
|
}),
|
|
INIT_TIMEOUT,
|
|
)
|
|
.await?;
|
|
*s.caps.lock() = if caps.is_object() { caps } else { json!({}) };
|
|
|
|
// The Kotlin adapter ignores the resolved classpath and project runtime on launch.
|
|
let request = if s.adapter == kotlin::ADAPTER && s.request == "launch" {
|
|
session::start_kotlin_debuggee(s, &mut arguments).await?;
|
|
"attach"
|
|
} else {
|
|
&s.request
|
|
};
|
|
if s.adapter == kotlin::ADAPTER {
|
|
let host = arguments
|
|
.get("hostName")
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| IdeError::invalid("Missing Kotlin JDWP host"))?
|
|
.to_owned();
|
|
let port = arguments
|
|
.get("port")
|
|
.and_then(Value::as_u64)
|
|
.and_then(|p| u16::try_from(p).ok())
|
|
.filter(|p| *p != 0)
|
|
.ok_or_else(|| IdeError::invalid("Invalid Kotlin JDWP port"))?;
|
|
let (relay, port) = kotlin_jdwp::StartupRelay::start(host, port).await?;
|
|
*s.kotlin_relay.lock() = Some(relay);
|
|
arguments["hostName"] = json!("127.0.0.1");
|
|
arguments["port"] = json!(port);
|
|
}
|
|
// Many adapters answer `launch` only after `configurationDone`, so don't wait for it yet.
|
|
let launch = s.conn.send_request(request, arguments)?;
|
|
let mut launch = Box::pin(async move { launch.await.unwrap_or(Err(DapError::Closed)) });
|
|
let mut initialized = s.initialized.subscribe();
|
|
let mut launch_result = None;
|
|
let got_initialized = tokio::select! {
|
|
r = initialized.wait_for(|v| *v) => { r.map_err(|_| DapError::Closed)?; true }
|
|
r = &mut launch => {
|
|
r?;
|
|
launch_result = Some(());
|
|
false
|
|
}
|
|
_ = tokio::time::sleep(LAUNCH_TIMEOUT) => return Err(DapError::Timeout(s.request.clone()).into()),
|
|
};
|
|
if !got_initialized {
|
|
// Launched before `initialized`; give the adapter a moment to send it.
|
|
let _ = tokio::time::timeout(INIT_TIMEOUT, initialized.wait_for(|v| *v)).await;
|
|
}
|
|
|
|
// A fresh adapter has no breakpoints, so a file with none enabled needs no request; the
|
|
// others go out together, as VS Code sends them, instead of one round trip per file.
|
|
let (files, idle): (Vec<_>, Vec<_>) = self
|
|
.breakpoints
|
|
.lock()
|
|
.iter()
|
|
.map(|(p, b)| (p.clone(), b.clone()))
|
|
.partition(|(_, b)| b.iter().any(|b| b.enabled));
|
|
let sent = join_all(files.iter().map(|(path, bps)| s.send_breakpoints(path, bps, None))).await;
|
|
// Breakpoint states follow the owner's first session.
|
|
let first = self.live_sessions().into_iter().find(|f| f.owner.key == s.owner.key).map(|f| f.id) == Some(s.id);
|
|
let idle = idle.into_iter().map(|(path, bps)| (path, Ok(unsent(&bps))));
|
|
for (path, result) in files.into_iter().map(|(path, _)| path).zip(sent).chain(idle) {
|
|
match result {
|
|
Ok(states) => {
|
|
if first {
|
|
self.states.lock().insert(path.clone(), states);
|
|
s.emit(ide_core::event::DebugEvent::BreakpointsChanged { path: path.into() });
|
|
}
|
|
}
|
|
Err(e) if s.adapter == kotlin::ADAPTER => return Err(e.into()),
|
|
Err(e) => tracing::debug!("setBreakpoints for {} failed: {e}", path.display()),
|
|
}
|
|
}
|
|
let functions = self.function_breakpoints.lock().clone();
|
|
if !functions.is_empty()
|
|
&& s.supports("supportsFunctionBreakpoints")
|
|
&& let Err(e) = s.send_function_breakpoints(&functions).await
|
|
{
|
|
tracing::debug!("setFunctionBreakpoints failed: {e}");
|
|
}
|
|
let data = self.data_breakpoints.lock().clone();
|
|
if !data.is_empty()
|
|
&& s.supports("supportsDataBreakpoints")
|
|
&& let Err(e) = s.send_data_breakpoints(&data).await
|
|
{
|
|
tracing::debug!("setDataBreakpoints failed: {e}");
|
|
}
|
|
let has_filters = s.caps.lock().get("exceptionBreakpointFilters").and_then(Value::as_array).is_some();
|
|
if has_filters {
|
|
let _ = self.send_exception_breakpoints(s).await;
|
|
}
|
|
if s.supports("supportsConfigurationDoneRequest") {
|
|
if s.adapter == kotlin::ADAPTER {
|
|
drop(s.conn.send_request("configurationDone", json!({}))?);
|
|
} else {
|
|
s.conn.request_timeout("configurationDone", Value::Null, REQUEST_TIMEOUT).await?;
|
|
}
|
|
}
|
|
s.configured.store(true, Ordering::Release);
|
|
if launch_result.is_none() {
|
|
match tokio::time::timeout(LAUNCH_TIMEOUT, launch).await {
|
|
Ok(r) => {
|
|
r?;
|
|
}
|
|
Err(_) => return Err(DapError::Timeout(s.request.clone()).into()),
|
|
}
|
|
}
|
|
if let Some(relay) = s.kotlin_relay.lock().as_mut() {
|
|
relay.release();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The connection dropped (adapter exited or `shutdown` finished).
|
|
pub(crate) fn session_closed(&self, s: &Arc<Session>) {
|
|
s.kill_processes();
|
|
let removed = self.sessions.lock().remove(&s.id).is_some();
|
|
self.order.lock().retain(|id| *id != s.id);
|
|
if removed && !s.terminated.swap(true, Ordering::AcqRel) {
|
|
s.emit(ide_core::event::DebugEvent::Terminated);
|
|
}
|
|
}
|
|
|
|
/// Ends a session: `terminate` (when asked and supported) → `disconnect` → kill.
|
|
pub(crate) async fn shutdown(&self, s: &Arc<Session>, terminate: bool) {
|
|
// java-debug runs the program itself and pipes its stdout over DAP. A `disconnect`
|
|
// ends that DAP session, so every line the program writes while shutting down is lost.
|
|
// Ask the program to exit ourselves and keep the adapter attached until it has drained
|
|
// the console (`terminated`), as IDEA does with the process it owns.
|
|
let pid = s.debuggee_pid.load(Ordering::Acquire);
|
|
let own_terminate = terminate && s.request == "launch" && pid > 0;
|
|
let graceful = terminate && s.request == "launch" && s.supports("supportsTerminateRequest");
|
|
if !s.conn.is_closed() {
|
|
if graceful {
|
|
let _ = s.conn.request_timeout("terminate", json!({ "restart": false }), SHUTDOWN_TIMEOUT).await;
|
|
}
|
|
if !graceful && !own_terminate {
|
|
let _ = s
|
|
.conn
|
|
.request_timeout(
|
|
"disconnect",
|
|
json!({ "restart": false, "terminateDebuggee": s.request == "launch" }),
|
|
SHUTDOWN_TIMEOUT,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
if own_terminate && !graceful {
|
|
// Signal before anything else: waiting on a request the adapter may not answer
|
|
// would delay the shutdown the console is supposed to show.
|
|
crate::session::signal_debuggee(s, false);
|
|
}
|
|
if graceful {
|
|
// java-debug sends `terminated` only after it has drained the program's console
|
|
// (it waits up to 5 s for the output pump). Close on the process id alone and the
|
|
// last shutdown lines are dropped, so wait for that event here.
|
|
let deadline = std::time::Instant::now() + crate::session::DEBUGGEE_EXIT_WAIT;
|
|
while !s.terminated.load(Ordering::Acquire) && std::time::Instant::now() < deadline {
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
}
|
|
}
|
|
// An adapter answers `disconnect` before the program it launched has exited, and that
|
|
// program keeps its port until it is gone. Reporting the session as terminated then
|
|
// lets Run start again into an occupied port, so wait for the program first — with the
|
|
// connection still open, so a second Stop can find the session and kill it.
|
|
crate::session::wait_for_debuggee(s, terminate).await;
|
|
if terminate && s.request == "launch" {
|
|
// The adapter reports `terminated` only after it has drained the program's console
|
|
// (java-debug waits up to 5 s for the pump): closing before that drops the last
|
|
// shutdown lines from the console.
|
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(6);
|
|
while !s.terminated.load(Ordering::Acquire) && !s.conn.is_closed() && std::time::Instant::now() < deadline {
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
}
|
|
}
|
|
if graceful && !s.conn.is_closed() {
|
|
let _ = s
|
|
.conn
|
|
.request_timeout(
|
|
"disconnect",
|
|
json!({ "restart": false, "terminateDebuggee": false }),
|
|
SHUTDOWN_TIMEOUT,
|
|
)
|
|
.await;
|
|
}
|
|
s.conn.close();
|
|
self.session_closed(s);
|
|
if s.adapter == "delve" && s.spec.server_host.is_none() {
|
|
let cwd = s.spec.cwd.clone();
|
|
tokio::task::spawn_blocking(move || remove_delve_binaries(&cwd));
|
|
}
|
|
}
|
|
|
|
/// Breakpoints are kept per file even without a session and sent on start.
|
|
/// Returns states from the first live session, or unverified states when none.
|
|
pub async fn set_breakpoints(
|
|
&self,
|
|
path: PathBuf,
|
|
breakpoints: Vec<Breakpoint>,
|
|
) -> IdeResult<Vec<BreakpointState>> {
|
|
{
|
|
let mut store = self.breakpoints.lock();
|
|
if breakpoints.is_empty() {
|
|
store.remove(&path);
|
|
} else {
|
|
store.insert(path.clone(), breakpoints.clone());
|
|
}
|
|
}
|
|
let mut result: Option<Vec<BreakpointState>> = None;
|
|
for s in self.live_sessions() {
|
|
if !s.configured.load(Ordering::Acquire) || s.conn.is_closed() {
|
|
continue;
|
|
}
|
|
match s.send_breakpoints(&path, &breakpoints, None).await {
|
|
Ok(states) => {
|
|
result.get_or_insert(states);
|
|
}
|
|
Err(e) if s.adapter == kotlin::ADAPTER => return Err(e.into()),
|
|
Err(e) => tracing::debug!("setBreakpoints on session {} failed: {e}", s.id.0),
|
|
}
|
|
}
|
|
let states = result.unwrap_or_else(|| unsent(&breakpoints));
|
|
self.states.lock().insert(path, states.clone());
|
|
Ok(states)
|
|
}
|
|
|
|
/// Refresh fallback copies after an editor save.
|
|
pub async fn refresh_source_mirrors(&self, owner: &str) -> IdeResult<()> {
|
|
let sessions: Vec<_> = self.sessions.lock().values().filter(|s| s.owner.key == owner).cloned().collect();
|
|
tokio::task::spawn_blocking(move || {
|
|
for s in sessions {
|
|
if let Some(mirror) = s.kotlin_mirror.lock().as_mut()
|
|
&& let Err(e) = mirror.refresh()
|
|
{
|
|
tracing::warn!("Could not refresh Kotlin debug sources: {e}");
|
|
}
|
|
}
|
|
Ok(())
|
|
})
|
|
.await
|
|
.map_err(|e| IdeError::internal(e.to_string()))?
|
|
}
|
|
|
|
/// Last known verification states of a file's breakpoints (after `BreakpointsChanged`).
|
|
pub fn breakpoint_states(&self, path: &Path) -> Vec<BreakpointState> {
|
|
self.states.lock().get(path).cloned().unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn update_breakpoint_state(&self, path: &Path, index: usize, bp: &Value) -> bool {
|
|
let mut states = self.states.lock();
|
|
let Some(slot) = states.get_mut(path).and_then(|v| v.get_mut(index)) else { return false };
|
|
let next = session::state_from(bp, slot.line);
|
|
let next = BreakpointState { message: next.message.or_else(|| slot.message.clone()), ..next };
|
|
let changed = *slot != next;
|
|
*slot = next;
|
|
changed
|
|
}
|
|
|
|
async fn send_exception_breakpoints(&self, s: &Arc<Session>) -> IdeResult<()> {
|
|
let filters = exception_filters_from(&s.caps.lock());
|
|
let enabled = enabled_filters(&filters, &self.exception_choices.lock(), &s.adapter);
|
|
s.conn.request_timeout("setExceptionBreakpoints", json!({ "filters": enabled }), REQUEST_TIMEOUT).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Exception filters offered by the adapter of each live session of `owner` (all when `None`).
|
|
pub fn exception_filters(&self, owner: Option<&str>) -> Vec<SessionExceptionFilters> {
|
|
self.live_sessions()
|
|
.iter()
|
|
.filter(|s| owner.is_none_or(|o| s.owner.key == o))
|
|
.map(|s| SessionExceptionFilters {
|
|
session: s.id,
|
|
adapter: s.adapter.clone(),
|
|
filters: exception_filters_from(&s.caps.lock()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Stores exception filter choices (filter id → enabled; kept without a session, applied on
|
|
/// start) and sends them to every configured session that offers exception filters.
|
|
pub async fn set_exception_breakpoints(&self, choices: BTreeMap<String, bool>) -> IdeResult<()> {
|
|
*self.exception_choices.lock() = choices;
|
|
for s in self.live_sessions() {
|
|
if !s.configured.load(Ordering::Acquire) || s.conn.is_closed() {
|
|
continue;
|
|
}
|
|
if exception_filters_from(&s.caps.lock()).is_empty() {
|
|
continue;
|
|
}
|
|
if let Err(e) = self.send_exception_breakpoints(&s).await {
|
|
tracing::debug!("setExceptionBreakpoints on session {} failed: {e}", s.id.0);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Changes a variable's value: `setVariable` in its container, or `setExpression`
|
|
/// on its evaluate name when the adapter only supports that. Returns the new value.
|
|
pub async fn set_variable(
|
|
&self,
|
|
s: DebugSessionId,
|
|
container: i64,
|
|
name: String,
|
|
evaluate_name: Option<String>,
|
|
frame_id: Option<i64>,
|
|
value: String,
|
|
) -> IdeResult<Variable> {
|
|
let session = self.session(s)?;
|
|
let body = if container > 0 && session.supports("supportsSetVariable") {
|
|
session
|
|
.conn
|
|
.request_timeout(
|
|
"setVariable",
|
|
json!({ "variablesReference": container, "name": name, "value": value }),
|
|
REQUEST_TIMEOUT,
|
|
)
|
|
.await?
|
|
} else if let (Some(expr), true) = (evaluate_name.as_ref(), session.supports("supportsSetExpression")) {
|
|
let mut args = json!({ "expression": expr, "value": value });
|
|
if let Some(f) = frame_id {
|
|
args["frameId"] = f.into();
|
|
}
|
|
session.conn.request_timeout("setExpression", args, REQUEST_TIMEOUT).await?
|
|
} else {
|
|
return Err(IdeError::unavailable(format!("debug adapter `{}` cannot change values", session.adapter)));
|
|
};
|
|
Ok(Variable {
|
|
name: name.clone(),
|
|
value: body.get("value").and_then(Value::as_str).unwrap_or(&value).to_owned(),
|
|
type_name: body.get("type").and_then(Value::as_str).filter(|t| !t.is_empty()).map(str::to_owned),
|
|
variables_reference: body.get("variablesReference").and_then(Value::as_i64).unwrap_or(0),
|
|
evaluate_name: evaluate_name.or(Some(name)),
|
|
})
|
|
}
|
|
|
|
pub async fn threads(&self, s: DebugSessionId) -> IdeResult<Vec<ThreadInfo>> {
|
|
let body = self.session(s)?.conn.request_timeout("threads", Value::Null, REQUEST_TIMEOUT).await?;
|
|
Ok(body
|
|
.get("threads")
|
|
.and_then(Value::as_array)
|
|
.map(|t| {
|
|
t.iter()
|
|
.map(|t| ThreadInfo {
|
|
id: t.get("id").and_then(Value::as_i64).unwrap_or_default(),
|
|
name: t.get("name").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
pub async fn stack_trace(&self, s: DebugSessionId, thread_id: i64) -> IdeResult<Vec<StackFrame>> {
|
|
let session = self.session(s)?;
|
|
let body = session
|
|
.conn
|
|
.request_timeout(
|
|
"stackTrace",
|
|
json!({ "threadId": thread_id, "startFrame": 0, "levels": 400 }),
|
|
REQUEST_TIMEOUT,
|
|
)
|
|
.await?;
|
|
let frame = |f: &Value| {
|
|
let mut frame = frame_from(f);
|
|
if let Some(client) = frame.path.as_deref().and_then(|p| session.client_path(&p.to_string_lossy())) {
|
|
frame.path = Some(client.into());
|
|
}
|
|
frame
|
|
};
|
|
Ok(body.get("stackFrames").and_then(Value::as_array).map(|f| f.iter().map(frame).collect()).unwrap_or_default())
|
|
}
|
|
|
|
pub async fn scopes(&self, s: DebugSessionId, frame_id: i64) -> IdeResult<Vec<Scope>> {
|
|
let body =
|
|
self.session(s)?.conn.request_timeout("scopes", json!({ "frameId": frame_id }), REQUEST_TIMEOUT).await?;
|
|
Ok(body
|
|
.get("scopes")
|
|
.and_then(Value::as_array)
|
|
.map(|sc| {
|
|
sc.iter()
|
|
.map(|sc| Scope {
|
|
name: sc.get("name").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
variables_reference: sc.get("variablesReference").and_then(Value::as_i64).unwrap_or(0),
|
|
expensive: sc.get("expensive").and_then(Value::as_bool).unwrap_or(false),
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
pub async fn variables(&self, s: DebugSessionId, reference: i64) -> IdeResult<Vec<Variable>> {
|
|
let body = self
|
|
.session(s)?
|
|
.conn
|
|
.request_timeout("variables", json!({ "variablesReference": reference }), REQUEST_TIMEOUT)
|
|
.await?;
|
|
Ok(body
|
|
.get("variables")
|
|
.and_then(Value::as_array)
|
|
.map(|v| v.iter().map(variable_from).collect())
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
/// Evaluates in the `repl` context.
|
|
pub async fn evaluate(&self, s: DebugSessionId, frame_id: Option<i64>, expression: String) -> IdeResult<Variable> {
|
|
self.evaluate_in(s, frame_id, expression, None).await
|
|
}
|
|
|
|
/// Evaluates with an explicit DAP context: `repl` (default), `watch`, `hover`, `clipboard`.
|
|
pub async fn evaluate_in(
|
|
&self,
|
|
s: DebugSessionId,
|
|
frame_id: Option<i64>,
|
|
expression: String,
|
|
context: Option<String>,
|
|
) -> IdeResult<Variable> {
|
|
let session = self.session(s)?;
|
|
let mut context = context.unwrap_or_else(|| "repl".into());
|
|
if context == "hover" && !session.supports("supportsEvaluateForHovers") {
|
|
context = "watch".into();
|
|
}
|
|
let mut args = json!({ "expression": expression, "context": context });
|
|
if let Some(f) = frame_id {
|
|
args["frameId"] = f.into();
|
|
}
|
|
let body = session.conn.request_timeout("evaluate", args, REQUEST_TIMEOUT).await?;
|
|
Ok(Variable {
|
|
name: expression.clone(),
|
|
value: body.get("result").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
type_name: body.get("type").and_then(Value::as_str).filter(|t| !t.is_empty()).map(str::to_owned),
|
|
variables_reference: body.get("variablesReference").and_then(Value::as_i64).unwrap_or(0),
|
|
evaluate_name: Some(expression),
|
|
})
|
|
}
|
|
|
|
/// Completions for `text` at `line` / `column` (0-based, UTF-16) in the frame's scope; none
|
|
/// when the adapter doesn't offer them.
|
|
pub async fn completions(
|
|
&self,
|
|
s: DebugSessionId,
|
|
frame_id: Option<i64>,
|
|
text: String,
|
|
line: u32,
|
|
column: u32,
|
|
) -> IdeResult<Vec<DebugCompletion>> {
|
|
let session = self.session(s)?;
|
|
if !session.supports("supportsCompletionsRequest") {
|
|
return Ok(Vec::new());
|
|
}
|
|
let mut args = json!({ "text": text, "line": line + 1, "column": column + 1 });
|
|
if let Some(f) = frame_id {
|
|
args["frameId"] = f.into();
|
|
}
|
|
let body = session.conn.request_timeout("completions", args, REQUEST_TIMEOUT).await?;
|
|
let text =
|
|
|v: &Value, key: &str| v.get(key).and_then(Value::as_str).filter(|t| !t.is_empty()).map(str::to_owned);
|
|
Ok(body
|
|
.get("targets")
|
|
.and_then(Value::as_array)
|
|
.map(|targets| {
|
|
targets
|
|
.iter()
|
|
.filter_map(|t| {
|
|
Some(DebugCompletion {
|
|
label: text(t, "label")?,
|
|
text: text(t, "text"),
|
|
kind: text(t, "type"),
|
|
detail: text(t, "detail"),
|
|
sort_text: text(t, "sortText"),
|
|
})
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
pub async fn resume(&self, s: DebugSessionId, thread_id: i64) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
let body = session.conn.request_timeout("continue", json!({ "threadId": thread_id }), REQUEST_TIMEOUT).await?;
|
|
let all = body.get("allThreadsContinued").and_then(Value::as_bool).unwrap_or(true);
|
|
session.emit(ide_core::event::DebugEvent::Continued { thread_id, all_threads_continued: all });
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn pause(&self, s: DebugSessionId, thread_id: i64) -> IdeResult<()> {
|
|
self.session(s)?.conn.request_timeout("pause", json!({ "threadId": thread_id }), REQUEST_TIMEOUT).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Types `text` into the terminal the session's program runs in (see [`ide_core::event::DebugEvent::ProgramInput`]).
|
|
pub fn send_input(&self, s: DebugSessionId, text: &str) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
let mut program = session.program.lock();
|
|
let program = program
|
|
.as_mut()
|
|
.ok_or_else(|| IdeError::invalid("The program doesn't read input from the debug console"))?;
|
|
program.write(text).map_err(|e| IdeError::internal(format!("cannot write to the program: {e}")))
|
|
}
|
|
|
|
/// Flutter's DAP adapter: injects the changed sources into the running app.
|
|
/// Not part of the DAP standard; adapters that do not know the request reject it.
|
|
pub async fn hot_reload(&self, s: DebugSessionId) -> IdeResult<()> {
|
|
self.session(s)?.conn.request_timeout("hotReload", json!({ "reason": "manual" }), REQUEST_TIMEOUT).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Flutter's DAP adapter: restarts the app without preserving state.
|
|
pub async fn hot_restart(&self, s: DebugSessionId) -> IdeResult<()> {
|
|
self.session(s)?.conn.request_timeout("hotRestart", json!({ "reason": "manual" }), REQUEST_TIMEOUT).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn step(&self, s: DebugSessionId, thread_id: i64, kind: StepKind) -> IdeResult<()> {
|
|
let command = match kind {
|
|
StepKind::Over => "next",
|
|
StepKind::Into => "stepIn",
|
|
StepKind::Out => "stepOut",
|
|
};
|
|
let session = self.session(s)?;
|
|
session.conn.request_timeout(command, json!({ "threadId": thread_id }), REQUEST_TIMEOUT).await?;
|
|
session.emit(ide_core::event::DebugEvent::Continued { thread_id, all_threads_continued: true });
|
|
Ok(())
|
|
}
|
|
|
|
/// Run to cursor: a temporary breakpoint at `line` (0-based) plus `continue`;
|
|
/// the file's real breakpoints are restored on the next stop. `force` (Force Run
|
|
/// to Cursor) also suspends every other breakpoint until then.
|
|
pub async fn run_to(
|
|
&self,
|
|
s: DebugSessionId,
|
|
thread_id: i64,
|
|
path: PathBuf,
|
|
line: u32,
|
|
force: bool,
|
|
) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
if force {
|
|
self.suspend_breakpoints(&session, Some(&path)).await?;
|
|
}
|
|
let stored = self.stored_breakpoints(&path);
|
|
let sent: Vec<Breakpoint> =
|
|
if force { stored.iter().map(|b| Breakpoint { enabled: false, ..b.clone() }).collect() } else { stored };
|
|
session.send_breakpoints(&path, &sent, Some(line)).await?;
|
|
session.pending_restore.lock().files.insert(path);
|
|
self.resume(s, thread_id).await
|
|
}
|
|
|
|
/// Sends empty breakpoint lists (line, function, data) to `session` until its next
|
|
/// stop, when the stored ones are restored. `except` is left to the caller.
|
|
async fn suspend_breakpoints(&self, session: &Arc<Session>, except: Option<&Path>) -> IdeResult<()> {
|
|
// While paused the adapter holds the stored lists: only files with something enabled change.
|
|
let files: Vec<PathBuf> = self
|
|
.breakpoints
|
|
.lock()
|
|
.iter()
|
|
.filter(|(path, bps)| Some(path.as_path()) != except && bps.iter().any(|b| b.enabled))
|
|
.map(|(path, _)| path.clone())
|
|
.collect();
|
|
let sent = join_all(files.iter().map(|path| session.send_breakpoints(path, &[], None))).await;
|
|
let mut failed = None;
|
|
for (path, result) in files.into_iter().zip(sent) {
|
|
match result {
|
|
Ok(_) => {
|
|
session.pending_restore.lock().files.insert(path);
|
|
}
|
|
Err(e) => failed = failed.or(Some(e)),
|
|
}
|
|
}
|
|
if let Some(e) = failed {
|
|
return Err(e.into());
|
|
}
|
|
if !self.function_breakpoints.lock().is_empty() && session.supports("supportsFunctionBreakpoints") {
|
|
session.send_function_breakpoints(&[]).await?;
|
|
session.pending_restore.lock().functions = true;
|
|
}
|
|
if !self.data_breakpoints.lock().is_empty() && session.supports("supportsDataBreakpoints") {
|
|
session.send_data_breakpoints(&[]).await?;
|
|
session.pending_restore.lock().data = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Re-sends what [`suspend_breakpoints`](Self::suspend_breakpoints) / run to cursor replaced.
|
|
pub(crate) async fn restore_breakpoints(&self, session: &Arc<Session>) {
|
|
let pending = std::mem::take(&mut *session.pending_restore.lock());
|
|
let files: Vec<(PathBuf, Vec<Breakpoint>)> =
|
|
pending.files.into_iter().map(|path| (path.clone(), self.stored_breakpoints(&path))).collect();
|
|
let sent = join_all(files.iter().map(|(path, bps)| session.send_breakpoints(path, bps, None))).await;
|
|
for ((path, _), result) in files.iter().zip(sent) {
|
|
if let Err(e) = result {
|
|
tracing::debug!("restoring breakpoints of {} failed: {e}", path.display());
|
|
}
|
|
}
|
|
if pending.functions {
|
|
let functions = self.function_breakpoints.lock().clone();
|
|
if let Err(e) = session.send_function_breakpoints(&functions).await {
|
|
tracing::debug!("restoring function breakpoints failed: {e}");
|
|
}
|
|
}
|
|
if pending.data {
|
|
let data = self.data_breakpoints.lock().clone();
|
|
if let Err(e) = session.send_data_breakpoints(&data).await {
|
|
tracing::debug!("restoring data breakpoints failed: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Force Step Over: `next` with every breakpoint suspended until the step ends.
|
|
pub async fn force_step_over(&self, s: DebugSessionId, thread_id: i64) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
self.suspend_breakpoints(&session, None).await?;
|
|
self.step(s, thread_id, StepKind::Over).await
|
|
}
|
|
|
|
/// Step into a specific call of the current line (Smart Step Into).
|
|
pub async fn step_in_target(&self, s: DebugSessionId, thread_id: i64, target_id: i64) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
session
|
|
.conn
|
|
.request_timeout("stepIn", json!({ "threadId": thread_id, "targetId": target_id }), REQUEST_TIMEOUT)
|
|
.await?;
|
|
session.emit(ide_core::event::DebugEvent::Continued { thread_id, all_threads_continued: true });
|
|
Ok(())
|
|
}
|
|
|
|
/// Calls on the current line of `frame_id` that can be stepped into.
|
|
pub async fn step_in_targets(&self, s: DebugSessionId, frame_id: i64) -> IdeResult<Vec<StepInTarget>> {
|
|
let session = self.session(s)?;
|
|
if !session.supports("supportsStepInTargetsRequest") {
|
|
return Err(IdeError::unavailable(format!(
|
|
"debug adapter `{}` does not support Smart Step Into",
|
|
session.adapter
|
|
)));
|
|
}
|
|
let body =
|
|
session.conn.request_timeout("stepInTargets", json!({ "frameId": frame_id }), REQUEST_TIMEOUT).await?;
|
|
Ok(body
|
|
.get("targets")
|
|
.and_then(Value::as_array)
|
|
.map(|t| t.iter().map(step_in_target_from).collect())
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
/// Reset Frame (IDEA's Drop Frame): `restartFrame`, so execution re-enters the frame.
|
|
pub async fn restart_frame(&self, s: DebugSessionId, frame_id: i64) -> IdeResult<()> {
|
|
let session = self.session(s)?;
|
|
if !session.supports("supportsRestartFrame") {
|
|
return Err(IdeError::unavailable(format!("debug adapter `{}` cannot reset frames", session.adapter)));
|
|
}
|
|
session.conn.request_timeout("restartFrame", json!({ "frameId": frame_id }), REQUEST_TIMEOUT).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn capabilities(&self, s: DebugSessionId) -> IdeResult<DebugCapabilities> {
|
|
let session = self.session(s)?;
|
|
let mut caps = capabilities_from(&session.caps.lock());
|
|
caps.hot_code_replace = HOT_CODE_REPLACE_ADAPTERS.contains(&session.adapter.as_str());
|
|
Ok(caps)
|
|
}
|
|
|
|
/// HotSwap: reloads the classes compiled since the session started (or since the
|
|
/// last reload) into the debuggee (java-debug `redefineClasses`).
|
|
pub async fn redefine_classes(&self, s: DebugSessionId) -> IdeResult<HotSwapResult> {
|
|
let session = self.session(s)?;
|
|
if !HOT_CODE_REPLACE_ADAPTERS.contains(&session.adapter.as_str()) {
|
|
return Err(IdeError::unavailable(format!(
|
|
"debug adapter `{}` cannot reload changed classes",
|
|
session.adapter
|
|
)));
|
|
}
|
|
let body = session.conn.request_timeout("redefineClasses", json!({}), HOT_SWAP_TIMEOUT).await?;
|
|
Ok(hot_swap_result(&body))
|
|
}
|
|
|
|
/// Stores method breakpoints (kept without a session, sent on start) and sends them to
|
|
/// every configured session whose adapter supports them. States from the first one.
|
|
pub async fn set_function_breakpoints(&self, list: Vec<FunctionBreakpoint>) -> IdeResult<Vec<BreakpointState>> {
|
|
*self.function_breakpoints.lock() = list.clone();
|
|
let mut result = None;
|
|
for s in self.live_sessions() {
|
|
if !s.configured.load(Ordering::Acquire) || s.conn.is_closed() || !s.supports("supportsFunctionBreakpoints")
|
|
{
|
|
continue;
|
|
}
|
|
match s.send_function_breakpoints(&list).await {
|
|
Ok(states) => {
|
|
result.get_or_insert(states);
|
|
}
|
|
Err(e) => tracing::debug!("setFunctionBreakpoints on session {} failed: {e}", s.id.0),
|
|
}
|
|
}
|
|
Ok(result.unwrap_or_else(|| unverified(list.iter().map(|b| b.enabled))))
|
|
}
|
|
|
|
/// Stores field watchpoints and sends them to every configured session that supports them.
|
|
pub async fn set_data_breakpoints(&self, list: Vec<DataBreakpoint>) -> IdeResult<Vec<BreakpointState>> {
|
|
*self.data_breakpoints.lock() = list.clone();
|
|
let mut result = None;
|
|
for s in self.live_sessions() {
|
|
if !s.configured.load(Ordering::Acquire) || s.conn.is_closed() || !s.supports("supportsDataBreakpoints") {
|
|
continue;
|
|
}
|
|
match s.send_data_breakpoints(&list).await {
|
|
Ok(states) => {
|
|
result.get_or_insert(states);
|
|
}
|
|
Err(e) => tracing::debug!("setDataBreakpoints on session {} failed: {e}", s.id.0),
|
|
}
|
|
}
|
|
Ok(result.unwrap_or_else(|| unverified(list.iter().map(|b| b.enabled))))
|
|
}
|
|
|
|
/// Whether (and how) `name` in the variables container `container` can be watched.
|
|
pub async fn data_breakpoint_info(
|
|
&self,
|
|
s: DebugSessionId,
|
|
container: i64,
|
|
name: String,
|
|
frame_id: Option<i64>,
|
|
) -> IdeResult<DataBreakpointInfo> {
|
|
let session = self.session(s)?;
|
|
if !session.supports("supportsDataBreakpoints") {
|
|
return Err(IdeError::unavailable(format!(
|
|
"debug adapter `{}` does not support field watchpoints",
|
|
session.adapter
|
|
)));
|
|
}
|
|
let mut args = json!({ "name": name });
|
|
if container > 0 {
|
|
args["variablesReference"] = container.into();
|
|
}
|
|
if let Some(f) = frame_id {
|
|
args["frameId"] = f.into();
|
|
}
|
|
let body = session.conn.request_timeout("dataBreakpointInfo", args, REQUEST_TIMEOUT).await?;
|
|
Ok(DataBreakpointInfo {
|
|
data_id: body.get("dataId").and_then(Value::as_str).map(str::to_owned),
|
|
description: body.get("description").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
|
access_types: body
|
|
.get("accessTypes")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default(),
|
|
})
|
|
}
|
|
|
|
/// Stops every session `key` started: the window that opened the project is closing.
|
|
pub async fn stop_key(&self, key: &str) {
|
|
let ids: Vec<DebugSessionId> = self.sessions(Some(key)).into_iter().map(|s| s.id).collect();
|
|
for id in ids {
|
|
if let Err(e) = self.stop(id).await {
|
|
tracing::debug!("debug session {id:?} did not stop: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stops every debug session started from `root` or below — closing a project stops the
|
|
/// programs it launched even when the session's owner key does not name the window doing
|
|
/// the closing (window groups, auxiliary windows).
|
|
pub async fn stop_under(&self, root: &Path) {
|
|
let ids: Vec<DebugSessionId> =
|
|
self.live_sessions().into_iter().filter(|s| s.spec.cwd.starts_with(root)).map(|s| s.id).collect();
|
|
tracing::info!(root = %root.display(), sessions = ids.len(), "closing project: stopping debug sessions");
|
|
for id in ids {
|
|
if let Err(e) = self.stop(id).await {
|
|
tracing::debug!("debug session {id:?} did not stop: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stops a session and its child sessions. A second Stop while the first one waits kills
|
|
/// the program instead of waiting for it — even after the adapter dropped the session.
|
|
pub async fn stop(&self, s: DebugSessionId) -> IdeResult<()> {
|
|
{
|
|
let in_flight = self.stopping.lock();
|
|
if let Some(session) = in_flight.get(&s) {
|
|
session.stop_force.store(true, Ordering::Release);
|
|
return Ok(());
|
|
}
|
|
}
|
|
let session = self.session(s)?;
|
|
self.stopping.lock().insert(s, session.clone());
|
|
let result = self.stop_tree(session).await;
|
|
self.stopping.lock().remove(&s);
|
|
result
|
|
}
|
|
|
|
async fn stop_tree(&self, session: Arc<Session>) -> IdeResult<()> {
|
|
let children: Vec<Arc<Session>> =
|
|
self.live_sessions().into_iter().filter(|c| c.parent == Some(session.id)).collect();
|
|
for child in children {
|
|
self.shutdown(&child, true).await;
|
|
}
|
|
self.shutdown(&session, true).await;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|