Файли
openidea/crates/ide-host/src/ops/debug.rs
T
l0ng-ai c821ab144c fix(run): Give forked and debugged JVMs the console encoding and render colours in the Debug console (#220)
Only the JVM the IDE launched directly was told the console's output encoding, so forked JVMs (spring-boot:run, Surefire, bootRun, Gradle workers), debugger-launched JVMs and Build-window builds wrote GBK on Windows while the console decoded UTF-8. Every JVM the IDE starts on Windows now gets the console encoding: java gets -Dfile.encoding alongside the std* properties, build tools and their forks get JAVA_TOOL_OPTIONS (std* only) with the IDE's own 'Picked up JAVA_TOOL_OPTIONS' line filtered, shell command lines are recognised by their first word, and java-debug launches get the matching vmArgs. The Debug console now shares the Run console's ANSI parser. Encoding helpers moved to ide-core::output_encoding.

Not verified on a Windows machine.

Fixes #180
Fixes #182

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
2026-10-29 11:58:19 +08:00

422 рядки
15 KiB
Rust

use crate::Host;
use crate::rpc::Dispatcher;
use ide_core::{DebugSessionId, IdeResult};
use ide_dap::*;
use std::path::PathBuf;
/// Starts a session owned by this window: its events (and those of child sessions) go here only.
pub async fn debug_start(host: &Host, window: &str, config: LaunchConfig) -> IdeResult<DebugSessionId> {
let config = match_java_debug_encoding(config, cfg!(windows));
let label = window;
let owner = SessionOwner::new(host.owner_window(label), host.window_sink(label));
let settings = match host.project(label) {
Ok(project) => project.settings.effective(),
Err(_) => host.global_settings.effective(),
};
let root = config.cwd.clone().map(PathBuf::from).unwrap_or_default();
let custom = |command: &str| {
let path = ide_sdk::apply::tool_command(&root, &settings, command)?;
Some(path.to_string_lossy().into_owned())
};
host.debugger.start_with(config, owner, custom).await
}
/// A JVM launched by java-debug writes what java-debug decodes its output as
/// (see [`ide_core::output_encoding::java_debug_vm_args`]).
fn match_java_debug_encoding(mut config: LaunchConfig, windows: bool) -> LaunchConfig {
if config.adapter != "java-debug" || config.request != "launch" {
return config;
}
let Some(serde_json::Value::Object(mut extra)) =
config.extra_json.as_deref().and_then(|e| serde_json::from_str(e).ok())
else {
return config;
};
let vm_args = extra.get("vmArgs").and_then(|v| v.as_str()).unwrap_or_default();
let encoding = extra.get("encoding").and_then(|v| v.as_str());
if let Some(vm_args) = ide_core::output_encoding::java_debug_vm_args(vm_args, encoding, windows) {
extra.insert("vmArgs".into(), vm_args.into());
config.extra_json = Some(serde_json::Value::Object(extra).to_string());
}
config
}
pub async fn debug_set_breakpoints(
host: &Host,
_window: &str,
path: PathBuf,
breakpoints: Vec<Breakpoint>,
) -> IdeResult<Vec<BreakpointState>> {
host.debugger.set_breakpoints(path, breakpoints).await
}
pub async fn debug_threads(host: &Host, _window: &str, session: DebugSessionId) -> IdeResult<Vec<ThreadInfo>> {
host.debugger.threads(session).await
}
pub async fn debug_stack_trace(
host: &Host,
_window: &str,
session: DebugSessionId,
thread_id: i64,
) -> IdeResult<Vec<StackFrame>> {
host.debugger.stack_trace(session, thread_id).await
}
pub async fn debug_scopes(host: &Host, _window: &str, session: DebugSessionId, frame_id: i64) -> IdeResult<Vec<Scope>> {
host.debugger.scopes(session, frame_id).await
}
pub async fn debug_variables(
host: &Host,
_window: &str,
session: DebugSessionId,
reference: i64,
) -> IdeResult<Vec<Variable>> {
host.debugger.variables(session, reference).await
}
pub async fn debug_evaluate(
host: &Host,
_window: &str,
session: DebugSessionId,
frame_id: Option<i64>,
expression: String,
) -> IdeResult<Variable> {
host.debugger.evaluate(session, frame_id, expression).await
}
pub async fn debug_resume(host: &Host, _window: &str, session: DebugSessionId, thread_id: i64) -> IdeResult<()> {
host.debugger.resume(session, thread_id).await
}
pub async fn debug_pause(host: &Host, _window: &str, session: DebugSessionId, thread_id: i64) -> IdeResult<()> {
host.debugger.pause(session, thread_id).await
}
pub async fn debug_step(
host: &Host,
_window: &str,
session: DebugSessionId,
thread_id: i64,
kind: StepKind,
) -> IdeResult<()> {
host.debugger.step(session, thread_id, kind).await
}
pub async fn debug_run_to(
host: &Host,
_window: &str,
session: DebugSessionId,
thread_id: i64,
path: PathBuf,
line: u32,
force: bool,
) -> IdeResult<()> {
host.debugger.run_to(session, thread_id, path, line, force).await
}
pub async fn debug_stop(host: &Host, _window: &str, session: DebugSessionId) -> IdeResult<()> {
host.debugger.stop(session).await
}
/// Evaluate with an explicit DAP context: `repl`, `watch`, `hover` or `clipboard`.
pub async fn debug_evaluate_in(
host: &Host,
_window: &str,
session: DebugSessionId,
frame_id: Option<i64>,
expression: String,
context: Option<String>,
) -> IdeResult<Variable> {
host.debugger.evaluate_in(session, frame_id, expression, context).await
}
/// Last known verification states of a file's breakpoints (refetch on `breakpointsChanged`).
pub fn debug_breakpoint_states(host: &Host, _window: &str, path: PathBuf) -> IdeResult<Vec<BreakpointState>> {
Ok(host.debugger.breakpoint_states(&path))
}
/// This window's live sessions, including adapter-started child sessions.
pub fn debug_sessions(host: &Host, window: &str) -> IdeResult<Vec<SessionInfo>> {
Ok(host.debugger.sessions(Some(host.owner_window(window))))
}
pub fn debug_exception_filters(host: &Host, window: &str) -> IdeResult<Vec<SessionExceptionFilters>> {
Ok(host.debugger.exception_filters(Some(host.owner_window(window))))
}
pub async fn debug_set_exception_breakpoints(
host: &Host,
_window: &str,
choices: std::collections::BTreeMap<String, bool>,
) -> IdeResult<()> {
host.debugger.set_exception_breakpoints(choices).await
}
#[allow(clippy::too_many_arguments)]
pub async fn debug_set_variable(
host: &Host,
_window: &str,
session: DebugSessionId,
container: i64,
name: String,
evaluate_name: Option<String>,
frame_id: Option<i64>,
value: String,
) -> IdeResult<Variable> {
host.debugger.set_variable(session, container, name, evaluate_name, frame_id, value).await
}
/// Force Step Over: step over with every breakpoint suspended until the step ends.
pub async fn debug_force_step_over(
host: &Host,
_window: &str,
session: DebugSessionId,
thread_id: i64,
) -> IdeResult<()> {
host.debugger.force_step_over(session, thread_id).await
}
/// Smart Step Into: the calls on the current line of `frame_id`.
pub async fn debug_step_in_targets(
host: &Host,
_window: &str,
session: DebugSessionId,
frame_id: i64,
) -> IdeResult<Vec<StepInTarget>> {
host.debugger.step_in_targets(session, frame_id).await
}
pub async fn debug_step_in_target(
host: &Host,
_window: &str,
session: DebugSessionId,
thread_id: i64,
target_id: i64,
) -> IdeResult<()> {
host.debugger.step_in_target(session, thread_id, target_id).await
}
/// Reset Frame (Drop Frame).
pub async fn debug_restart_frame(host: &Host, _window: &str, session: DebugSessionId, frame_id: i64) -> IdeResult<()> {
host.debugger.restart_frame(session, frame_id).await
}
pub fn debug_capabilities(host: &Host, _window: &str, session: DebugSessionId) -> IdeResult<DebugCapabilities> {
host.debugger.capabilities(session)
}
/// Method breakpoints (all files); states aligned with `breakpoints`.
pub async fn debug_set_function_breakpoints(
host: &Host,
_window: &str,
breakpoints: Vec<FunctionBreakpoint>,
) -> IdeResult<Vec<BreakpointState>> {
host.debugger.set_function_breakpoints(breakpoints).await
}
/// Field watchpoints; states aligned with `breakpoints`.
pub async fn debug_set_data_breakpoints(
host: &Host,
_window: &str,
breakpoints: Vec<DataBreakpoint>,
) -> IdeResult<Vec<BreakpointState>> {
host.debugger.set_data_breakpoints(breakpoints).await
}
pub async fn debug_data_breakpoint_info(
host: &Host,
_window: &str,
session: DebugSessionId,
container: i64,
name: String,
frame_id: Option<i64>,
) -> IdeResult<DataBreakpointInfo> {
host.debugger.data_breakpoint_info(session, container, name, frame_id).await
}
/// Attach to Process: the user's processes a debugger can attach to.
pub async fn debug_attachable_processes(_host: &Host, _window: &str) -> IdeResult<Vec<attach::AttachableProcess>> {
tokio::task::spawn_blocking(attach::attachable_processes)
.await
.map_err(|e| ide_core::IdeError::internal(format!("listing processes failed: {e}")))?
}
/// Remote JVM Debug in listen mode: waits (cancellable through `request`) for the remote
/// JVM to connect to `host:port`; returns the loopback port java-debug attaches to.
pub async fn debug_listen_for_jvm(
host: &Host,
_window: &str,
request: ide_core::RequestId,
jvm_host: String,
port: u16,
) -> IdeResult<u16> {
let token = host.begin_request(request);
let result = jdwp::listen_for_jvm(&jvm_host, port, token).await;
host.end_request(request);
result
}
/// HotSwap (Reload Changed Classes) in a java-debug session.
pub async fn debug_redefine_classes(host: &Host, _window: &str, session: DebugSessionId) -> IdeResult<HotSwapResult> {
host.debugger.redefine_classes(session).await
}
/// Commands of this module the remote server answers.
pub(crate) fn register(d: &mut Dispatcher) {
d.add("debug_start", |c| async move { debug_start(&c.host, &c.window, c.arg("config")?).await });
d.add("debug_set_breakpoints", |c| async move {
debug_set_breakpoints(&c.host, &c.window, c.arg("path")?, c.arg("breakpoints")?).await
});
d.add("debug_threads", |c| async move { debug_threads(&c.host, &c.window, c.arg("session")?).await });
d.add("debug_stack_trace", |c| async move {
debug_stack_trace(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?).await
});
d.add(
"debug_scopes",
|c| async move { debug_scopes(&c.host, &c.window, c.arg("session")?, c.arg("frameId")?).await },
);
d.add("debug_variables", |c| async move {
debug_variables(&c.host, &c.window, c.arg("session")?, c.arg("reference")?).await
});
d.add("debug_evaluate", |c| async move {
debug_evaluate(&c.host, &c.window, c.arg("session")?, c.arg("frameId")?, c.arg("expression")?).await
});
d.add(
"debug_resume",
|c| async move { debug_resume(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?).await },
);
d.add(
"debug_pause",
|c| async move { debug_pause(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?).await },
);
d.add("debug_step", |c| async move {
debug_step(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?, c.arg("kind")?).await
});
d.add("debug_run_to", |c| async move {
debug_run_to(
&c.host,
&c.window,
c.arg("session")?,
c.arg("threadId")?,
c.arg("path")?,
c.arg("line")?,
c.arg("force")?,
)
.await
});
d.add("debug_stop", |c| async move { debug_stop(&c.host, &c.window, c.arg("session")?).await });
d.add("debug_evaluate_in", |c| async move {
debug_evaluate_in(
&c.host,
&c.window,
c.arg("session")?,
c.arg("frameId")?,
c.arg("expression")?,
c.arg("context")?,
)
.await
});
d.add("debug_breakpoint_states", |c| async move { debug_breakpoint_states(&c.host, &c.window, c.arg("path")?) });
d.add("debug_sessions", |c| async move { debug_sessions(&c.host, &c.window) });
d.add("debug_exception_filters", |c| async move { debug_exception_filters(&c.host, &c.window) });
d.add("debug_set_exception_breakpoints", |c| async move {
debug_set_exception_breakpoints(&c.host, &c.window, c.arg("choices")?).await
});
d.add("debug_set_variable", |c| async move {
debug_set_variable(
&c.host,
&c.window,
c.arg("session")?,
c.arg("container")?,
c.arg("name")?,
c.arg("evaluateName")?,
c.arg("frameId")?,
c.arg("value")?,
)
.await
});
d.add("debug_force_step_over", |c| async move {
debug_force_step_over(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?).await
});
d.add("debug_step_in_targets", |c| async move {
debug_step_in_targets(&c.host, &c.window, c.arg("session")?, c.arg("frameId")?).await
});
d.add("debug_step_in_target", |c| async move {
debug_step_in_target(&c.host, &c.window, c.arg("session")?, c.arg("threadId")?, c.arg("targetId")?).await
});
d.add("debug_restart_frame", |c| async move {
debug_restart_frame(&c.host, &c.window, c.arg("session")?, c.arg("frameId")?).await
});
d.add("debug_capabilities", |c| async move { debug_capabilities(&c.host, &c.window, c.arg("session")?) });
d.add("debug_set_function_breakpoints", |c| async move {
debug_set_function_breakpoints(&c.host, &c.window, c.arg("breakpoints")?).await
});
d.add("debug_set_data_breakpoints", |c| async move {
debug_set_data_breakpoints(&c.host, &c.window, c.arg("breakpoints")?).await
});
d.add("debug_data_breakpoint_info", |c| async move {
debug_data_breakpoint_info(
&c.host,
&c.window,
c.arg("session")?,
c.arg("container")?,
c.arg("name")?,
c.arg("frameId")?,
)
.await
});
d.add("debug_attachable_processes", |c| async move { debug_attachable_processes(&c.host, &c.window).await });
d.add("debug_listen_for_jvm", |c| async move {
debug_listen_for_jvm(&c.host, &c.window, c.arg("request")?, c.arg("host")?, c.arg("port")?).await
});
d.add(
"debug_redefine_classes",
|c| async move { debug_redefine_classes(&c.host, &c.window, c.arg("session")?).await },
);
}
#[cfg(test)]
mod tests {
use super::*;
fn launch(adapter: &str, request: &str, extra: serde_json::Value) -> LaunchConfig {
LaunchConfig {
name: "DromaraApplication".into(),
adapter: adapter.into(),
request: request.into(),
program: None,
args: vec![],
cwd: None,
env: Default::default(),
extra_json: Some(extra.to_string()),
}
}
fn vm_args(config: &LaunchConfig) -> serde_json::Value {
serde_json::from_str::<serde_json::Value>(config.extra_json.as_deref().unwrap()).unwrap()["vmArgs"].clone()
}
#[test]
fn java_debug_launches_on_windows_write_utf8() {
let extra =
serde_json::json!({"mainClass": "org.dromara.DromaraApplication", "vmArgs": "-Xmx1g", "debugServer": 1});
let fixed = match_java_debug_encoding(launch("java-debug", "launch", extra.clone()), true);
assert_eq!(
vm_args(&fixed),
"-Xmx1g -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8"
);
assert_eq!(
serde_json::from_str::<serde_json::Value>(fixed.extra_json.as_deref().unwrap()).unwrap()["mainClass"],
"org.dromara.DromaraApplication"
);
// Attaching, other adapters and other platforms are left alone.
for (adapter, request, windows) in
[("java-debug", "attach", true), ("debugpy", "launch", true), ("java-debug", "launch", false)]
{
assert_eq!(vm_args(&match_java_debug_encoding(launch(adapter, request, extra.clone()), windows)), "-Xmx1g");
}
}
}