Skip to content

debugger support for dx #3814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 209 additions & 29 deletions packages/cli/src/build/builder.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -156,6 +160,7 @@ impl AppBuilder {
entropy_app_exe: None,
artifacts: None,
patch_cache: None,
pid: None,
})
}

Expand Down Expand Up @@ -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 }
}
}
}
};

Expand Down Expand Up @@ -263,6 +272,7 @@ impl AppBuilder {
StdoutReceived { .. } => {}
StderrReceived { .. } => {}
ProcessExited { .. } => {}
ProcessWaitFailed { .. } => {}
}

update
Expand Down Expand Up @@ -402,6 +412,7 @@ impl AppBuilder {
BuilderUpdate::StdoutReceived { .. } => {}
BuilderUpdate::StderrReceived { .. } => {}
BuilderUpdate::ProcessExited { .. } => {}
BuilderUpdate::ProcessWaitFailed { .. } => {}
}
}
}
Expand Down Expand Up @@ -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.
Expand All @@ -473,34 +484,22 @@ 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
Platform::Server
| 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(())
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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.
Expand Down Expand Up @@ -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 changes: 6 additions & 0 deletions packages/cli/src/build/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/build/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ impl RunArgs {

break;
}
BuilderUpdate::ProcessWaitFailed { .. } => {}
}
}
_ => {}
Expand Down
Loading
Loading