debugger support for dx (#3814)

* add vscode debugger for apps

* todo: need to disconnect when rebuilding

* update debugger

* add uri handler to remote launch the debugger

* attach

* dont panic

* fixed it - hung processes are not errors

* wip: android debugger

* clean it up a bit

* revert ios stdout/stderr

* wire up pid from ios

* use target family instead of target_arch

* fix android

* bring back softkill

* typo
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-24 23:31:42 +01:00
1 parent eb15b42ab1
commit b2bd1f48d4
13 files changed
+305 -41

No files matched your search

+209 -29
View File
@@ -1,10 +1,11 @@
use crate::{
BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform, ProgressRx, ProgressTx,
Result, StructuredOutput,
serve::WebServer, BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform,
ProgressRx, ProgressTx, Result, StructuredOutput,
};
use anyhow::Context;
use dioxus_cli_opt::process_file_to;
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
use itertools::Itertools;
use std::{
env,
time::{Duration, Instant, SystemTime},
@@ -91,6 +92,9 @@ pub(crate) struct AppBuilder {
pub compile_end: Option<Instant>,
pub bundle_start: Option<Instant>,
pub bundle_end: Option<Instant>,
/// The debugger for the app - must be enabled with the `d` key
pub(crate) pid: Option<u32>,
}
impl AppBuilder {
@@ -156,6 +160,7 @@ impl AppBuilder {
entropy_app_exe: None,
artifacts: None,
patch_cache: None,
pid: None,
})
}
@@ -183,12 +188,16 @@ impl AppBuilder {
StderrReceived { msg }
},
Some(status) = OptionFuture::from(self.child.as_mut().map(|f| f.wait())) => {
// Panicking here is on purpose. If the task crashes due to a JoinError (a panic),
// we want to propagate that panic up to the serve controller.
let status = status.unwrap();
self.child = None;
ProcessExited { status }
match status {
Ok(status) => {
self.child = None;
ProcessExited { status }
},
Err(err) => {
let () = futures_util::future::pending().await;
ProcessWaitFailed { err }
}
}
}
};
@@ -263,6 +272,7 @@ impl AppBuilder {
StdoutReceived { .. } => {}
StderrReceived { .. } => {}
ProcessExited { .. } => {}
ProcessWaitFailed { .. } => {}
}
update
@@ -402,6 +412,7 @@ impl AppBuilder {
BuilderUpdate::StdoutReceived { .. } => {}
BuilderUpdate::StderrReceived { .. } => {}
BuilderUpdate::ProcessExited { .. } => {}
BuilderUpdate::ProcessWaitFailed { .. } => {}
}
}
}
@@ -464,7 +475,7 @@ impl AppBuilder {
}
// We try to use stdin/stdout to communicate with the app
let running_process = match self.build.platform {
match self.build.platform {
// Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead
// use use the websocket to communicate with it. I wish we could merge the concepts here,
// like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else.
@@ -473,15 +484,12 @@ impl AppBuilder {
if open_browser {
self.open_web(open_address.unwrap_or(devserver_ip));
}
None
}
Platform::Ios => Some(self.open_ios_sim(envs).await?),
Platform::Ios => self.open_ios_sim(envs).await?,
Platform::Android => {
self.open_android_sim(false, devserver_ip, envs).await?;
None
}
// These are all just basically running the main exe, but with slightly different resource dir paths
@@ -489,18 +497,9 @@ impl AppBuilder {
| Platform::MacOS
| Platform::Windows
| Platform::Linux
| Platform::Liveview => Some(self.open_with_main_exe(envs)?),
| Platform::Liveview => self.open_with_main_exe(envs)?,
};
// If we have a running process, we need to attach to it and wait for its outputs
if let Some(mut child) = running_process {
let stdout = BufReader::new(child.stdout.take().unwrap());
let stderr = BufReader::new(child.stderr.take().unwrap());
self.stdout = Some(stdout.lines());
self.stderr = Some(stderr.lines());
self.child = Some(child);
}
self.builds_opened += 1;
Ok(())
@@ -728,19 +727,25 @@ impl AppBuilder {
/// paths right now, but they will when we start to enable things like swift integration.
///
/// Server/liveview/desktop are all basically the same, though
fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<()> {
let main_exe = self.app_exe();
tracing::debug!("Opening app with main exe: {main_exe:?}");
let child = Command::new(main_exe)
let mut child = Command::new(main_exe)
.envs(envs)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(child)
let stdout = BufReader::new(child.stdout.take().unwrap());
let stderr = BufReader::new(child.stderr.take().unwrap());
self.stdout = Some(stdout.lines());
self.stderr = Some(stderr.lines());
self.child = Some(child);
Ok(())
}
/// Open the web app by opening the browser to the given address.
@@ -765,7 +770,7 @@ impl AppBuilder {
///
/// TODO(jon): we should probably check if there's a simulator running before trying to install,
/// and open the simulator if we have to.
async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<()> {
tracing::debug!("Installing app to simulator {:?}", self.build.root_dir());
let res = Command::new("xcrun")
@@ -784,7 +789,7 @@ impl AppBuilder {
.iter()
.map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone()));
let child = Command::new("xcrun")
let mut child = Command::new("xcrun")
.arg("simctl")
.arg("launch")
.arg("--console")
@@ -796,7 +801,13 @@ impl AppBuilder {
.kill_on_drop(true)
.spawn()?;
Ok(child)
let stdout = BufReader::new(child.stdout.take().unwrap());
let stderr = BufReader::new(child.stderr.take().unwrap());
self.stdout = Some(stdout.lines());
self.stderr = Some(stderr.lines());
self.child = Some(child);
Ok(())
}
/// We have this whole thing figured out, but we don't actually use it yet.
@@ -1339,4 +1350,173 @@ We checked the folder: {}
pub(crate) fn can_receive_hotreloads(&self) -> bool {
matches!(&self.stage, BuildStage::Success | BuildStage::Failed)
}
pub(crate) async fn open_debugger(&mut self, server: &WebServer) -> Result<()> {
let url = match self.build.platform {
Platform::MacOS
| Platform::Windows
| Platform::Linux
| Platform::Server
| Platform::Liveview => {
let Some(Some(pid)) = self.child.as_mut().map(|f| f.id()) else {
tracing::warn!("No process to attach debugger to");
return Ok(());
};
format!(
"vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{}}}",
pid
)
}
Platform::Web => {
// code --open-url "vscode://DioxusLabs.dioxus/debugger?uri=http://127.0.0.1:8080"
// todo - debugger could open to the *current* page afaik we don't have a way to have that info
let address = server.devserver_address();
let base_path = self.build.config.web.app.base_path.clone();
let https = self.build.config.web.https.enabled.unwrap_or_default();
let protocol = if https { "https" } else { "http" };
let base_path = match base_path.as_deref() {
Some(base_path) => format!("/{}", base_path.trim_matches('/')),
None => "".to_owned(),
};
format!("vscode://DioxusLabs.dioxus/debugger?uri={protocol}://{address}{base_path}")
}
Platform::Ios => {
let Some(pid) = self.pid else {
tracing::warn!("No process to attach debugger to");
return Ok(());
};
format!(
"vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{pid}}}"
)
}
// https://stackoverflow.com/questions/53733781/how-do-i-use-lldb-to-debug-c-code-on-android-on-command-line/64997332#64997332
// https://android.googlesource.com/platform/development/+/refs/heads/main/scripts/gdbclient.py
// run lldbserver on the device and then connect
//
// # TODO: https://code.visualstudio.com/api/references/vscode-api#debug and
// # https://code.visualstudio.com/api/extension-guides/debugger-extension and
// # https://github.com/vadimcn/vscode-lldb/blob/6b775c439992b6615e92f4938ee4e211f1b060cf/extension/pickProcess.ts#L6
//
// res = {
// "name": "(lldbclient.py) Attach {} (port: {})".format(binary_name.split("/")[-1], port),
// "type": "lldb",
// "request": "custom",
// "relativePathBase": root,
// "sourceMap": { "/b/f/w" : root, '': root, '.': root },
// "initCommands": ['settings append target.exec-search-paths {}'.format(' '.join(solib_search_path))],
// "targetCreateCommands": ["target create {}".format(binary_name),
// "target modules search-paths add / {}/".format(sysroot)],
// "processCreateCommands": ["gdb-remote {}".format(str(port))]
// }
//
// https://github.com/vadimcn/codelldb/issues/213
//
// lots of pain to figure this out:
//
// (lldb) image add target/dx/tw6/debug/android/app/app/src/main/jniLibs/arm64-v8a/libdioxusmain.so
// (lldb) settings append target.exec-search-paths target/dx/tw6/debug/android/app/app/src/main/jniLibs/arm64-v8a/libdioxusmain.so
// (lldb) process handle SIGSEGV --pass true --stop false --notify true (otherwise the java threads cause crash)
//
Platform::Android => {
// adb push ./sdk/ndk/29.0.13113456/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/20/lib/linux/aarch64/lldb-server /tmp
// adb shell "/tmp/lldb-server --server --listen ..."
// "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'connect','port': {}}}",
// format!(
// "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{pid}}}"
// )
let tools = &self.build.workspace.android_tools()?;
// get the pid of the app
let pid = Command::new(&tools.adb)
.arg("shell")
.arg("pidof")
.arg(self.build.bundle_identifier())
.output()
.await
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap();
// copy the lldb-server to the device
let lldb_server = tools
.android_tools_dir()
.parent()
.unwrap()
.join("lib")
.join("clang")
.join("20")
.join("lib")
.join("linux")
.join("aarch64")
.join("lldb-server");
tracing::info!("Copying lldb-server to device: {lldb_server:?}");
_ = Command::new(&tools.adb)
.arg("push")
.arg(lldb_server)
.arg("/tmp/lldb-server")
.output()
.await;
// Forward requests on 10086 to the device
_ = Command::new(&tools.adb)
.arg("forward")
.arg("tcp:10086")
.arg("tcp:10086")
.output()
.await;
// start the server - running it multiple times will make the subsequent ones fail (which is fine)
_ = Command::new(&tools.adb)
.arg("shell")
.arg(r#"cd /tmp && ./lldb-server platform --server --listen '*:10086'"#)
.kill_on_drop(false)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
let program_path = self.build.main_exe();
format!(
r#"vscode://vadimcn.vscode-lldb/launch/config?{{
'name':'Attach to Android',
'type':'lldb',
'request':'attach',
'pid': '{pid}',
'processCreateCommands': [
'platform select remote-android',
'platform connect connect://localhost:10086',
'settings set target.inherit-env false',
'settings set target.inline-breakpoint-strategy always',
'settings set target.process.thread.step-avoid-regexp \"JavaBridge|JDWP|Binder|ReferenceQueueDaemon\"',
'process handle SIGSEGV --pass true --stop false --notify true"',
'settings append target.exec-search-paths {program_path}',
'attach --pid {pid}',
'continue'
]
}}"#,
program_path = program_path.display(),
)
.lines()
.map(|line| line.trim())
.join("")
}
};
tracing::info!("Opening debugger for [{}]: {url}", self.build.platform);
_ = tokio::process::Command::new("code")
.arg("--open-url")
.arg(url)
.spawn();
Ok(())
}
}
+6
View File
@@ -65,6 +65,12 @@ pub enum BuilderUpdate {
ProcessExited {
status: ExitStatus,
},
/// Waiting for the process failed. This might be because it's hung or being debugged.
/// This is not the same as the process exiting, so it should just be logged but not treated as an error.
ProcessWaitFailed {
err: std::io::Error,
},
}
impl BuildContext {
+6
View File
@@ -2128,6 +2128,12 @@ impl BuildRequest {
));
}
// for debuggability, we need to make sure android studio can properly understand our build
// https://stackoverflow.com/questions/68481401/debugging-a-prebuilt-shared-library-in-android-studio
if self.platform == Platform::Android {
cargo_args.push("-Clink-arg=-Wl,--build-id=sha1".to_string());
}
// Handle frameworks/dylibs by setting the rpath
// This is dependent on the bundle structure - in this case, appimage and appbundle for mac/linux
// todo: we need to figure out what to do for windows