дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(debug): debug programs in a plugin run target (#2264)
Цей коміт міститься в:
1 джерело
0ccc1e687d
коміт
542584abb8
70 files changed
+709
-62
No files matched your search
Створено
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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:?}"));
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(®istration.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
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 @@
|
||||
446ff7b14c2d5b0fca8a3febe489906e9d56fc5b66878c953887e16dd54f5d5a
|
||||
4647fa9aec9732e3dd824efccd61b1c6b9c36645a3586f402f6a7a47419db61f
|
||||
@@ -1 +1 @@
|
||||
c55acafff3dbe090aac50873d957e73add4db50b4ed3af0def7ec77c1cd1a9d5
|
||||
6787b195df1057176ea9fcf11d0fcaf8cd347fc182fab94fe3a229063deed831
|
||||
+1
-1
@@ -1 +1 @@
|
||||
543e921a71667b481e9d7d3b6cf86bc46113ac7068a902d93063eff85c2c812d
|
||||
c2d17076337b1225ef8b0323b335a3b03bf022485088957c520e0cb797d7df7d
|
||||
+1
-1
@@ -1 +1 @@
|
||||
fa9ccf5c4f999031e1fb94b9e42b6caddfd5e5aeb08181660f51488711326550
|
||||
415a4cd17c573aeeb1b45d4b1af55834eab5e686c4f624495323e4fb63c3ac71
|
||||
+1
-1
@@ -1 +1 @@
|
||||
69fca6e054ccf188b7cfe23081470676bfbee56f96b0247c7ff67aba93a24d39
|
||||
5f502b7650acd5d07bffba8016680248b5ceac0926013916ff351562e3c6d831
|
||||
@@ -1 +1 @@
|
||||
cc2fdbb7953bac992375701812e8c4cf666652e027e8b6ecc68b43e0ae7d7ae1
|
||||
19e8d460269999aeac7b4021bfedb803ad0d68c24ce1c22f058070fb406a64c3
|
||||
@@ -1 +1 @@
|
||||
4e146d6caa1cc114d33ae18f396ac8da4d6391477daa32cc69e6feca087cf20a
|
||||
63809abb0f8a4f40b0c0f465062cc8ba0ebfd03d41a108ff19866e6565eda931
|
||||
+1
-1
@@ -1 +1 @@
|
||||
cfb6e388588d01fd3f772302049e673729138717ce7b761dfcecc4ee213a8d98
|
||||
99c968b98d8f1d895c428b4048451275bd322e7070aa0a020737097efea17eb1
|
||||
+1
-1
@@ -1 +1 @@
|
||||
6117ebfa5220ab04f03884d589140426d2ee693a23f058e2457952f30314099d
|
||||
c40a0fd8c6cf791e70ba0335c6cf0b2c82b250670ebad59b185e3e8719172409
|
||||
+1
-1
@@ -1 +1 @@
|
||||
e746d420d5f357e00b523e6e118d283df08e7b5f09f59b672645721a02f6ca46
|
||||
162774155d8b49d53b0c0c055736bd098723c66b02c0e1dc4389985982c57282
|
||||
@@ -1 +1 @@
|
||||
e127b6562e398cdb2d9d748434315ae78e61cfdfa3f1859da2e6eaeb2144030d
|
||||
39e3de11f69c09348906878367318157fca9a777f9ac1c4fb6ed303fa058e5a2
|
||||
@@ -1 +1 @@
|
||||
6459f7899464237399e33f4190a7526f05a010d6384a36f42c25195202fb6c58
|
||||
4a3f595d2a6a9b79da7e316a3da86c287e9761ea670319c331467e7b10639663
|
||||
@@ -1 +1 @@
|
||||
15236a23798998a0e1669c0ce657effb88481ef7236f5738519601e23b07dd1f
|
||||
bede1f0eaec356e3e35302e65e85531870625560bc9a3197f1acb58969beb4bd
|
||||
+1
-1
@@ -1 +1 @@
|
||||
8af4f2658b074aebdabefbe49d0641da767c109b107ecf182bcb8cb937092b82
|
||||
ad2dc0ad12adb9c6b25017b2cef78b7496e96d1bb6e88b24ea9b7b682120e786
|
||||
@@ -1 +1 @@
|
||||
57a4527c74f62fb98ba388223863d69afdfc9f75b6027af65658f5909379e3c9
|
||||
2c329cf7af36c9d65ed5b6be7c2a017c7a5a6214a4893cab7ae574acb76fe62f
|
||||
+1
-1
@@ -1 +1 @@
|
||||
c2a29a1982c278a21a772e440662817b5f096b4740ac99539c5ab2721251acf8
|
||||
39c85942177e937e48f32bcde86a0068dac484ba028a8d6785a5a2728db9e11b
|
||||
@@ -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 @@
|
||||
d560db74cda33ff86e7a5329df8f6cd8675f3df0288e427325e0777778f03f44
|
||||
616157bd77dc272f9e2dfed4c54c9d65fca110e55ad01f92b7c4f8a6136537b4
|
||||
@@ -1 +1 @@
|
||||
20b3cfd84d9129dd74b56c8766766fe714df7a5e6e051dc72c379d5fdb59b127
|
||||
022f216f0f4ca5da56f4b2fabf5ee4a987d5e9f9a752feecbb438f9b00b1acf4
|
||||
@@ -1 +1 @@
|
||||
d30ffd74e9e444f51024586f9c67cefede44ad89bd251410ea3881bd2255b1d5
|
||||
626f0e0c83e69704c0365c273b6f63d43278ee37786f38ed467ce908c30eadf7
|
||||
+1
-1
@@ -1 +1 @@
|
||||
9a354ee52f1ee1080ce9c687e9fc59e137da8f38c6723316b8aeecb5bdf1e4ad
|
||||
aa141701049f5c6c0d045341c3337344ed7a368800f08237b1b7812cce216814
|
||||
@@ -1 +1 @@
|
||||
ae183781f1fc1639fa7b1b3c4b8ff100e4e25daab69bbf876584050e5363473f
|
||||
d6c40099cc959ef2dead996beeb4acd73c1327597cd029674026c6f2bc878300
|
||||
@@ -1 +1 @@
|
||||
8a8c92661df8d693d5eabc45946e2d98c9e887090e4233e65e896421c655dcdd
|
||||
3201f643cf868f7a358ba812f9c90e06b982d864ed141b6b4c3fdeee13cb1559
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 @@
|
||||
34bea4bf36b7a814dafe02aaee02fe3354a667d1c8ca66f238de00322d0a035a
|
||||
e3c11d8969a1b8b52e7e745b558d12f804ebf533e7f0f3fe614ffe6f7b9c9468
|
||||
@@ -1 +1 @@
|
||||
fdd68915997803652ad6f5c549107b043b9001ad4e176d2473d9b37469fbe1cc
|
||||
b28ccfd442c6348e0826680ec97391cbe4ec6a93a27ff02e40f47341e21586b4
|
||||
@@ -1 +1 @@
|
||||
ad3fe934d30b48d53097bf4dee36a091b2410c0325924702c66e717405b58203
|
||||
d669a2600b0c59e91ac8a9b875a24bddf0eeddd404ce7b67da31733861a846b8
|
||||
@@ -1 +1 @@
|
||||
5710601823307d167805a7ca20e7498fb9c37082b20df7fd1f86e1cd66b438f3
|
||||
a5f0ebce6d30e0dc03769ebe5cb82d3704c5ccc32207aa2b3f5500de7b1a9152
|
||||
@@ -1 +1 @@
|
||||
1a486ceda9986bc253a0098c983f755c8860e6b7a3f2d7f0284b3b928b4e6c9e
|
||||
ee3ccb648ca4b98a0359521c4cd895b5e7336fd65101563f2ca82b8cbaeedc3d
|
||||
@@ -1 +1 @@
|
||||
13f27c819384f92e406bf7226a46d3fe5b063e850db3d64916e661e1a63f345a
|
||||
51f7167dd5ed21ed37e227bd3b100a5be5a57e7890a494c942fdd036b1307993
|
||||
@@ -1 +1 @@
|
||||
c7b2b471f26201c037522ee4a5b086d1c8ef88fbbb3f11f7279ad1a7185bdbae
|
||||
cd860e7a652951aaa42029da23fdeee7a3c09ab1bfcbfb0d1bb6c89036fc8817
|
||||
@@ -1 +1 @@
|
||||
82c71af4a1f771dbd16a4c8f71b9bd6a6a53d82bd92958d4862467237b82d228
|
||||
db3ee94b5cf4234cee7d61e08a9f8629d7e7f6adbf11208b54cd4ea44cf1fc13
|
||||
@@ -1 +1 @@
|
||||
65096a9b6331af9d40049801547e99e2fa1d5ae0f1065f6931af0aee8d2f24a7
|
||||
76d13e772046b192485ea59c7720177b563986e0167475c449a9a5c7430ef908
|
||||
@@ -1 +1 @@
|
||||
cf8d21da412fd64f1f42c72b13be762ff1bcd43dd19965fc1fce805c5e1012ae
|
||||
a22c9e7ebf79b2d5926424a028edc09aef40293db9ebc959d9ececa1f0ebe4ca
|
||||
@@ -1 +1 @@
|
||||
1ea1d7116e6b943e42580e43cb13199b6e48fc5acdf9784c4864adb7433ebb59
|
||||
fcc3d666fd1e89363fa8668dd10f89efc49f2376766282b734de4b719fdc9aff
|
||||
@@ -1 +1 @@
|
||||
05ff46a6c1af09b84f9a931d8956d79fbad9507dcff708ba021e074c55c062e2
|
||||
2eb742315f877e3251f992884160ed4d9a2940ee12a7d4d7a52669238d495930
|
||||
@@ -1 +1 @@
|
||||
26e57f5294b40bc31d9283843b2fcf6ad09d475fd160d61ddd3d420e54880a24
|
||||
c2feb6a4cd0e9908f078a3d0074732cfce653cc57cce9d1907838598515048ee
|
||||
@@ -1 +1 @@
|
||||
0d2c1140fe718d06e08af903ee94bfe87cd04de77c1299f6f5c337042c6f6197
|
||||
590b46034f6c39592a27cabd5b0b76809e7f7133d258f322604614ecb3479317
|
||||
@@ -1 +1 @@
|
||||
f4251a1cad1d5cea54559baca12d84fdf175e65db85ee3f4d29e381e045c40fb
|
||||
0d068ea0f0851e6e3166af0a91f290da8dc3f4a852b86050e7f7ced214a39124
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "未知调试适配器"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Заблокувати користувача