feat(debug): debug programs in a plugin run target (#2264)

Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-09 12:52:41 +08:00
1 джерело 0ccc1e687d
коміт 542584abb8
70 files changed
+709 -62

No files matched your search

Створено
+3
Переглянути файл
@@ -4306,6 +4306,8 @@ dependencies = [
"ide-core",
"ide-jsonrpc",
"ide-language",
"ide-remote",
"ide-run",
"libc",
"memchr",
"parking_lot",
@@ -4453,6 +4455,7 @@ dependencies = [
"ignore",
"jsonschema",
"parking_lot",
"percent-encoding",
"quick-xml 0.42.0",
"regex-lite",
"regex-syntax",
+2
Переглянути файл
@@ -24,5 +24,7 @@ tracing.workspace = true
libc = "0.2"
[dev-dependencies]
ide-remote.workspace = true
ide-run.workspace = true
tracing-subscriber.workspace = true
tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros", "test-util"] }
+4 -1
Переглянути файл
@@ -31,6 +31,7 @@ pub enum Transport {
/// A fully resolved adapter command line.
#[derive(Clone, Debug)]
pub struct AdapterSpec {
pub target: Option<crate::target::TargetMapping>,
pub adapter: String,
pub command: String,
pub args: Vec<String>,
@@ -51,6 +52,7 @@ pub struct AdapterSpec {
/// Built-in adapters used when no language definition names `adapter`.
fn builtin(adapter: &str) -> Option<DebuggerConfig> {
let cfg = |command: &str, args: &[&str], transport: &str, hint: &str| DebuggerConfig {
target_placement: None,
adapter: adapter.to_owned(),
command: command.to_owned(),
args: args.iter().map(|a| (*a).to_owned()).collect(),
@@ -134,6 +136,7 @@ pub fn default_type(adapter: &str) -> Option<&'static str> {
pub fn resolve(config: &DebuggerConfig, cwd: PathBuf) -> AdapterSpec {
AdapterSpec {
target: None,
adapter: config.adapter.clone(),
command: config.command.clone(),
args: config.args.clone(),
@@ -447,7 +450,7 @@ impl AdapterLauncher for ProcessLauncher {
program_output: None,
});
}
if spec.adapter == DEBUGPY_TOOL {
if spec.adapter == DEBUGPY_TOOL && spec.target.is_none() {
prepare_debugpy(&mut spec).await?;
}
if is_plugin_bundle(&spec.command) {
+24 -2
Переглянути файл
@@ -71,6 +71,7 @@ pub enum Incoming {
type Pending = HashMap<i64, (String, oneshot::Sender<Result<Value, DapError>>)>;
struct Inner {
mapping: Option<crate::target::TargetMapping>,
out: mpsc::UnboundedSender<Vec<u8>>,
pending: Mutex<Pending>,
seq: AtomicI64,
@@ -106,9 +107,22 @@ impl Connection {
/// [`start`](Self::start) that hands lines outside the protocol (an adapter's
/// own output on stdout, decoded) to `on_output`.
pub fn start_with_output<R, W>(
reader: R,
writer: W,
on_output: Box<dyn Fn(String) + Send + Sync>,
) -> (Self, mpsc::UnboundedReceiver<Incoming>)
where
R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static,
{
Self::start_with_target(reader, writer, on_output, None)
}
pub fn start_with_target<R, W>(
reader: R,
mut writer: W,
on_output: Box<dyn Fn(String) + Send + Sync>,
target: Option<crate::target::TargetMapping>,
) -> (Self, mpsc::UnboundedReceiver<Incoming>)
where
R: AsyncRead + Unpin + Send + 'static,
@@ -117,6 +131,7 @@ impl Connection {
let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (in_tx, in_rx) = mpsc::unbounded_channel();
let inner = Arc::new(Inner {
mapping: target,
out: out_tx,
pending: Mutex::new(HashMap::new()),
seq: AtomicI64::new(1),
@@ -187,7 +202,11 @@ impl Connection {
if self.is_closed() {
return Err(DapError::Closed);
}
let body = serde_json::to_vec(value).map_err(|e| DapError::Protocol(e.to_string()))?;
let mut value = value.clone();
if let Some(mapping) = self.inner.mapping.as_ref() {
mapping.rewrite(&mut value, ide_core::path_mapping::Direction::ToTarget);
}
let body = serde_json::to_vec(&value).map_err(|e| DapError::Protocol(e.to_string()))?;
tracing::trace!(target: "ide_dap::wire", "--> {}", String::from_utf8_lossy(&body));
self.inner.out.send(body).map_err(|_| DapError::Closed)
}
@@ -246,13 +265,16 @@ impl Connection {
fn dispatch(inner: &Inner, incoming: &mpsc::UnboundedSender<Incoming>, body: &[u8]) {
tracing::trace!(target: "ide_dap::wire", "<-- {}", String::from_utf8_lossy(body));
let msg: Value = match serde_json::from_slice(body) {
let mut msg: Value = match serde_json::from_slice(body) {
Ok(v) => v,
Err(e) => {
tracing::warn!("dap: unparsable message: {e}");
return;
}
};
if let Some(mapping) = inner.mapping.as_ref() {
mapping.rewrite(&mut msg, ide_core::path_mapping::Direction::ToHost);
}
match msg.get("type").and_then(Value::as_str) {
Some("response") => {
let Some(request_seq) = msg.get("request_seq").and_then(Value::as_i64) else {
+57 -4
Переглянути файл
@@ -16,6 +16,7 @@ pub mod jdwp;
pub mod kotlin;
mod kotlin_jdwp;
mod session;
pub mod target;
use adapter::{AdapterLauncher, AdapterSpec, ProcessLauncher};
use connection::{Connection, DapError};
@@ -696,6 +697,17 @@ impl DebugManager {
config: LaunchConfig,
owner: SessionOwner,
custom_command: impl FnOnce(&str) -> Option<String>,
) -> IdeResult<DebugSessionId> {
self.start_prepared(config, owner, custom_command, |_, _| Ok(())).await
}
/// Configure a target adapter before any host tool resolution or launch.
pub async fn start_prepared(
&self,
config: LaunchConfig,
owner: SessionOwner,
custom_command: impl FnOnce(&str) -> Option<String>,
prepare: impl FnOnce(&mut AdapterSpec, &mut Value) -> IdeResult<()>,
) -> IdeResult<DebugSessionId> {
let mut arguments = launch_arguments(&config)?;
// An IDE-side marker (never sent to the adapter): the session is a Flutter
@@ -710,6 +722,22 @@ impl DebugManager {
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());
prepare(&mut spec, &mut arguments)?;
if spec.target.is_some() {
return self
.start_session(
DebugSessionId::next(),
owner,
config.name,
config.adapter,
None,
config.request,
arguments,
spec,
None,
)
.await;
}
if config.adapter == kotlin::ADAPTER
&& let Some(home) = arguments
.as_object_mut()
@@ -858,11 +886,22 @@ impl DebugManager {
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 io = self.launcher.launch(spec.clone(), reuse_port).await.map_err(|e| {
if spec.target.is_some() {
IdeError::new(e.kind, format!("{}\nargv: {:?} {:?}", e.message, spec.command, spec.args))
} else {
e
}
})?;
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)))
Connection::start_with_target(
io.reader,
io.writer,
Box::new(move |line| adapter::push_line(&stderr, line)),
spec.target.clone(),
)
};
let rdbg_pid = (adapter_name == "rdbg" && request == "launch")
.then(|| io.child.as_ref().and_then(tokio::process::Child::id))
@@ -903,6 +942,9 @@ impl DebugManager {
Err(e) => {
let e = if session.conn.is_closed() {
let mut msg = format!("debug adapter `{}` exited during startup", session.adapter);
if session.spec.target.is_some() {
msg.push_str(&format!("\nargv: {:?} {:?}", session.spec.command, session.spec.args));
}
let tail = stderr.lock().join("\n");
if !tail.is_empty() {
msg.push_str(":\n");
@@ -913,6 +955,17 @@ impl DebugManager {
msg.push_str(hint);
}
IdeError::unavailable(msg)
} else if session.spec.target.is_some() {
IdeError::new(
e.kind,
format!(
"{}\nargv: {:?} {:?}\n{}",
e.message,
session.spec.command,
session.spec.args,
stderr.lock().join("\n")
),
)
} else {
e
};
@@ -950,8 +1003,8 @@ impl DebugManager {
"pathFormat": "path",
"supportsVariableType": true,
"supportsVariablePaging": false,
"supportsRunInTerminalRequest": true,
"supportsStartDebuggingRequest": true,
"supportsRunInTerminalRequest": s.spec.target.is_none(),
"supportsStartDebuggingRequest": s.spec.target.is_none(),
"supportsProgressReporting": true,
"supportsInvalidatedEvent": false,
"supportsMemoryReferences": false,
+4
Переглянути файл
@@ -579,6 +579,10 @@ async fn on_event(mgr: &Arc<DebugManager>, s: &Arc<Session>, event: &str, mut bo
}
fn on_reverse_request(mgr: &Arc<DebugManager>, s: &Arc<Session>, seq: i64, command: &str, args: Value) {
if s.spec.target.is_some() {
let _ = s.conn.respond(seq, command, Err("This debugger does not support plugin run targets".into()));
return;
}
match command {
"runInTerminal" => {
let result = run_in_terminal(s, &args);
+72
Переглянути файл
@@ -0,0 +1,72 @@
//! DAP path fields only: output, expressions, environment and program arguments are opaque.
use ide_core::path_mapping::{Direction, PathMapping, file_path, file_uri};
use serde_json::Value;
#[derive(Clone, Debug)]
pub struct TargetMapping {
pub paths: PathMapping,
pub external_prefix: String,
}
impl TargetMapping {
pub fn rewrite(&self, value: &mut Value, direction: Direction) {
match value {
Value::Array(items) => items.iter_mut().for_each(|v| self.rewrite(v, direction)),
Value::Object(fields) => {
for (key, value) in fields {
if key == "pathMappings" {
continue;
}
if matches!(key.as_str(), "path" | "program" | "cwd" | "localRoot" | "remoteRoot") {
if let Some(path) = value.as_str() {
if matches!(direction, Direction::ToTarget) && path.starts_with(&self.external_prefix) {
if let Some(decoded) =
file_path(&format!("file:///{}", path.trim_start_matches(&self.external_prefix)))
{
*value = decoded.into();
}
} else if let Some(mapped) = self.paths.map(path, direction) {
*value = mapped.into();
} else if matches!(direction, Direction::ToHost) && key == "path" && path.starts_with('/') {
*value = format!(
"{}{}",
self.external_prefix,
file_uri(path).trim_start_matches("file:///")
)
.into();
}
}
} else if !matches!(key.as_str(), "args" | "env" | "output" | "expression" | "value") {
self.rewrite(value, direction);
}
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn maps_protocol_paths_without_changing_user_data_or_adapter_mappings() {
let m = TargetMapping {
paths: PathMapping::new("C:/project", "/app"),
external_prefix: "openidea-target://test/0/".into(),
};
let mut request = json!({"arguments":{"source":{"path":"C:/project/a.py"},"program":"C:/project/a.py","cwd":"C:/project", "args":["C:/project/text"],"env":{"path":"C:/project/text"},"pathMappings":[{"localRoot":"/app","remoteRoot":"/app"}]}});
m.rewrite(&mut request, Direction::ToTarget);
assert_eq!(request["arguments"]["source"]["path"], "/app/a.py");
assert_eq!(request["arguments"]["cwd"], "/app");
assert_eq!(request["arguments"]["args"][0], "C:/project/text");
assert_eq!(request["arguments"]["env"]["path"], "C:/project/text");
let mut response = json!({"body":{"stackFrames":[{"source":{"path":"/app/a.py"}},{"source":{"path":"/usr/lib/a.py"}}],"sources":[{"path":"/app/b.py"}],"output":"/app/a.py"}});
m.rewrite(&mut response, Direction::ToHost);
assert_eq!(response["body"]["stackFrames"][0]["source"]["path"], "C:\\project\\a.py");
assert_eq!(response["body"]["sources"][0]["path"], "C:\\project\\b.py");
assert_eq!(response["body"]["stackFrames"][1]["source"]["path"], "openidea-target://test/0/usr/lib/a.py");
assert_eq!(response["body"]["output"], "/app/a.py");
}
}
+1
Переглянути файл
@@ -1166,6 +1166,7 @@ fn godot_registry(port: u16, command: &str, args: &[&str]) -> Arc<LanguageRegist
tree_sitter: None,
language_servers: vec![],
debugger: Some(ide_language::DebuggerConfig {
target_placement: None,
adapter: "godot".into(),
command: command.into(),
args: args.iter().map(|a| (*a).to_owned()).collect(),
+30
Переглянути файл
@@ -0,0 +1,30 @@
import sys,json,os,socket
root=os.environ.get("FAKE_ROOT","/app")
r=sys.stdin.buffer; w=sys.stdout.buffer
seq=0
def send(v):
global seq
seq+=1; v['seq']=seq
b=json.dumps(v).encode(); w.write(('Content-Length: %d\r\n\r\n'%len(b)).encode()+b); w.flush()
while True:
line=r.readline()
if not line: break
n=int(line.split(b':')[1]); assert r.readline()==b'\r\n'
q=json.loads(r.read(n)); c=q['command']; a=q.get('arguments',{}); body={}
if c=='initialize': body={'supportsConfigurationDoneRequest':True}
if c=='launch':
if 'program' in a: assert a['program']==root+'/main.py',a
if os.environ.get('FAKE_CONNECT_BACK'):
listener=socket.socket(); listener.bind(('127.0.0.1',a['port'])); listener.listen()
assert a['cwd']==root,a
send({'type':'event','event':'initialized'})
if c=='setBreakpoints':
assert a['source']['path']==root+'/main.py',a
body={'breakpoints':[{'verified':True,'line':3}]}
if c=='threads': body={'threads':[{'id':1,'name':'target main'}]}
if c=='configurationDone': send({'type':'event','event':'stopped','body':{'reason':'breakpoint','threadId':1,'allThreadsStopped':True}})
if c=='stackTrace' and os.environ.get('FAKE_CONNECT_BACK'):
client,_=listener.accept(); assert client.recv(4096).decode()==root+'/main.py'; client.close()
if c=='stackTrace': body={'stackFrames':[{'id':1,'name':'main','line':3,'column':1,'source':{'path':root+'/main.py'}}]}
send({'type':'response','request_seq':q['seq'],'command':c,'success':True,'body':body})
if c=='disconnect': break
+17 -3
Переглянути файл
@@ -93,9 +93,23 @@ async fn listens_for_xdebug_until_stopped_with_path_mappings() {
.await
.unwrap();
let id = manager
.start_with(listen_config(port, &served, &local), SessionOwner::new("main", sink.clone()), |_| {
Some(adapter.clone())
})
.start_prepared(
listen_config(port, &served, &served),
SessionOwner::new("main", sink.clone()),
|_| None,
|spec, _| {
spec.command = adapter.clone();
spec.cwd = local.clone();
spec.target = Some(ide_dap::target::TargetMapping {
paths: ide_core::path_mapping::PathMapping::new(
&local.to_string_lossy(),
&served.to_string_lossy(),
),
external_prefix: "openidea-target://php/0/".into(),
});
Ok(())
},
)
.await
.unwrap_or_else(|e| panic!("start: {e:?}"));
+130
Переглянути файл
@@ -0,0 +1,130 @@
//! Real stdio transport through a target wrapper, without Docker or installed adapters.
#![cfg(unix)]
use ide_core::{SharedSink, path_mapping::PathMapping};
use ide_dap::{target::TargetMapping, *};
use ide_language::LanguageRegistry;
use std::{collections::BTreeMap, sync::Arc};
const ADAPTER: &str = include_str!("fixtures/target_adapter.py");
#[tokio::test]
async fn stdio_adapter_through_target_maps_launch_breakpoints_and_frames() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().canonicalize().unwrap();
let adapter = root.join("adapter.py");
std::fs::write(&adapter, ADAPTER).unwrap();
let wrapper = root.join("wrapper");
std::fs::write(&wrapper, "#!/bin/sh\nprintf '%s\\n' \"$@\" > wrapper-argv\nexec \"$@\"\n").unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap();
let target = ide_core::remote_settings::PluginRunTarget {
id: "fake".into(),
name: "Fake".into(),
program: "wrapper".into(),
args: vec![],
workdir_args: vec![],
container_root: Some("/app".into()),
commands: vec![],
};
// Keep the fixture adapter outside mapped argv; this is the container's executable.
let run: ide_run::RunConfig = serde_json::from_value(serde_json::json!({"id":"adapter","name":"Adapter","kind":"shell","command":"/usr/bin/python3","args":["-u","-c",ADAPTER],"cwd":".","env":{},"detected":false,"isTest":false})).unwrap();
let run = ide_remote::plugin_target::prepare(&root, &target, &wrapper, &run).unwrap();
let manager = DebugManager::new(Arc::new(LanguageRegistry::load(None).unwrap()));
manager
.set_breakpoints(
root.join("main.py"),
vec![Breakpoint { line: 2, enabled: true, condition: None, hit_condition: None, log_message: None }],
)
.await
.unwrap();
let config = LaunchConfig {
name: "Target".into(),
adapter: "debugpy".into(),
request: "launch".into(),
program: Some(root.join("main.py").to_string_lossy().into_owned()),
args: vec![],
cwd: Some(root.clone().into()),
env: BTreeMap::new(),
extra_json: None,
};
let sink: SharedSink = Arc::new(|_| {});
let mapping = TargetMapping {
paths: PathMapping::new(&root.to_string_lossy(), "/app"),
external_prefix: "openidea-target://fake/0/".into(),
};
let id = manager
.start_prepared(
config,
SessionOwner::new("test", sink),
|_| None,
move |spec, _| {
spec.command = run.command;
spec.args = run.args;
spec.cwd = root;
spec.target = Some(mapping);
Ok(())
},
)
.await
.unwrap();
let frames = manager.stack_trace(id, 1).await.unwrap();
assert_eq!(
frames[0].path.as_ref().unwrap().to_string_lossy(),
tmp.path().canonicalize().unwrap().join("main.py").to_string_lossy()
);
assert_eq!(frames[0].line, 2);
assert!(std::fs::read_to_string(tmp.path().join("wrapper-argv")).unwrap().starts_with("python3\n-u\n-c\n"));
manager.stop(id).await.unwrap();
}
#[tokio::test]
async fn host_adapter_accepts_connect_back_from_target_with_mapped_sources() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().canonicalize().unwrap();
let port = ide_dap::adapter::free_port().unwrap();
let manager = DebugManager::new(Arc::new(LanguageRegistry::load(None).unwrap()));
manager
.set_breakpoints(
root.join("main.py"),
vec![Breakpoint { line: 2, enabled: true, condition: None, hit_condition: None, log_message: None }],
)
.await
.unwrap();
let config = LaunchConfig {
name: "Connect back".into(),
adapter: "php-debug".into(),
request: "launch".into(),
program: None,
args: vec![],
cwd: Some(root.clone().into()),
env: BTreeMap::new(),
extra_json: Some(serde_json::json!({"port":port,"pathMappings":{"/app":"/app"}}).to_string()),
};
let sink: SharedSink = Arc::new(|_| {});
let mapping = TargetMapping {
paths: PathMapping::new(&root.to_string_lossy(), "/app"),
external_prefix: "openidea-target://fake/0/".into(),
};
let id = manager
.start_prepared(
config,
SessionOwner::new("test", sink),
|_| None,
move |spec, _| {
spec.command = "/usr/bin/python3".into();
spec.args = vec!["-u".into(), "-c".into(), ADAPTER.into()];
spec.env.push(("FAKE_CONNECT_BACK".into(), "1".into()));
spec.target = Some(mapping);
Ok(())
},
)
.await
.unwrap();
let code =
format!("import socket;s=socket.create_connection(('127.0.0.1', {port}));s.sendall(b'/app/main.py');s.close()");
let mut child = tokio::process::Command::new("/usr/bin/python3").args(["-c", &code]).spawn().unwrap();
let frames = manager.stack_trace(id, 1).await.unwrap();
assert_eq!(frames[0].path.as_ref().unwrap().to_path_buf(), root.join("main.py"));
assert!(child.wait().await.unwrap().success());
manager.stop(id).await.unwrap();
}
+1
Переглянути файл
@@ -10,6 +10,7 @@ name = "openidea-server"
path = "src/bin/server.rs"
[dependencies]
percent-encoding = "2"
encoding_rs.workspace = true
ide-core.workspace = true
ide-mcp.workspace = true
+10
Переглянути файл
@@ -1,4 +1,5 @@
mod output;
mod target;
use crate::Host;
use crate::rpc::Dispatcher;
@@ -9,6 +10,15 @@ 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> {
host.require_trust(window)?;
let target_id = config
.extra_json
.as_deref()
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.and_then(|v| v.get("__runTarget").and_then(|v| v.as_str()).map(str::to_owned));
if let Some(id) = target_id {
let owner = SessionOwner::new(host.owner_window(window), host.window_sink(window));
return target::start(host, window, config, owner, id).await;
}
let mut config = match_java_debug_encoding(config, cfg!(windows));
if config.adapter == "debugpy"
&& config.request == "launch"
+102
Переглянути файл
@@ -0,0 +1,102 @@
use crate::Host;
use ide_core::path_mapping::PathMapping;
use ide_core::{IdeError, IdeResult};
use ide_dap::target::TargetMapping;
use ide_dap::{LaunchConfig, SessionOwner};
use ide_language::ProgramPin;
use serde_json::json;
pub(super) async fn start(
host: &Host,
window: &str,
mut config: LaunchConfig,
owner: SessionOwner,
id: String,
) -> IdeResult<ide_core::DebugSessionId> {
let project = host.project(window)?;
let plugins =
host.plugin_host().ok_or_else(|| IdeError::unavailable("plugin run target is no longer available"))?;
let registration = plugins
.run_targets(&project.root)
.into_iter()
.find(|t| t.id == id)
.ok_or_else(|| IdeError::unavailable("plugin run target is no longer available"))?;
let container = registration
.target
.container_root
.ok_or_else(|| IdeError::invalid("Debugging a plugin target requires a container root"))?;
let dbg = ide_dap::adapter::find_config(&host.languages, &config.adapter)
.ok_or_else(|| IdeError::invalid("unknown debug adapter"))?;
let placement = dbg.target_placement.as_deref().unwrap_or("");
if !matches!(placement, "connect-back" | "adapter-in-target")
|| (placement == "adapter-in-target" && dbg.transport != "stdio")
{
return Err(IdeError::invalid("This debugger does not support plugin run targets"));
}
let mapping = TargetMapping {
paths: PathMapping::new(&project.root.to_string_lossy(), &container),
external_prefix: format!(
"openidea-target://{}/0/",
percent_encoding::utf8_percent_encode(&id, percent_encoding::NON_ALPHANUMERIC)
),
};
let mut extra: serde_json::Value = config
.extra_json
.as_deref()
.map(serde_json::from_str)
.transpose()
.map_err(|e| IdeError::invalid(e.to_string()))?
.unwrap_or(json!({}));
extra.as_object_mut().ok_or_else(|| IdeError::invalid("invalid debug launch arguments"))?.remove("__runTarget");
// DAP paths already use target coordinates; adapter-native mappings stay identity.
if placement == "connect-back" {
config.program = None;
config.args.clear();
for key in ["program", "runtimeExecutable", "runtimeArgs"] {
extra.as_object_mut().unwrap().remove(key);
}
if extra.get("port").and_then(|v| v.as_u64()).unwrap_or(0) == 0 {
extra["port"] = json!(9003);
}
extra["hostname"] = json!("0.0.0.0");
extra["pathMappings"] = json!({container.clone(): container.clone()});
} else {
for key in ["debugLauncherPython", "pythonPath"] {
extra.as_object_mut().unwrap().remove(key);
}
extra["console"] = json!("internalConsole");
extra["python"] = json!(dbg.command);
extra["pathMappings"] = json!([{"localRoot":container,"remoteRoot":container}]);
}
config.extra_json = Some(extra.to_string());
let wrapped = if placement == "adapter-in-target" {
let run = serde_json::from_value(json!({"id":"debug-adapter","name":"Debug adapter","kind":"shell","command":dbg.command,"args":dbg.args,"cwd":config.cwd,"env":config.env,"detected":false,"isTest":false})).map_err(|e| IdeError::invalid(e.to_string()))?;
let root = project.root.clone();
Some(host.blocking(move || plugins.prepare_run_target(&root, &id, &run)).await?)
} else {
None
};
host.require_trust(window)?;
let root = project.root.clone();
let settings = project.settings.effective();
host.debugger
.start_prepared(
config,
owner,
|_| None,
move |spec, _| {
if let Some(run) = wrapped {
spec.command = run.command;
spec.args = run.args;
spec.cwd = run.cwd.map(Into::into).unwrap_or(root);
spec.env = run.env.into_iter().collect();
spec.pinned = Some(ProgramPin::approve(&spec.command)?);
} else if let Some(command) = ide_sdk::apply::tool_command(&root, &settings, &spec.command) {
spec.command = command.to_string_lossy().into_owned();
}
spec.target = Some(mapping);
Ok(())
},
)
.await
}
+37 -1
Переглянути файл
@@ -4,7 +4,7 @@ use crate::apply_workspace_edit as apply_edit;
use crate::plugins::{ProviderKind, editor_providers_for as editor_providers};
use crate::rpc::Dispatcher;
use ide_core::error::ErrorKind;
use ide_core::{DocId, IdeResult, Location, Pos, Range, RequestId, ServerId, TextEdit, WorkspaceEdit};
use ide_core::{DocId, IdeError, IdeResult, Location, Pos, Range, RequestId, ServerId, TextEdit, WorkspaceEdit};
use ide_lsp::types::*;
use ide_plugin::ProviderDocument;
use std::path::Path;
@@ -545,6 +545,42 @@ pub async fn lsp_virtual_document(host: &Host, window: &str, uri: String) -> Ide
if uri.starts_with("git-revision:") {
return crate::ops::vcs::revision_virtual_document(host, window, &uri).await;
}
if uri.starts_with("openidea-target:") && uri.contains("/0/") {
host.require_trust(window)?;
let project = host.project(window)?;
let plugins =
host.plugin_host().ok_or_else(|| IdeError::unavailable("container document reader is unavailable"))?;
for registration in plugins.run_targets(&project.root) {
let prefix = format!(
"openidea-target://{}/0/",
percent_encoding::utf8_percent_encode(&registration.id, percent_encoding::NON_ALPHANUMERIC)
);
if let Some(suffix) = uri.strip_prefix(&prefix) {
let path = ide_core::path_mapping::file_path(&format!("file:///{suffix}"))
.filter(|p| p.starts_with('/') && !p.split('/').any(|s| s == ".." || s == "."))
.ok_or_else(|| IdeError::invalid("invalid container document URI"))?;
let container_root = registration
.target
.container_root
.ok_or_else(|| IdeError::invalid("invalid container document URI"))?;
let target = ide_language::ServerTarget {
id: registration.id,
origin: None,
project_root: project.root.clone(),
mapping: ide_core::path_mapping::PathMapping::new(&project.root.to_string_lossy(), &container_root),
container_root,
};
let title = std::path::Path::new(&path)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let language_id = host.languages.detect(std::path::Path::new(&path), None);
let text = host.blocking(move || super::lsp_target::read(&plugins, &target, &path)).await?;
return Ok(VirtualDocument { uri, text, title, language_id, description: None, decompiled: false });
}
}
return Err(IdeError::unavailable("plugin run target is no longer available"));
}
host.project(window)?.lsp.virtual_document(&uri).await
}
+102
Переглянути файл
@@ -0,0 +1,102 @@
//! Target debugging uses the same trusted plugin wrapper on desktop and the SSH server.
#![cfg(unix)]
use ide_core::IdeEvent;
use ide_host::{
Host, HostConfig,
plugins::{Consent, HostWorkspace, PluginEvent, PluginHost},
};
use ide_language::LanguageRegistry;
use ide_plugin::{Invocation, Limits};
use parking_lot::Mutex;
use serde_json::json;
use std::{os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
#[tokio::test(flavor = "multi_thread")]
async fn registered_target_debugs_over_stdio_and_rechecks_program_grant() {
let tmp = tempfile::tempdir().unwrap();
let base = dunce::canonicalize(tmp.path()).unwrap();
let root = base.join("project");
std::fs::create_dir(&root).unwrap();
let adapter = base.join("adapter.py");
std::fs::write(&adapter, include_str!("../../ide-dap/tests/fixtures/target_adapter.py")).unwrap();
let wrapper = base.join("wrapper");
std::fs::write(
&wrapper,
format!(
r#"#!/bin/sh
[ "$1" = wrapped ] && [ "$2" = --cwd=/container ] || exit 91
shift 2
if [ "$1" = cat ]; then exec cat "$2" "$3"; fi
export FAKE_ROOT=/container
exec /usr/bin/python3 -u '{}'
"#,
adapter.display()
),
)
.unwrap();
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let sink_events = events.clone();
let host = Arc::new(Host::new(HostConfig {
config_dir: base.join("config"),
data_dir: base.join("data"),
local_data_dir: base.join("local"),
languages: Arc::new(LanguageRegistry::load(None).unwrap()),
syntax: Arc::default(),
sink_for: Box::new(move |_| {
let e = sink_events.clone();
Arc::new(move |event: IdeEvent| e.lock().push(event))
}),
owner: |w| w,
runtime: tokio::runtime::Handle::current(),
project_environment: false,
}));
host.open_project("main", &root).unwrap();
host.set_project_trust(&root, true, false).await.unwrap();
let plugins = Arc::new(
PluginHost::new(base.join("plugins"), Arc::new(|_: Option<&str>, _: PluginEvent| {}), Limits::default())
.unwrap(),
);
plugins.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host)))));
host.set_plugin_host(&plugins);
let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/execution");
plugins.install_from_dir(&package, &Consent::all()).unwrap();
plugins.choose_program("execution", "process:execution-tool", Some(&root), Some(&wrapper)).unwrap();
let invocation =
|| Invocation { client: Some("main".into()), workspace_root: Some(root.clone()), ..Default::default() };
plugins.run_command("execution", "target", None, invocation()).unwrap();
let id = plugins.run_targets(&root)[0].id.clone();
let launch: ide_dap::LaunchConfig=serde_json::from_value(json!({"name":"Fake target","adapter":"debugpy","request":"launch","program":root.join("main.py"),"args":[],"cwd":root,"env":{},"extraJson":json!({"__runTarget":id}).to_string()})).unwrap();
host.debugger
.set_breakpoints(
root.join("main.py"),
vec![ide_dap::Breakpoint {
line: 2,
enabled: true,
condition: None,
hit_condition: None,
log_message: None,
}],
)
.await
.unwrap();
let session = ide_host::ops::debug::debug_start(&host, "main", launch.clone()).await.unwrap();
let frames = host.debugger.stack_trace(session, 1).await.unwrap();
assert_eq!(frames[0].path.as_ref().unwrap().to_path_buf(), root.join("main.py"));
host.debugger.stop(session).await.unwrap();
// External debug sources use the container reader even with no LSP running.
let library = base.join("library.py");
std::fs::write(&library, "container library").unwrap();
let uri = format!(
"openidea-target://{}/0/{}",
percent_encoding::utf8_percent_encode(&id, percent_encoding::NON_ALPHANUMERIC),
library.to_string_lossy().trim_start_matches('/')
);
let doc = ide_host::ops::doc::doc_open_virtual(&host, "main", uri).await.unwrap();
assert!(doc.read_only);
assert_eq!(doc.text, "container library");
std::fs::write(&wrapper, "#!/bin/sh\nexit 99\n").unwrap();
let error = ide_host::ops::debug::debug_start(&host, "main", launch).await.unwrap_err();
assert!(error.message.contains("changed") || error.message.contains("approved"), "{error:?}");
host.close_project("main");
}
+3
Переглянути файл
@@ -197,6 +197,9 @@ pub struct ServerTarget {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DebuggerConfig {
/// Placement in plugin run targets; absent means unsupported.
#[serde(default)]
pub target_placement: Option<String>,
/// DAP adapter name, e.g. `codelldb`, `debugpy`, `js-debug`, `delve`.
pub adapter: String,
pub command: String,
+1 -1
Переглянути файл
@@ -1 +1 @@
446ff7b14c2d5b0fca8a3febe489906e9d56fc5b66878c953887e16dd54f5d5a
4647fa9aec9732e3dd824efccd61b1c6b9c36645a3586f402f6a7a47419db61f
+1 -1
Переглянути файл
@@ -1 +1 @@
c55acafff3dbe090aac50873d957e73add4db50b4ed3af0def7ec77c1cd1a9d5
6787b195df1057176ea9fcf11d0fcaf8cd347fc182fab94fe3a229063deed831
+1 -1
Переглянути файл
@@ -1 +1 @@
543e921a71667b481e9d7d3b6cf86bc46113ac7068a902d93063eff85c2c812d
c2d17076337b1225ef8b0323b335a3b03bf022485088957c520e0cb797d7df7d
+1 -1
Переглянути файл
@@ -1 +1 @@
fa9ccf5c4f999031e1fb94b9e42b6caddfd5e5aeb08181660f51488711326550
415a4cd17c573aeeb1b45d4b1af55834eab5e686c4f624495323e4fb63c3ac71
+1 -1
Переглянути файл
@@ -1 +1 @@
69fca6e054ccf188b7cfe23081470676bfbee56f96b0247c7ff67aba93a24d39
5f502b7650acd5d07bffba8016680248b5ceac0926013916ff351562e3c6d831
+1 -1
Переглянути файл
@@ -1 +1 @@
cc2fdbb7953bac992375701812e8c4cf666652e027e8b6ecc68b43e0ae7d7ae1
19e8d460269999aeac7b4021bfedb803ad0d68c24ce1c22f058070fb406a64c3
+1 -1
Переглянути файл
@@ -1 +1 @@
4e146d6caa1cc114d33ae18f396ac8da4d6391477daa32cc69e6feca087cf20a
63809abb0f8a4f40b0c0f465062cc8ba0ebfd03d41a108ff19866e6565eda931
+1 -1
Переглянути файл
@@ -1 +1 @@
cfb6e388588d01fd3f772302049e673729138717ce7b761dfcecc4ee213a8d98
99c968b98d8f1d895c428b4048451275bd322e7070aa0a020737097efea17eb1
+1 -1
Переглянути файл
@@ -1 +1 @@
6117ebfa5220ab04f03884d589140426d2ee693a23f058e2457952f30314099d
c40a0fd8c6cf791e70ba0335c6cf0b2c82b250670ebad59b185e3e8719172409
+1 -1
Переглянути файл
@@ -1 +1 @@
e746d420d5f357e00b523e6e118d283df08e7b5f09f59b672645721a02f6ca46
162774155d8b49d53b0c0c055736bd098723c66b02c0e1dc4389985982c57282
+1 -1
Переглянути файл
@@ -1 +1 @@
e127b6562e398cdb2d9d748434315ae78e61cfdfa3f1859da2e6eaeb2144030d
39e3de11f69c09348906878367318157fca9a777f9ac1c4fb6ed303fa058e5a2
+1 -1
Переглянути файл
@@ -1 +1 @@
6459f7899464237399e33f4190a7526f05a010d6384a36f42c25195202fb6c58
4a3f595d2a6a9b79da7e316a3da86c287e9761ea670319c331467e7b10639663
+1 -1
Переглянути файл
@@ -1 +1 @@
15236a23798998a0e1669c0ce657effb88481ef7236f5738519601e23b07dd1f
bede1f0eaec356e3e35302e65e85531870625560bc9a3197f1acb58969beb4bd
+1 -1
Переглянути файл
@@ -1 +1 @@
8af4f2658b074aebdabefbe49d0641da767c109b107ecf182bcb8cb937092b82
ad2dc0ad12adb9c6b25017b2cef78b7496e96d1bb6e88b24ea9b7b682120e786
+1 -1
Переглянути файл
@@ -1 +1 @@
57a4527c74f62fb98ba388223863d69afdfc9f75b6027af65658f5909379e3c9
2c329cf7af36c9d65ed5b6be7c2a017c7a5a6214a4893cab7ae574acb76fe62f
+1 -1
Переглянути файл
@@ -1 +1 @@
c2a29a1982c278a21a772e440662817b5f096b4740ac99539c5ab2721251acf8
39c85942177e937e48f32bcde86a0068dac484ba028a8d6785a5a2728db9e11b
+1 -1
Переглянути файл
@@ -1 +1 @@
634b3da9468c13d5a74a90d187f6c510b28abe4684e6072db630b976f331e899
30a8cc05a40c7b99a78700430ae87d025ad7567b44384fdb696119aa421f8174
+1 -1
Переглянути файл
@@ -1 +1 @@
b9d5c9b532e022bd4f43ecb80539efd6cab628ecba05c1ecb79d4ba677c1c8c0
5f9c92b23c00269ec2b5dc8829ce526a8fc4c0d76ecb2925ec9a06ffeac5f0b4
+1 -1
Переглянути файл
@@ -1 +1 @@
787ab4dbe4ef4ee8cd9af98079ae6332aeddacaa3f66fc1b3327f35e76f7ca01
d695409029a66c8a44f524072be1d47ed974fc8810f6fe1d8d068765184d4028
+1 -1
Переглянути файл
@@ -1 +1 @@
f0771c02543e5c96ae6a4f38402b666ef06d22abc1157dbc963c7fb2baee0221
d5e81a43dc2c5298d0aaafd70c0bd91ab6cc040d70224d2936349de41253735f
+1 -1
Переглянути файл
@@ -1 +1 @@
86bd9d7f47e9dd9463bcef0173cd24bac4d83e9172adef3e620acc4abc6bc620
c29076023ccedf2da8fd9e5a851a47bbe7131b97713c8140a20cd55608a1cc0d
+1 -1
Переглянути файл
@@ -1 +1 @@
d560db74cda33ff86e7a5329df8f6cd8675f3df0288e427325e0777778f03f44
616157bd77dc272f9e2dfed4c54c9d65fca110e55ad01f92b7c4f8a6136537b4
+1 -1
Переглянути файл
@@ -1 +1 @@
20b3cfd84d9129dd74b56c8766766fe714df7a5e6e051dc72c379d5fdb59b127
022f216f0f4ca5da56f4b2fabf5ee4a987d5e9f9a752feecbb438f9b00b1acf4
+1 -1
Переглянути файл
@@ -1 +1 @@
d30ffd74e9e444f51024586f9c67cefede44ad89bd251410ea3881bd2255b1d5
626f0e0c83e69704c0365c273b6f63d43278ee37786f38ed467ce908c30eadf7
+1 -1
Переглянути файл
@@ -1 +1 @@
9a354ee52f1ee1080ce9c687e9fc59e137da8f38c6723316b8aeecb5bdf1e4ad
aa141701049f5c6c0d045341c3337344ed7a368800f08237b1b7812cce216814
+1 -1
Переглянути файл
@@ -1 +1 @@
ae183781f1fc1639fa7b1b3c4b8ff100e4e25daab69bbf876584050e5363473f
d6c40099cc959ef2dead996beeb4acd73c1327597cd029674026c6f2bc878300
+1 -1
Переглянути файл
@@ -1 +1 @@
8a8c92661df8d693d5eabc45946e2d98c9e887090e4233e65e896421c655dcdd
3201f643cf868f7a358ba812f9c90e06b982d864ed141b6b4c3fdeee13cb1559
+1
Переглянути файл
@@ -38,6 +38,7 @@ require-root-marker = true
install-hint = "composer global require laravel/lsp"
[debugger]
target-placement = "connect-back"
adapter = "php-debug"
command = "php-debug-adapter"
install-hint = "Install PHP Debug (Xdebug) in Settings › Languages & Frameworks › Language Servers, and enable Xdebug"
+1
Переглянути файл
@@ -60,6 +60,7 @@ require-root-marker = true
install-hint = "pip install ruff (or: uv tool install ruff)"
[debugger]
target-placement = "adapter-in-target"
adapter = "debugpy"
command = "python3"
args = ["-m", "debugpy.adapter"]
+16
Переглянути файл
@@ -2484,3 +2484,19 @@ The virtual URI identifies its originating server, including for document links
and diagnostic-related locations. Navigation inside these documents returns to
that server; editing and saving
are disabled. No WIT signature change is needed for this opt-in host setting.
### Debugging run targets
Plugin targets require `container-root` for debugging. PHP uses `connect-back`: the
IDE runs php-debug on the host (on the SSH server for remote projects), listens on
port 9003 unless configured otherwise, then starts the program through the normal
target run path. The target must enable Xdebug debug mode and request startup, and
set `xdebug.client_host` and `xdebug.client_port` to reach that listener. DDEV normally
provides the host setting; enable its Xdebug support before debugging.
Python uses `adapter-in-target`: the wrapper starts `python3 -m debugpy.adapter`
over stdio. Install debugpy in that container interpreter; no host installation is
used. The wrapper must preserve stdin/stdout and forward environment as needed.
Project paths and DAP sources map between the project root and `container-root`.
Other debuggers default to unsupported: Node and Go adapters currently require
additional ports and have no supported stdio target configuration.
+1 -1
Переглянути файл
@@ -1 +1 @@
34bea4bf36b7a814dafe02aaee02fe3354a667d1c8ca66f238de00322d0a035a
e3c11d8969a1b8b52e7e745b558d12f804ebf533e7f0f3fe614ffe6f7b9c9468
+1 -1
Переглянути файл
@@ -1 +1 @@
fdd68915997803652ad6f5c549107b043b9001ad4e176d2473d9b37469fbe1cc
b28ccfd442c6348e0826680ec97391cbe4ec6a93a27ff02e40f47341e21586b4
+1 -1
Переглянути файл
@@ -1 +1 @@
ad3fe934d30b48d53097bf4dee36a091b2410c0325924702c66e717405b58203
d669a2600b0c59e91ac8a9b875a24bddf0eeddd404ce7b67da31733861a846b8
+1 -1
Переглянути файл
@@ -1 +1 @@
5710601823307d167805a7ca20e7498fb9c37082b20df7fd1f86e1cd66b438f3
a5f0ebce6d30e0dc03769ebe5cb82d3704c5ccc32207aa2b3f5500de7b1a9152
+1 -1
Переглянути файл
@@ -1 +1 @@
1a486ceda9986bc253a0098c983f755c8860e6b7a3f2d7f0284b3b928b4e6c9e
ee3ccb648ca4b98a0359521c4cd895b5e7336fd65101563f2ca82b8cbaeedc3d
+1 -1
Переглянути файл
@@ -1 +1 @@
13f27c819384f92e406bf7226a46d3fe5b063e850db3d64916e661e1a63f345a
51f7167dd5ed21ed37e227bd3b100a5be5a57e7890a494c942fdd036b1307993
+1 -1
Переглянути файл
@@ -1 +1 @@
c7b2b471f26201c037522ee4a5b086d1c8ef88fbbb3f11f7279ad1a7185bdbae
cd860e7a652951aaa42029da23fdeee7a3c09ab1bfcbfb0d1bb6c89036fc8817
+1 -1
Переглянути файл
@@ -1 +1 @@
82c71af4a1f771dbd16a4c8f71b9bd6a6a53d82bd92958d4862467237b82d228
db3ee94b5cf4234cee7d61e08a9f8629d7e7f6adbf11208b54cd4ea44cf1fc13
+1 -1
Переглянути файл
@@ -1 +1 @@
65096a9b6331af9d40049801547e99e2fa1d5ae0f1065f6931af0aee8d2f24a7
76d13e772046b192485ea59c7720177b563986e0167475c449a9a5c7430ef908
+1 -1
Переглянути файл
@@ -1 +1 @@
cf8d21da412fd64f1f42c72b13be762ff1bcd43dd19965fc1fce805c5e1012ae
a22c9e7ebf79b2d5926424a028edc09aef40293db9ebc959d9ececa1f0ebe4ca
+1 -1
Переглянути файл
@@ -1 +1 @@
1ea1d7116e6b943e42580e43cb13199b6e48fc5acdf9784c4864adb7433ebb59
fcc3d666fd1e89363fa8668dd10f89efc49f2376766282b734de4b719fdc9aff
+1 -1
Переглянути файл
@@ -1 +1 @@
05ff46a6c1af09b84f9a931d8956d79fbad9507dcff708ba021e074c55c062e2
2eb742315f877e3251f992884160ed4d9a2940ee12a7d4d7a52669238d495930
+1 -1
Переглянути файл
@@ -1 +1 @@
26e57f5294b40bc31d9283843b2fcf6ad09d475fd160d61ddd3d420e54880a24
c2feb6a4cd0e9908f078a3d0074732cfce653cc57cce9d1907838598515048ee
+1 -1
Переглянути файл
@@ -1 +1 @@
0d2c1140fe718d06e08af903ee94bfe87cd04de77c1299f6f5c337042c6f6197
590b46034f6c39592a27cabd5b0b76809e7f7133d258f322604614ecb3479317
+1 -1
Переглянути файл
@@ -1 +1 @@
f4251a1cad1d5cea54559baca12d84fdf175e65db85ee3f4d29e381e045c40fb
0d068ea0f0851e6e3166af0a91f290da8dc3f4a852b86050e7f7ced214a39124
+4 -1
Переглянути файл
@@ -16,7 +16,10 @@ interface run-targets {
/// Without workdir-args the wrapper manages its container directory itself.
/// The wrapper always starts in the configuration's LOCAL working directory.
/// Environment reaches the wrapper; forwarding into a container depends on its conventions.
/// Registration never selects the target. Debugging plugin targets is invalid.
/// Registration never selects the target. Debugging requires container-root and an
/// explicitly supported language debugger placement. Connect-back adapters listen
/// on the host; the target must configure the debuggee to connect to that host.
/// Stdio adapters run inside the target and must be installed there.
record run-target {
id: string,
name: string,
+16 -4
Переглянути файл
@@ -1,4 +1,5 @@
/** Debugger actions (ids match `keymaps/intellij-macos.ts`). */
import { withRunTarget } from "@/platform/runTargetSelection";
import { executeAction, getAction, registerActions, type Action } from "@/platform/actions";
import {
call,
@@ -315,11 +316,12 @@ async function startConfig(arg: unknown) {
return;
}
}
if (isRemoteConfig(config)) {
config = withRunTarget(config);
if (!config.target?.startsWith("plugin-target:") && isRemoteConfig(config)) {
await startRemote(config, root, show);
return;
}
if (isJavaDebuggable(config)) {
if (!config.target?.startsWith("plugin-target:") && isJavaDebuggable(config)) {
await startJavaConfig(config, root, origin ?? config, show);
return;
}
@@ -339,11 +341,21 @@ async function startConfig(arg: unknown) {
});
return;
}
const xdebugCheck = config.debugAdapter === "php-debug" && isTauri() ? warnWithoutXdebug(launch) : null;
const pluginTarget = config.target?.startsWith("plugin-target:");
if (pluginTarget) {
const extra = JSON.parse(launch.extraJson ?? "{}");
extra.__runTarget = config.target;
launch.extraJson = JSON.stringify(extra);
}
const xdebugCheck =
!pluginTarget && config.debugAdapter === "php-debug" && isTauri() ? warnWithoutXdebug(launch) : null;
await maybeEditorService()
?.saveAll()
.catch(() => undefined);
await startSession(launch, { runConfig: origin ?? config, show });
const started = await startSession(launch, { runConfig: origin ?? config, show });
if (started && pluginTarget && config.debugAdapter === "php-debug") {
await executeAction("run.runConfig", config);
}
await xdebugCheck;
}
+6
Переглянути файл
@@ -2,6 +2,7 @@
* Run configurations, the title bar run widget, the Run tool window (console +
* test runner) and run/debug/stop actions.
*/
import { withRunTarget } from "@/platform/runTargetSelection";
import { registerEditorContribution } from "@/editor/api";
import { executeAction, executeActionOrThrow, getAction, registerActions } from "@/platform/actions";
import { getContext, setContext, useContextKeys } from "@/platform/context";
@@ -146,6 +147,7 @@ async function runSelected() {
}
async function debugConfig(config: RunConfig, chain: readonly string[] = [], show?: boolean) {
config = withRunTarget(config);
if (config.kind === "compound") {
if (!(await prepareLaunch(config, chain, show))) return;
const children = compoundChildren(config, useRun.getState().configs);
@@ -155,6 +157,10 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = [], sho
if (remoteDebugBlocked(config)) return;
if (!(await prepareLaunch(config, chain, show))) return;
recordRun(config.id);
if (config.target?.startsWith("plugin-target:")) {
await executeAction("debug.startConfig", show === false ? { config, show: false } : config);
return;
}
// Python on a Docker / Compose target: debugpy in the container, the debugger attaches.
if (containerDebuggable(config) && getAction("debug.attach")) {
await debugInContainer(config);
+8
Переглянути файл
@@ -217,6 +217,14 @@ describe("remote debugging", () => {
);
});
it("dispatches plugin targets to the target-aware debugger", async () => {
const config = cfg("app", { target: "plugin-target:4:wrapper", debugAdapter: "debugpy" });
useRun.setState({ configs: [config], loaded: true });
await executeAction("run.debugConfig", config.id);
expect(startConfig).toHaveBeenCalledWith(config);
expect(remoteDebugBlocked(config)).toBe(false);
});
describe("in containers", () => {
const container: RunTargetValid = { ...DEFAULT_RUN_TARGET, kind: "docker", id: "py", name: "Python", image: "x" };
const attach = vi.fn();
+2 -1
Переглянути файл
@@ -69,7 +69,8 @@ export function containerDebuggable(
*/
export function remoteDebugBlocked(config: RunConfig, targets: readonly RuntimeRunTarget[] = allRunTargets()): boolean {
config = withRunTarget(config);
if (!config.target || containerDebuggable(config, targets)) return false;
if (!config.target || config.target.startsWith("plugin-target:") || containerDebuggable(config, targets))
return false;
const target = targetName(config, targets) ?? config.target;
const container = targetOf(config, targets);
notifications().notify({
+5 -1
Переглянути файл
@@ -620,5 +620,9 @@
"plugin run target changed; navigate again": "plugin run target changed; navigate again",
"plugin host is unavailable": "plugin host is unavailable",
"language server targets belong to project settings": "language server targets belong to project settings",
"library documents are always read-only": "library documents are always read-only"
"library documents are always read-only": "library documents are always read-only",
"Debugging a plugin target requires a container root": "Debugging a plugin target requires a container root",
"This debugger does not support plugin run targets": "This debugger does not support plugin run targets",
"invalid debug launch arguments": "invalid debug launch arguments",
"unknown debug adapter": "unknown debug adapter"
}
+5 -1
Переглянути файл
@@ -6963,5 +6963,9 @@
"plugin run target changed; navigate again": "插件运行目标已更改;请重新导航",
"plugin host is unavailable": "插件宿主不可用",
"language server targets belong to project settings": "语言服务器目标属于项目设置",
"library documents are always read-only": "库文档始终为只读"
"library documents are always read-only": "库文档始终为只读",
"Debugging a plugin target requires a container root": "调试插件目标需要容器根目录",
"This debugger does not support plugin run targets": "此调试器不支持插件运行目标",
"invalid debug launch arguments": "调试启动参数无效",
"unknown debug adapter": "未知调试适配器"
}
+4 -1
Переглянути файл
@@ -16,7 +16,10 @@ interface run-targets {
/// Without workdir-args the wrapper manages its container directory itself.
/// The wrapper always starts in the configuration's LOCAL working directory.
/// Environment reaches the wrapper; forwarding into a container depends on its conventions.
/// Registration never selects the target. Debugging plugin targets is invalid.
/// Registration never selects the target. Debugging requires container-root and an
/// explicitly supported language debugger placement. Connect-back adapters listen
/// on the host; the target must configure the debuggee to connect to that host.
/// Stdio adapters run inside the target and must be installed there.
record run-target {
id: string,
name: string,