diff --git a/README.md b/README.md index a117c68..928fbec 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ * [Examining the stack](#examining-the-stack) * [Examining source files](#examining-source-files) * [Examining data](#examining-data) + * [Async rust](#async-rust) + * [Async backtrace](#async-backtrace) * [Other commands](#other-commands) * [Tui interface](#tui-interface) * [Configuration](#configuration) @@ -426,6 +428,34 @@ Some examples: element at index 1 at field `field2` in dereferenced value of field `field1` at variable var1 🤡 +## Async rust + +Now BugStalker support some commands for interaction with async runtimes (currently only tokio multithread runtime is +supported). +There is also `oracle tokio`, but it adds some overhead to your program and +is not very informative unlike the commands presented below. + +### Async backtrace + +[demo async backtrace](https://github.com/godzie44/BugStalker/blob/master/doc/demo_async_bt.gif) + +While debugging an asynchronous application, you may want to control the state of your application. +If it were a regular synchronous application, you could use the `backtrace` command, +unfortunately for an application with an asynchronous runtime, this command is of little use. + +Therefore, BugStalker presents a family of commands "asynchronous backtrace". With their help +you can get information about the state of your asynchronous runtime - +the state of asynchronous workers and blocking threads, as well as information about each task in the system, +including its current state and its own "backtrace" - a stack of futures starting from the root. + +- `async backtrace` - show information about tokio async workers and blocking threads (alias: `async bt`). + It contains worker/blocking thread id, worker local tasks queue info, currently executed tasks for each worker. +- `async backtrace all` - same as previous (alias: `async bt all`), but contains information about all tasks in the + system. + Each task contains an id, and represents as a futures stack, where one future wait for other, and so on. +- `async task {regex}` - print all task with root async functions with names matched to regex. If regex are empty + then print active task. + ## Other commands Of course, the debugger provides many more commands: diff --git a/doc/demo_async_bt.gif b/doc/demo_async_bt.gif new file mode 100644 index 0000000..6739365 Binary files /dev/null and b/doc/demo_async_bt.gif differ diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 4eec2ae..20b44d8 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -82,18 +82,19 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.12", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", "itoa", "matchit", "memchr", @@ -105,28 +106,33 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", - "http-body", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -150,12 +156,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -257,7 +257,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags 2.6.0", + "bitflags", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -504,10 +504,27 @@ dependencies = [ ] [[package]] -name = "http-range-header" -version = "0.3.1" +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] [[package]] name = "httparse" @@ -533,7 +550,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -545,6 +562,40 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "tokio", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -676,9 +727,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", @@ -715,7 +766,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -954,7 +1005,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -1085,7 +1136,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper", + "hyper 0.14.30", "indexmap", "log", "memchr", @@ -1114,7 +1165,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1313,6 +1364,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "tempfile" version = "3.10.1" @@ -1382,9 +1439,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.39.2" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -1433,6 +1490,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio_tcp" +version = "0.0.0" +dependencies = [ + "log", + "rand", + "tokio", +] + [[package]] name = "tokioticker" version = "0.0.0" @@ -1494,17 +1560,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body", - "http-range-header", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "pin-project-lite", "tower-layer", "tower-service", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index fd83a06..4166731 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -15,6 +15,7 @@ members = [ "shlib/calc_bin", "shlib/printer_lib", "panic", - "calculations" + "calculations", + "tokio_tcp" ] resolver = "2" \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 4f17c8f..1d87196 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,4 +61,8 @@ Initiated by user or system panic (like divide by zero panic). ### Calculations -Program that calculates some values. Useful for watchpoints testing. \ No newline at end of file +Program that calculates some values. Useful for watchpoints testing. + +### Tokio_tcp + +Tokio tcp echo-server. Useful for testing `async ...` commands. \ No newline at end of file diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3482bfe..45ebfa9 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" publish = false [dependencies] -axum = {version = "0.6.18", features = ["default"]} +axum = { version = "0.7.5", features = ["default"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.0", features = ["full"] } tower = { version = "0.4", features = ["util", "timeout"] } -tower-http = { version = "0.4.0", features = ["add-extension", "trace"] } +tower-http = { version = "0.5.0", features = ["add-extension", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.0", features = ["serde", "v4"] } \ No newline at end of file diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index bd1c74b..2fdac41 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -4,7 +4,7 @@ //! //! - `GET /todos`: return a JSON list of Todos. //! - `POST /todos`: create a new Todo. -//! - `PUT /todos/:id`: update a specific Todo. +//! - `PATCH /todos/:id`: update a specific Todo. //! - `DELETE /todos/:id`: delete a specific Todo. //! //! Run with @@ -57,7 +57,7 @@ async fn main() { } else { Err(( StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), + format!("Unhandled internal error: {error}"), )) } })) @@ -67,11 +67,11 @@ async fn main() { ) .with_state(db); - tracing::debug!("listening on 127.0.0.1:3000"); - axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); + tracing::debug!("listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await.unwrap(); } // The query parameters for todos index diff --git a/examples/tokio_tcp/Cargo.toml b/examples/tokio_tcp/Cargo.toml new file mode 100644 index 0000000..124a9bc --- /dev/null +++ b/examples/tokio_tcp/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tokio_tcp" +version = "0.0.0" +edition = "2021" +workspace = "./.." +publish = false + +[dependencies] +tokio = { version = "1.40.0", features = ["full"] } +rand = "0.8" +log = "0.4.20" diff --git a/examples/tokio_tcp/src/main.rs b/examples/tokio_tcp/src/main.rs new file mode 100644 index 0000000..276739f --- /dev/null +++ b/examples/tokio_tcp/src/main.rs @@ -0,0 +1,64 @@ +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +use std::env; +use std::error::Error; +use std::time::Duration; + +#[tokio::main(worker_threads = 3)] +async fn main() -> Result<(), Box> { + // Allow passing an address to listen on as the first argument of this + // program, but otherwise we'll just set up our TCP listener on + // 127.0.0.1:8080 for connections. + let addr = env::args() + .nth(1) + .unwrap_or_else(|| "127.0.0.1:8080".to_string()); + + // Next up we create a TCP listener which will listen for incoming + // connections. This TCP listener is bound to the address we determined + // above and must be associated with an event loop. + let listener = TcpListener::bind(&addr).await?; + println!("Listening on: {}", addr); + + loop { + // Asynchronously wait for an inbound socket. + let (mut socket, _) = listener.accept().await?; + + // And this is where much of the magic of this server happens. We + // crucially want all clients to make progress concurrently, rather than + // blocking one on completion of another. To achieve this we use the + // `tokio::spawn` function to execute the work in the background. + // + // Essentially here we're executing a new task to run concurrently, + // which will allow all of our clients to be processed concurrently. + + tokio::spawn(async move { + let mut buf = vec![0; 1024]; + + // In a loop, read data from the socket and write the data back. + loop { + let n = socket + .read(&mut buf) + .await + .expect("failed to read data from socket"); + if n == 0 { + return; + } + + let (tx, rx) = tokio::sync::oneshot::channel(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(20)).await; + tx.send(1).unwrap(); + }); + + tokio::time::sleep(Duration::from_secs(5)).await; + _ = rx.await; + + socket + .write_all(&buf[0..n]) + .await + .expect("failed to write data to socket"); + } + }); + } +} diff --git a/examples/tokiotiker/Cargo.toml b/examples/tokiotiker/Cargo.toml index 9f9a65f..7029c68 100644 --- a/examples/tokiotiker/Cargo.toml +++ b/examples/tokiotiker/Cargo.toml @@ -6,6 +6,6 @@ workspace = "./.." publish = false [dependencies] -tokio = {version = "1.35.1", features = ["time", "default", "rt-multi-thread", "macros"]} +tokio = { version = "1.40.0", features = ["time", "default", "rt-multi-thread", "macros"] } rand = "0.8" log = "0.4.20" diff --git a/examples/vars/src/vars.rs b/examples/vars/src/vars.rs index fa7324b..c45845d 100644 --- a/examples/vars/src/vars.rs +++ b/examples/vars/src/vars.rs @@ -538,6 +538,13 @@ fn thread_local_const_init() { let nop: Option = None; } +fn boxed_array() { + let v = vec![1, 2, 3, 4, 5]; + let box_v = v.into_boxed_slice(); + + let nop: Option = None; +} + pub fn main() { scalar_types(); compound_types(); @@ -571,4 +578,5 @@ pub fn main() { uuid(); datetime(); thread_local_const_init(); + boxed_array(); } diff --git a/src/debugger/async/context.rs b/src/debugger/async/context.rs new file mode 100644 index 0000000..844e23d --- /dev/null +++ b/src/debugger/async/context.rs @@ -0,0 +1,19 @@ +use crate::debugger::Debugger; + +pub struct TokioAnalyzeContext<'a> { + debugger: &'a mut Debugger, +} + +impl<'a> TokioAnalyzeContext<'a> { + pub fn new(debugger: &'a mut Debugger) -> Self { + Self { debugger } + } + + pub fn debugger_mut(&mut self) -> &mut Debugger { + self.debugger + } + + pub fn debugger(&self) -> &Debugger { + self.debugger + } +} diff --git a/src/debugger/async/future.rs b/src/debugger/async/future.rs new file mode 100644 index 0000000..ba7ce59 --- /dev/null +++ b/src/debugger/async/future.rs @@ -0,0 +1,254 @@ +use crate::debugger::debugee::dwarf::r#type::TypeIdentity; +use crate::debugger::r#async::context::TokioAnalyzeContext; +use crate::debugger::r#async::task_from_header; +use crate::debugger::r#async::AsyncError; +use crate::debugger::r#async::Task; +use crate::debugger::variable::execute::QueryResult; +use crate::debugger::variable::value::{RustEnumValue, SpecializedValue, StructValue, Value}; +use crate::debugger::Error; +use std::num::ParseIntError; + +/// Container for storing the tasks spawned on a scheduler. +pub struct OwnedList {} + +impl OwnedList { + pub fn try_extract<'a>( + analyze_ctx: &'a TokioAnalyzeContext, + context: QueryResult<'a>, + ) -> Result, Error> { + let list = context + .modify_value(|ctx, val| { + val.field("current")? + .field("handle")? + .field("value")? + .field("__0")? + .field("__0")? + .deref(ctx)? + .field("data")? + .field("shared")? + .field("owned")? + .field("list") + }) + .ok_or(AsyncError::IncorrectAssumption("error while extract field (*CONTEXT.current.handle.value.__0.__0).data.shared.owned.list"))?; + + let lists = + list.modify_value(|_, l| l.field("lists")) + .ok_or(AsyncError::IncorrectAssumption( + "error while extract field `list.lists`", + ))?; + let lists_len = lists + .clone() + .into_value() + .field("length") + .ok_or(AsyncError::IncorrectAssumption( + "error while extract field `list.lists.length`", + ))? + .into_scalar() + .and_then(|scalar| scalar.try_as_number()) + .ok_or(AsyncError::IncorrectAssumption( + "`list.lists.length` should be number", + ))?; + + let data_qr = lists + .modify_value(|ctx, val| { + val.field("data_ptr")? + .slice(ctx, None, Some(lists_len as usize)) + }) + .ok_or(AsyncError::IncorrectAssumption( + "error while extract field `list.lists.data_ptr`", + ))?; + + let data = + data_qr + .clone() + .into_value() + .into_array() + .ok_or(AsyncError::IncorrectAssumption( + "`list.lists.data_ptr` should be an array", + ))?; + + let mut tasks = vec![]; + for el in data.items.unwrap_or_default() { + let value = el.value; + + let is_parking_lot_mutex = value + .clone() + .field("__0") + .ok_or(AsyncError::IncorrectAssumption("`__0` field not found"))? + .field("data") + .is_none(); + let field = if is_parking_lot_mutex { "__1" } else { "__0" }; + + let maybe_head = value + .field(field) + .and_then(|f| { + f.field("data") + .and_then(|f| f.field("value").and_then(|f| f.field("head"))) + }) + .ok_or(AsyncError::IncorrectAssumption( + "error while extract field `__0(__1).data.value.head` of OwnedList element", + ))?; + + if let Some(ptr) = maybe_head.field("__0") { + let ptr = ptr.field("pointer").ok_or(AsyncError::IncorrectAssumption( + "`pointer` field not found in OwnedList element", + ))?; + let mut next_ptr_qr = data_qr.clone().modify_value(|_, _| Some(ptr)); + + while let Some(ptr_qr) = next_ptr_qr { + next_ptr_qr = ptr_qr.clone().modify_value(|ctx, val| { + val.deref(ctx)? + .field("queue_next")? + .field("__0")? + .field("value")? + .field("__0")? + .field("pointer") + }); + + tasks.push(task_from_header(analyze_ctx.debugger(), ptr_qr)?); + } + } + } + + Ok(tasks) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseFutureStateError { + #[error("unexpected future structure representation")] + UnexpectedStructureRepr, + #[error("parse suspend state: {0}")] + ParseSuspendState(ParseIntError), + #[error("unexpected future state: {0}")] + UnexpectedState(String), +} + +#[derive(Debug)] +pub enum AsyncFnFutureState { + /// A future in this state is suspended at the await point in the code. + /// The compiler generates a special type to indicate a stop at such await point - + /// `SuspendX` where X is an integer number of such a point. + Suspend(u32), + /// The state of async fn that has been panicked on a previous poll. + Panicked, + /// Already resolved async fn. In other words, this future has been + /// polled and returned Poll::Ready(result) from the poll function. + Returned, + /// Already created async fn future but not yet polled (using await or select! or any other + /// async operation). + Unresumed, +} + +#[derive(Debug)] +pub struct AsyncFnFuture { + /// Future name (from debug info). + pub name: String, + /// Async function name. + pub async_fn: String, + /// Async function state. + pub state: AsyncFnFutureState, +} + +impl TryFrom<&RustEnumValue> for AsyncFnFuture { + type Error = AsyncError; + + fn try_from(repr: &RustEnumValue) -> Result { + const UNRESUMED_STATE: &str = "Unresumed"; + const RETURNED_STATE: &str = "Returned"; + const PANICKED_STATE: &str = "Panicked"; + const SUSPEND_STATE: &str = "Suspend"; + + let async_fn = repr.type_ident.namespace().join("::").to_string(); + let name = repr.type_ident.name_fmt().to_string(); + + let Some(Value::Struct(state)) = repr.value.as_deref().map(|m| &m.value) else { + return Err(AsyncError::ParseFutureState( + ParseFutureStateError::UnexpectedStructureRepr, + )); + }; + + let state = match state.type_ident.name_fmt() { + UNRESUMED_STATE => Ok(AsyncFnFutureState::Unresumed), + RETURNED_STATE => Ok(AsyncFnFutureState::Returned), + PANICKED_STATE => Ok(AsyncFnFutureState::Panicked), + str => { + if str.starts_with(SUSPEND_STATE) { + let str = str.trim_start_matches(SUSPEND_STATE); + let num: u32 = str.parse().map_err(|e| { + AsyncError::ParseFutureState(ParseFutureStateError::ParseSuspendState(e)) + })?; + Ok(AsyncFnFutureState::Suspend(num)) + } else { + return Err(AsyncError::ParseFutureState( + ParseFutureStateError::UnexpectedState(str.to_string()), + )); + } + } + }?; + + Ok(Self { + async_fn, + name, + state, + }) + } +} + +#[derive(Debug)] +pub struct CustomFuture { + pub name: TypeIdentity, +} + +impl From<&StructValue> for CustomFuture { + fn from(repr: &StructValue) -> Self { + let name = repr.type_ident.clone(); + Self { name } + } +} + +#[derive(Debug)] +pub struct TokioSleepFuture { + pub name: TypeIdentity, + pub instant: (i64, u32), +} + +impl TryFrom for TokioSleepFuture { + type Error = AsyncError; + + fn try_from(val: StructValue) -> Result { + let name = val.type_ident.clone(); + + let Some(Value::Struct(entry)) = val.field("entry") else { + return Err(AsyncError::IncorrectAssumption( + "Sleep future should contains `entry` field", + )); + }; + + let Some(Value::Struct(deadline)) = entry.field("deadline") else { + return Err(AsyncError::IncorrectAssumption( + "Sleep future should contains `entry.deadline` field", + )); + }; + + let Some(Value::Specialized { + value: Some(SpecializedValue::Instant(instant)), + .. + }) = deadline.field("std") + else { + return Err(AsyncError::IncorrectAssumption( + "Sleep future should contains `entry.deadline.std` field", + )); + }; + + Ok(Self { name, instant }) + } +} + +#[derive(Debug)] +pub enum Future { + AsyncFn(AsyncFnFuture), + TokioSleep(TokioSleepFuture), + Custom(CustomFuture), + UnknownFuture, +} diff --git a/src/debugger/async/mod.rs b/src/debugger/async/mod.rs new file mode 100644 index 0000000..c4dc2f6 --- /dev/null +++ b/src/debugger/async/mod.rs @@ -0,0 +1,342 @@ +mod context; +mod future; +mod park; +mod worker; + +pub use crate::debugger::r#async::future::AsyncFnFutureState; +pub use crate::debugger::r#async::future::Future; +pub use crate::debugger::r#async::park::BlockThread; +pub use crate::debugger::r#async::worker::Worker; + +use crate::debugger::address::RelocatedAddress; +use crate::debugger::debugee::dwarf::unit::DieVariant; +use crate::debugger::r#async::context::TokioAnalyzeContext; +use crate::debugger::r#async::future::OwnedList; +use crate::debugger::r#async::future::ParseFutureStateError; +use crate::debugger::r#async::future::{AsyncFnFuture, CustomFuture, TokioSleepFuture}; +use crate::debugger::r#async::park::try_as_park_thread; +use crate::debugger::r#async::worker::try_as_worker; +use crate::debugger::utils::PopIf; +use crate::debugger::variable::dqe::{Dqe, PointerCast, Selector}; +use crate::debugger::variable::execute::QueryResult; +use crate::debugger::variable::value::RustEnumValue; +use crate::debugger::variable::value::Value; +use crate::debugger::{Debugger, Error}; +use crate::{disable_when_not_stared, resolve_unit_call, weak_error}; +use nix::unistd::Pid; +use std::rc::Rc; + +#[derive(Debug)] +pub struct TaskBacktrace { + /// Tokio task id. + pub task_id: u64, + /// Futures stack. + pub futures: Vec, +} + +/// Async backtrace - represent information about current async runtime state. +#[derive(Debug)] +pub struct AsyncBacktrace { + /// Async workers information. + pub workers: Vec, + /// Blocking (parked) threads information. + pub block_threads: Vec, + /// Known tasks. Each task has own backtrace, where root is an async function. + pub tasks: Rc>, +} + +#[derive(Debug, thiserror::Error)] +pub enum AsyncError { + #[error("Backtrace for thread {0} not found")] + BacktraceShouldExist(Pid), + #[error("Parse future state: {0}")] + ParseFutureState(ParseFutureStateError), + #[error("Incorrect assumption about async runtime: {0}")] + IncorrectAssumption(&'static str), +} + +struct Task { + id: u64, + repr: RustEnumValue, +} + +impl Task { + fn from_enum_repr(id: u64, repr: RustEnumValue) -> Self { + Self { id, repr } + } + + fn backtrace(self) -> Result { + Ok(TaskBacktrace { + task_id: self.id, + futures: self.future_stack()?, + }) + } + + fn future_stack(self) -> Result, AsyncError> { + const AWAITEE_FIELD: &str = "__awaitee"; + + let mut result = vec![]; + + let mut next_future_repr = Some(self.repr); + while let Some(next_future) = next_future_repr.take() { + let future = AsyncFnFuture::try_from(&next_future)?; + result.push(Future::AsyncFn(future)); + + let Some(member) = next_future.value else { + break; + }; + let Value::Struct(val) = member.value else { + break; + }; + + let awaitee = val.field(AWAITEE_FIELD); + match awaitee { + Some(Value::RustEnum(next_future)) => { + next_future_repr = Some(next_future); + } + Some(Value::Struct(next_future)) => { + let type_ident = &next_future.type_ident; + + match type_ident.name_fmt() { + "Sleep" => { + let future = weak_error!(TokioSleepFuture::try_from(next_future)) + .map(Future::TokioSleep) + .unwrap_or(Future::UnknownFuture); + result.push(future); + } + _ => { + let future = CustomFuture::from(&next_future); + result.push(Future::Custom(future)); + } + } + + break; + } + _ => {} + } + } + + Ok(result) + } +} + +/// Get task information using `Header` structure. +/// See https://github.com/tokio-rs/tokio/blob/tokio-1.38.0/tokio/src/runtime/task/core.rs#L150 +fn task_from_header<'a>( + debugger: &'a Debugger, + task_header_ptr: QueryResult<'a>, +) -> Result { + let Value::Pointer(ref ptr) = task_header_ptr.value() else { + return Err(Error::Async(AsyncError::IncorrectAssumption( + "task.__0.raw.ptr.pointer not a pointer", + ))); + }; + + let vtab_ptr = task_header_ptr + .clone() + .modify_value(|ctx, val| val.deref(ctx)?.field("vtable")?.deref(ctx)?.field("poll")) + .unwrap(); + let Value::Pointer(ref fn_ptr) = vtab_ptr.value() else { + return Err(Error::Async(AsyncError::IncorrectAssumption( + "(*(*task.__0.raw.ptr.pointer).vtable).poll should be a pointer", + ))); + }; + let poll_fn_addr = fn_ptr + .value + .map(|a| RelocatedAddress::from(a as usize)) + .ok_or(AsyncError::IncorrectAssumption( + "(*(*task.__0.raw.ptr.pointer).vtable).poll fn pointer should contain a value", + ))?; + + // Now using the value of fn pointer finds poll function of this task + let poll_fn_addr_global = poll_fn_addr.into_global(&debugger.debugee)?; + let debug_info = debugger.debugee.debug_info(poll_fn_addr)?; + let poll_fn_die = debug_info.find_function_by_pc(poll_fn_addr_global)?.ok_or( + AsyncError::IncorrectAssumption("poll function for a task not found"), + )?; + + // poll function should have `T: Future` and `S: Schedule` type parameters + let t_tpl_die = + poll_fn_die + .get_template_parameter("T") + .ok_or(AsyncError::IncorrectAssumption( + "poll function should have `T` type argument", + ))?; + let s_tpl_die = + poll_fn_die + .get_template_parameter("S") + .ok_or(AsyncError::IncorrectAssumption( + "poll function should have `S` type argument", + ))?; + + // Now we try to find suitable `tokio::runtime::task::core::Cell` type + let unit = poll_fn_die.unit(); + let iter = resolve_unit_call!(debug_info.inner, unit, type_iter); + let mut cell_type_die = None; + for (typ, offset) in iter { + if typ.starts_with("Cell") { + let typ_entry = resolve_unit_call!(debug_info.inner, unit, find_entry, *offset); + if let Some(typ_entry) = typ_entry { + if let DieVariant::StructType(ref struct_type) = typ_entry.die { + let mut s_tpl_found = false; + let mut t_tpl_found = false; + + typ_entry.node.children.iter().for_each(|&idx| { + let entry = resolve_unit_call!(debug_info.inner, unit, entry, idx); + if let DieVariant::TemplateType(ref tpl) = entry.die { + if tpl.type_ref == t_tpl_die.type_ref { + t_tpl_found = true; + } + if tpl.type_ref == s_tpl_die.type_ref { + s_tpl_found = true; + } + } + }); + + if s_tpl_found & t_tpl_found { + cell_type_die = Some(struct_type.clone()); + break; + } + } + } + } + } + + let cell_type_die = cell_type_die.ok_or(AsyncError::IncorrectAssumption( + "tokio::runtime::task::core::Cell type not found", + ))?; + + // Cell type found, not cast task pointer to this type + let ptr = RelocatedAddress::from(ptr.value.unwrap() as usize); + let typ = format!( + "NonNull", + cell_type_die.base_attributes.name.unwrap() + ); + // let dqe = format!("*(({typ}){}).pointer", ptr); + let dqe = Dqe::Deref( + Dqe::Field( + Dqe::PtrCast(PointerCast { + ptr: ptr.as_usize(), + ty: typ, + }) + .boxed(), + "pointer".to_string(), + ) + .boxed(), + ); + + // having this type now possible to take underlying future and task_id + let task_id_dqe = Dqe::Field( + Dqe::Field(dqe.clone().boxed(), "core".to_string()).boxed(), + "task_id".to_string(), + ); + let future_dqe = Dqe::Field( + Dqe::Field( + Dqe::Field( + Dqe::Field( + Dqe::Field( + Dqe::Field(dqe.clone().boxed(), "core".to_string()).boxed(), + "stage".to_string(), + ) + .boxed(), + "stage".to_string(), + ) + .boxed(), + "__0".to_string(), + ) + .boxed(), + "value".to_string(), + ) + .boxed(), + "__0".to_string(), + ); + + let task_id = debugger + .read_variable(task_id_dqe)? + .pop_if_cond(|v| v.len() == 1) + .ok_or(Error::Async(AsyncError::IncorrectAssumption( + "task_id field not found in task structure", + )))?; + let task_id = task_id + .into_value() + .field("__0") + .and_then(|v| v.field("__0")?.field("__0")) + .ok_or(Error::Async(AsyncError::IncorrectAssumption( + "task_id field not found in task structure", + )))?; + let Value::Scalar(task_id) = task_id else { + return Err(Error::Async(AsyncError::IncorrectAssumption( + "unexpected task_id field format in task structure", + ))); + }; + let task_id = task_id.try_as_number().expect("should be a number") as u64; + + let mut future = debugger.read_variable(future_dqe)?; + let Some(QueryResult { + value: Some(Value::RustEnum(future)), + .. + }) = future.pop() + else { + return Err(Error::Async(AsyncError::IncorrectAssumption( + "task root future not found", + ))); + }; + let task = Task::from_enum_repr(task_id, future); + Ok(task) +} + +impl Debugger { + pub fn async_backtrace(&mut self) -> Result { + disable_when_not_stared!(self); + + let expl_ctx = self.exploration_ctx().clone(); + + let threads = self.debugee.thread_state(&expl_ctx)?; + let mut analyze_context = TokioAnalyzeContext::new(self); + let mut backtrace = AsyncBacktrace { + workers: vec![], + block_threads: vec![], + tasks: Rc::new(vec![]), + }; + + let mut tasks = Rc::new(vec![]); + + for thread in threads { + let worker = weak_error!(try_as_worker(&mut analyze_context, &thread)); + + if let Some(Some(w)) = worker { + // if this is an async worker we need to extract whole future list once + if tasks.is_empty() { + let mut context_initialized_var = analyze_context + .debugger() + .read_variable(Dqe::Variable(Selector::by_name("CONTEXT", false)))?; + let context_initialized = context_initialized_var + .pop_if_cond(|results| results.len() == 1) + .ok_or(Error::Async(AsyncError::IncorrectAssumption( + "CONTEXT not found", + )))?; + + tasks = Rc::new( + OwnedList::try_extract(&analyze_context, context_initialized)? + .into_iter() + .map(|t| t.backtrace().unwrap()) + .collect(), + ); + backtrace.tasks = tasks.clone(); + } + + backtrace.workers.push(w); + } else { + // maybe thread block on future? + let thread = weak_error!(try_as_park_thread(&mut analyze_context, &thread)); + if let Some(Some(pt)) = thread { + backtrace.block_threads.push(pt); + } + } + } + + self.expl_ctx_swap(expl_ctx); + + Ok(backtrace) + } +} diff --git a/src/debugger/async/park.rs b/src/debugger/async/park.rs new file mode 100644 index 0000000..5f79de7 --- /dev/null +++ b/src/debugger/async/park.rs @@ -0,0 +1,61 @@ +use crate::debugger::r#async::context::TokioAnalyzeContext; +use crate::debugger::r#async::{AsyncError, Task, TaskBacktrace}; +use crate::debugger::utils::PopIf; +use crate::debugger::variable::dqe::{Dqe, Selector}; +use crate::debugger::variable::value::Value; +use crate::debugger::{Error, ThreadSnapshot, Tracee}; + +/// Represent a thread that blocks on future execution (`tokio::task::spawn_blocking` for example how to create this thread). +#[derive(Debug)] +pub struct BlockThread { + /// A thread that block on future. + pub thread: Tracee, + /// A futures backtrace. + pub bt: TaskBacktrace, + /// True if thread in focus. This how `bs` choose an "active worker". + pub in_focus: bool, +} + +/// If thread `thread` is block on than return it, return `Ok(None)` if it's not. +pub fn try_as_park_thread( + context: &mut TokioAnalyzeContext, + thread: &ThreadSnapshot, +) -> Result, Error> { + let backtrace = thread + .bt + .as_ref() + .ok_or(AsyncError::BacktraceShouldExist(thread.thread.pid))?; + + let Some(block_on_frame_num) = backtrace.iter().position(|frame| { + let Some(fn_name) = frame.func_name.as_ref() else { + return false; + }; + fn_name.ends_with("CachedParkThread::block_on") + }) else { + return Ok(None); + }; + + let debugger = context.debugger_mut(); + debugger.expl_ctx_switch_thread(thread.thread.pid)?; + debugger.set_frame_into_focus(block_on_frame_num as u32)?; + + let future = debugger + .read_variable(Dqe::Variable(Selector::by_name("f", true)))? + .pop_if_cond(|qr| qr.len() == 1) + .ok_or(AsyncError::IncorrectAssumption( + "it looks like it's a park thread, but variable `f` not found at `block_on` fn", + ))?; + + let Some(Value::RustEnum(fut)) = future.value else { + return Err(Error::Async(AsyncError::IncorrectAssumption( + "it looks like it's a park thread, but variable `f` not a future", + ))); + }; + let task = Task::from_enum_repr(0, fut); + + Ok(Some(BlockThread { + thread: thread.thread.clone(), + in_focus: thread.in_focus, + bt: task.backtrace()?, + })) +} diff --git a/src/debugger/async/worker.rs b/src/debugger/async/worker.rs new file mode 100644 index 0000000..7146b89 --- /dev/null +++ b/src/debugger/async/worker.rs @@ -0,0 +1,277 @@ +use crate::debugger::r#async::context::TokioAnalyzeContext; +use crate::debugger::r#async::task_from_header; +use crate::debugger::r#async::Task; +use crate::debugger::r#async::{AsyncError, TaskBacktrace}; +use crate::debugger::utils::PopIf; +use crate::debugger::variable::dqe::{Dqe, Literal, Selector}; +use crate::debugger::variable::execute::QueryResult; +use crate::debugger::variable::value::{SupportedScalar, Value}; +use crate::debugger::{utils, Debugger, Error, ThreadSnapshot, Tracee}; +use crate::ui::command::parser::expression; +use chumsky::Parser; + +/// Async worker tasks local queue representation. +pub(super) struct LocalQueue { + pub _head: u32, + pub _tail: u32, + pub buff: Vec, +} + +fn extract_u32_from_atomic_u64(val: Value) -> Option { + let value = val.field("v")?.field("value")?; + if let Value::Scalar(value) = value { + if let Some(SupportedScalar::U64(u)) = value.value { + return Some((u & u32::MAX as u64) as u32); + } + } + None +} + +fn extract_u32_from_atomic_32(val: Value) -> Option { + let value = val + .field("inner")? + .field("value")? + .field("v")? + .field("value")?; + if let Value::Scalar(value) = value { + if let Some(SupportedScalar::U32(u)) = value.value { + return Some(u); + } + } + + None +} + +impl LocalQueue { + fn from_query_result( + debugger: &Debugger, + local_queue_inner: QueryResult, + ) -> Option { + let head = local_queue_inner.clone().into_value().field("head")?; + let head = extract_u32_from_atomic_u64(head)?; + + let tail = local_queue_inner.clone().into_value().field("tail")?; + let tail = extract_u32_from_atomic_32(tail)?; + + let mut task_buffer = Vec::with_capacity((tail - head) as usize); + let buffer = local_queue_inner + .clone() + .modify_value(|ctx, val| val.field("buffer")?.deref(ctx))?; + + let mut start = head; + while start < tail { + let task_header_ptr = buffer.clone().modify_value(|_, val| { + // extract pointer to `Header` from value of + // `UnsafeCell>>>` type + val.index(&Literal::Int(head as i64))? + .field("__0")? + .field("value")? + .field("value")? + .field("value")? + .field("__0")? + .field("raw")? + .field("ptr")? + .field("pointer") + })?; + let task = task_from_header(debugger, task_header_ptr).unwrap(); + task_buffer.push(task); + + start += 1; + } + + Some(LocalQueue { + _head: head, + _tail: tail, + buff: task_buffer, + }) + } +} + +/// Async worker known states. +pub(super) enum WorkerState { + RunTask(usize), + Park, + Unknown, +} + +/// Async worker internal information +pub(super) struct WorkerInternal { + pub(super) state: WorkerState, + pub(super) local_queue: LocalQueue, +} + +impl WorkerInternal { + /// Analyze a thread candidate to tokio multy_thread worker. + /// Return `None` if the thread is definitely not a worker, otherwise return [`WorkerInternal`]. + /// + /// # Arguments + /// + /// * `thread`: thread information + pub(super) fn analyze(ctx: &mut TokioAnalyzeContext, thread: &ThreadSnapshot) -> Option { + let debugger = ctx.debugger_mut(); + let context = debugger + .read_variable(Dqe::Variable(Selector::by_name("CONTEXT", false))) + .ok()? + .pop_if_cond(|results| results.len() == 1)?; + + let backtrace = thread.bt.as_ref()?; + + let mut state = None; + // find frame numer where run_task function executed + let run_task_frame_num = backtrace.iter().position(|frame| { + let Some(fn_name) = frame.func_name.as_ref() else { + return false; + }; + fn_name.ends_with("Context::run_task") + }); + if let Some(frame_num) = run_task_frame_num { + state = Some(WorkerState::RunTask(frame_num)); + } + + let park_frame_num = backtrace.iter().position(|frame| { + let Some(fn_name) = frame.func_name.as_ref() else { + return false; + }; + fn_name.ends_with("Context::park") + }); + if park_frame_num.is_some() { + state = Some(WorkerState::Park); + } + + let worker_run_frame_num = backtrace.iter().position(|frame| { + let Some(fn_name) = frame.func_name.as_ref() else { + return false; + }; + fn_name.ends_with("multi_thread::worker::run") + }); + if worker_run_frame_num.is_none() { + state = Some(WorkerState::Unknown); + } + let state = state?; + + use utils::PopIf; + + // local queue DQE: var (*(*(*CONTEXT.scheduler.inner).0.core.value.0).run_queue.inner).data + let local_queue = context.modify_value(|c, v| { + v.field("scheduler")? + .field("inner")? + .deref(c)? + .field("__0")? + .field("core")? + .field("value")? + .field("__0")? + .deref(c)? + .field("run_queue")? + .field("inner")? + .deref(c)? + .field("data") + })?; + + Some(Self { + state, + local_queue: LocalQueue::from_query_result(debugger, local_queue)?, + }) + } +} + +/// Tokio async worker (https://github.com/tokio-rs/tokio/blob/tokio-1.39.x/tokio/src/runtime/scheduler/multi_thread/worker.rs#L91) representation. +#[derive(Debug)] +pub struct Worker { + /// Active task number. + pub active_task: Option, + /// Active task taken directly from the stack trace (as an argument to the run function). + pub active_task_standby: Option, + /// Worker worker-local run queue. + pub queue: Vec, + /// A thread that holding a worker. + pub thread: Tracee, + /// True if thread in focus. This how `bs` choose an "active worker". + pub in_focus: bool, +} + +/// If thread `thread` is a worker return it, return `Ok(None)` if it's not. +pub fn try_as_worker( + context: &mut TokioAnalyzeContext, + thread: &ThreadSnapshot, +) -> Result, Error> { + let debugger = context.debugger_mut(); + debugger.expl_ctx_switch_thread(thread.thread.pid)?; + + let main_debug_info = debugger + .debugee + .program_debug_info()? + .pathname() + .to_path_buf(); + for i in 0..thread.bt.as_ref().map(|bt| bt.len()).unwrap_or_default() { + let expl_ctx = debugger.exploration_ctx(); + let debug_info = debugger.debugee.debug_info(expl_ctx.location().pc)?; + if debug_info.pathname() == main_debug_info { + break; + } + debugger.set_frame_into_focus(i as u32)?; + } + + let Some(worker) = WorkerInternal::analyze(context, thread) else { + return Ok(None); + }; + + let WorkerState::RunTask(frame_num) = worker.state else { + return Ok(Some(Worker { + active_task: None, + active_task_standby: None, + queue: Vec::default(), + thread: thread.thread.clone(), + in_focus: thread.in_focus, + })); + }; + + // first switch to run_task frame + context + .debugger_mut() + .set_frame_into_focus(frame_num as u32)?; + + let active_task_from_frame = || -> Option { + let task_header_ptr_dqe = expression::parser() + .parse("task.__0.raw.ptr.pointer") + .into_output()?; + let task_header_ptr = context + .debugger() + .read_argument(task_header_ptr_dqe) + .ok()? + .pop_if_cond(|results| results.len() == 1)?; + + let task = task_from_header(context.debugger(), task_header_ptr).ok()?; + task.backtrace().ok() + }; + let task_bt_standby = active_task_from_frame(); + + let context_initialized = context + .debugger() + .read_variable(Dqe::Variable(Selector::by_name("CONTEXT", false)))? + .pop_if_cond(|results| results.len() == 1) + .ok_or(Error::Async(AsyncError::IncorrectAssumption( + "CONTEXT not found", + )))?; + + let current_task_id = context_initialized + .into_value() + .field("current_task_id") + .and_then(|v| v.field("__0")) + .and_then(|v| v.field("__0")) + .and_then(|v| v.field("__0")) + .and_then(|v| v.field("__0")); + + let worker_bt = Worker { + active_task: current_task_id + .and_then(|t| t.into_scalar()?.try_as_number()) + .map(|id| id as u64), + active_task_standby: task_bt_standby, + queue: worker.local_queue.buff.into_iter().map(|t| t.id).collect(), + thread: thread.thread.clone(), + in_focus: thread.in_focus, + }; + + Ok(Some(worker_bt)) +} diff --git a/src/debugger/debugee/dwarf/mod.rs b/src/debugger/debugee/dwarf/mod.rs index 80d1921..0c2f330 100644 --- a/src/debugger/debugee/dwarf/mod.rs +++ b/src/debugger/debugee/dwarf/mod.rs @@ -17,7 +17,7 @@ use crate::debugger::debugee::dwarf::r#type::ComplexType; use crate::debugger::debugee::dwarf::symbol::SymbolTab; use crate::debugger::debugee::dwarf::unit::{ DieRef, DieVariant, DwarfUnitParser, Entry, FunctionDie, Node, ParameterDie, - PlaceDescriptorOwned, Unit, VariableDie, + PlaceDescriptorOwned, TemplateTypeParameter, Unit, VariableDie, }; use crate::debugger::debugee::dwarf::utils::PathSearchIndex; use crate::debugger::debugee::{Debugee, Location}; @@ -54,7 +54,8 @@ pub type EndianArcSlice = gimli::EndianArcSlice; pub struct DebugInformation { file: PathBuf, - inner: Dwarf, + // TODO tpm pub + pub inner: Dwarf, eh_frame: EhFrame, bases: BaseAddresses, units: Option>, @@ -1137,6 +1138,23 @@ impl<'ctx> ContextualDieRef<'ctx, 'ctx, FunctionDie> { } ranges } + + /// Return template parameter die by its name. + /// + /// # Arguments + /// + /// * `name`: tpl parameter name + pub fn get_template_parameter(&self, name: &str) -> Option<&TemplateTypeParameter> { + self.node.children.iter().find_map(|&idx| { + let entry = ctx_resolve_unit_call!(self, entry, idx); + if let DieVariant::TemplateType(ref tpl) = entry.die { + if tpl.base_attributes.name.as_deref() == Some(name) { + return Some(tpl); + } + } + None + }) + } } impl<'ctx> ContextualDieRef<'ctx, 'ctx, VariableDie> { diff --git a/src/debugger/debugee/dwarf/type.rs b/src/debugger/debugee/dwarf/type.rs index 497507c..3db7929 100644 --- a/src/debugger/debugee/dwarf/type.rs +++ b/src/debugger/debugee/dwarf/type.rs @@ -13,6 +13,7 @@ use gimli::{AttributeValue, DwAte, Expression}; use log::warn; use std::cell::Cell; use std::collections::{HashMap, HashSet, VecDeque}; +use std::fmt::{Display, Formatter}; use std::mem; use std::rc::Rc; use strum_macros::Display; @@ -63,7 +64,7 @@ impl TypeIdentity { self.name.as_deref() } - /// Return formatted type name. + /// Return formatted type name. #[inline(always)] pub fn name_fmt(&self) -> &str { self.name().unwrap_or("unknown") @@ -94,6 +95,16 @@ impl TypeIdentity { } } +impl Display for TypeIdentity { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}::{}", + self.namespace.join("::"), + self.name_fmt() + )) + } +} + #[derive(Clone, Debug)] pub struct MemberLocationExpression { expr: Expression, @@ -552,7 +563,7 @@ impl<'a> Iterator for BfsIterator<'a> { } } -/// Dwarf DIE parser. +/// DWARF DIE parser. pub struct TypeParser { known_type_ids: HashSet, processed_types: HashMap, diff --git a/src/debugger/debugee/dwarf/unit/mod.rs b/src/debugger/debugee/dwarf/unit/mod.rs index 5d9c605..2908522 100644 --- a/src/debugger/debugee/dwarf/unit/mod.rs +++ b/src/debugger/debugee/dwarf/unit/mod.rs @@ -740,7 +740,16 @@ impl Unit { } } + /// Return iterator over pairs (type_name, offset). + pub fn type_iter(&self) -> UnitResult> { + match self.lazy_part.get() { + None => UnitResult::Reload, + Some(additional) => UnitResult::Ok(additional.type_index.iter()), + } + } + /// Return all function entries suitable for template. + /// Note: this method requires a full unit. /// /// # Arguments /// diff --git a/src/debugger/debugee/dwarf/unwind.rs b/src/debugger/debugee/dwarf/unwind.rs index 6ce9aff..75e40d3 100644 --- a/src/debugger/debugee/dwarf/unwind.rs +++ b/src/debugger/debugee/dwarf/unwind.rs @@ -215,6 +215,7 @@ impl<'a> UnwindContext<'a> { fn return_address(&self) -> Option { let register = self.fde.cie().return_address_register(); + println!("register for unwind: {:?}", register); self.registers .value(register) .map(RelocatedAddress::from) diff --git a/src/debugger/debugee/mod.rs b/src/debugger/debugee/mod.rs index 286cc83..aa612e3 100644 --- a/src/debugger/debugee/mod.rs +++ b/src/debugger/debugee/mod.rs @@ -52,6 +52,7 @@ pub struct FrameInfo { } /// Debugee thread description. +#[derive(Debug, Clone)] pub struct ThreadSnapshot { /// Running thread info - pid, number and status. pub thread: Tracee, diff --git a/src/debugger/error.rs b/src/debugger/error.rs index 29036d8..dbdb463 100644 --- a/src/debugger/error.rs +++ b/src/debugger/error.rs @@ -1,6 +1,7 @@ use crate::debugger::address::GlobalAddress; use crate::debugger::debugee::dwarf::unit::DieRef; use crate::debugger::debugee::RendezvousError; +use crate::debugger::r#async::AsyncError; use crate::debugger::variable::value::ParsingError; use gimli::UnitOffset; use nix::unistd::Pid; @@ -164,6 +165,10 @@ pub enum Error { AttachedProcessNotFound(Pid), #[error("attach a running process: {0}")] Attach(nix::Error), + + // --------------------------------- async exploration errors ---------------------------------- + #[error("{0}. Maybe your async runtime version is unsupported.")] + Async(#[from] AsyncError), } impl Error { @@ -226,6 +231,7 @@ impl Error { Error::AddressAlreadyObserved => false, Error::UnknownScope => false, Error::VarFrameNotFound => false, + Error::Async(_) => false, // currently fatal errors Error::DwarfParsing(_) => true, diff --git a/src/debugger/mod.rs b/src/debugger/mod.rs index 41a43cf..6a323d2 100644 --- a/src/debugger/mod.rs +++ b/src/debugger/mod.rs @@ -1,4 +1,5 @@ pub mod address; +pub mod r#async; mod breakpoint; mod code; mod debugee; @@ -424,6 +425,10 @@ impl Debugger { Ok(&self.expl_context) } + fn expl_ctx_swap(&mut self, new: ExplorationContext) { + self.expl_context = new; + } + /// Restore frame from user defined to real. fn expl_ctx_restore_frame(&mut self) -> Result<&ExplorationContext, Error> { self.expl_ctx_update_location() diff --git a/src/debugger/utils.rs b/src/debugger/utils.rs index 1c9e767..324f1e4 100644 --- a/src/debugger/utils.rs +++ b/src/debugger/utils.rs @@ -21,3 +21,22 @@ impl TryGetOrInsert for Option { } } } + +pub trait PopIf { + fn pop_if_cond(&mut self, pred: F) -> Option + where + F: FnOnce(&Self) -> bool; +} + +impl PopIf for Vec { + fn pop_if_cond(&mut self, pred: F) -> Option + where + F: FnOnce(&Self) -> bool, + { + if pred(self) { + self.pop() + } else { + None + } + } +} diff --git a/src/debugger/variable/execute.rs b/src/debugger/variable/execute.rs index 55f390b..a7d2a51 100644 --- a/src/debugger/variable/execute.rs +++ b/src/debugger/variable/execute.rs @@ -28,7 +28,8 @@ pub enum QueryResultKind { /// Result of DQE evaluation. #[derive(Clone)] pub struct QueryResult<'a> { - value: Option, + // TODO tmp pub + pub value: Option, scope: Option>, kind: QueryResultKind, base_type: Rc, diff --git a/src/debugger/variable/mod.rs b/src/debugger/variable/mod.rs index a0d1087..8d88d5a 100644 --- a/src/debugger/variable/mod.rs +++ b/src/debugger/variable/mod.rs @@ -1,6 +1,6 @@ use crate::debugger::debugee::dwarf::{AsAllocatedData, ContextualDieRef, NamespaceHierarchy}; use bytes::Bytes; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Display, Formatter}; pub mod dqe; pub mod execute; @@ -10,7 +10,7 @@ mod r#virtual; /// Identifier of a query result. /// Consists name and namespace of the variable or argument. -#[derive(Clone, Default, PartialEq, Debug)] +#[derive(Clone, Default, PartialEq)] pub struct Identity { namespace: NamespaceHierarchy, pub name: Option, diff --git a/src/debugger/variable/value/mod.rs b/src/debugger/variable/value/mod.rs index 055ebe2..3067429 100644 --- a/src/debugger/variable/value/mod.rs +++ b/src/debugger/variable/value/mod.rs @@ -18,7 +18,7 @@ use uuid::Uuid; mod bfs; pub(super) mod parser; -mod specialization; +pub mod specialization; pub use crate::debugger::variable::value::specialization::SpecializedValue; @@ -137,7 +137,7 @@ pub struct ScalarValue { } impl ScalarValue { - fn try_as_number(&self) -> Option { + pub fn try_as_number(&self) -> Option { match self.value { Some(SupportedScalar::I8(num)) => Some(num as i64), Some(SupportedScalar::I16(num)) => Some(num as i64), @@ -183,6 +183,15 @@ impl StructValue { } }) } + + pub fn into_member_n(self, n: usize) -> Option { + let mut this = self; + if this.members.len() > n { + let m = this.members.swap_remove(n); + return Some(m.value); + }; + None + } } /// Array item representation. @@ -381,6 +390,20 @@ impl Debug for Value { } impl Value { + pub fn into_scalar(self) -> Option { + match self { + Value::Scalar(v) => Some(v), + _ => None, + } + } + + pub fn into_array(self) -> Option { + match self { + Value::Array(arr) => Some(arr), + _ => None, + } + } + /// Return literal equals representation of a value. pub fn as_literal(&self) -> Option { match self { diff --git a/src/debugger/variable/value/specialization/mod.rs b/src/debugger/variable/value/specialization/mod.rs index 4c54317..58afa5d 100644 --- a/src/debugger/variable/value/specialization/mod.rs +++ b/src/debugger/variable/value/specialization/mod.rs @@ -396,6 +396,10 @@ impl<'a> VariableParserExtension<'a> { inner_val: Value, type_params: &HashMap>, ) -> Result, ParsingError> { + if type_params.is_empty() { + return Ok(None); + } + let inner_type = type_params .get("T") .ok_or(TypeParameterNotFound("T"))? diff --git a/src/ui/command/async.rs b/src/ui/command/async.rs new file mode 100644 index 0000000..c5b935b --- /dev/null +++ b/src/ui/command/async.rs @@ -0,0 +1,28 @@ +use crate::debugger::r#async::AsyncBacktrace; +use crate::debugger::{Debugger, Error}; + +#[derive(Debug, Clone)] +pub enum Command { + ShortBacktrace, + FullBacktrace, + CurrentTask(Option), +} + +pub struct Handler<'a> { + dbg: &'a mut Debugger, +} + +impl<'a> Handler<'a> { + pub fn new(debugger: &'a mut Debugger) -> Self { + Self { dbg: debugger } + } + + pub fn handle(&mut self, cmd: &Command) -> Result { + let result = match cmd { + Command::ShortBacktrace => self.dbg.async_backtrace()?, + Command::FullBacktrace => self.dbg.async_backtrace()?, + Command::CurrentTask(_) => self.dbg.async_backtrace()?, + }; + Ok(result) + } +} diff --git a/src/ui/command/mod.rs b/src/ui/command/mod.rs index eff96c5..5c7105a 100644 --- a/src/ui/command/mod.rs +++ b/src/ui/command/mod.rs @@ -2,10 +2,10 @@ //! This is the most preferred way to use a debugger functional from UI layer. //! //! Contains commands and corresponding command handlers. Command is a some sort of request to -//! debugger that define an action and a list of input arguments. Command handler validate command, -//! define what exactly debugger must to do and return result of it. +//! debugger that defines an action and a list of input arguments. pub mod arguments; +pub mod r#async; pub mod backtrace; pub mod r#break; pub mod r#continue; @@ -63,6 +63,7 @@ pub enum Command { SourceCode(source_code::Command), SkipInput, Oracle(String, Option), + Async(r#async::Command), Help { command: Option, reason: Option, diff --git a/src/ui/command/parser/expression.rs b/src/ui/command/parser/expression.rs index dc94243..36574ba 100644 --- a/src/ui/command/parser/expression.rs +++ b/src/ui/command/parser/expression.rs @@ -25,6 +25,9 @@ fn ptr_cast<'a>() -> impl Parser<'a, &'a str, Dqe, Err<'a>> + Clone { || *c == '&' || *c == '_' || *c == ',' + || *c == '{' + || *c == '}' + || *c == '#' || *c == '\'' }) .repeated() diff --git a/src/ui/command/parser/mod.rs b/src/ui/command/parser/mod.rs index 9f387b3..f975315 100644 --- a/src/ui/command/parser/mod.rs +++ b/src/ui/command/parser/mod.rs @@ -1,7 +1,7 @@ pub mod expression; use super::r#break::BreakpointIdentity; -use super::{frame, memory, register, source_code, thread, watch, Command, CommandError}; +use super::{frame, memory, r#async, register, source_code, thread, watch, Command, CommandError}; use super::{r#break, CommandResult}; use crate::debugger::register::debug::BreakCondition; use crate::debugger::variable::dqe::Dqe; @@ -66,6 +66,10 @@ pub const SOURCE_COMMAND: &str = "source"; pub const SOURCE_COMMAND_DISASM_SUBCOMMAND: &str = "asm"; pub const SOURCE_COMMAND_FUNCTION_SUBCOMMAND: &str = "fn"; pub const ORACLE_COMMAND: &str = "oracle"; +pub const ASYNC_COMMAND: &str = "async"; +pub const ASYNC_COMMAND_BACKTRACE_SUBCOMMAND: &str = "backtrace"; +pub const ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT: &str = "bt"; +pub const ASYNC_COMMAND_TASK_SUBCOMMAND: &str = "task"; pub const HELP_COMMAND: &str = "help"; pub const HELP_COMMAND_SHORT: &str = "h"; @@ -265,6 +269,7 @@ impl Command { let op2 = |full, short| op(full).or(op(short)); let op2_w_arg = |full, short| op_w_arg(full).or(op_w_arg(short)); + let sub_op2 = |full, short| sub_op(full).or(sub_op(short)); let sub_op2_w_arg = |full, short| sub_op_w_arg(full).or(sub_op_w_arg(short)); let r#continue = op2(CONTINUE_COMMAND, CONTINUE_COMMAND_SHORT).to(Command::Continue); @@ -420,6 +425,33 @@ impl Command { .to(Command::SharedLib) .boxed(); + let r#async = op_w_arg(ASYNC_COMMAND) + .ignore_then(choice(( + sub_op2( + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND, + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT, + ) + .ignore_then(sub_op(BACKTRACE_ALL_SUBCOMMAND).or_not()) + .map(|all| { + if all.is_some() { + Command::Async(r#async::Command::FullBacktrace) + } else { + Command::Async(r#async::Command::ShortBacktrace) + } + }), + sub_op(ASYNC_COMMAND_TASK_SUBCOMMAND) + .ignore_then(any().repeated().padded().to_slice()) + .map(|s| { + let s = s.trim(); + if s.is_empty() { + Command::Async(r#async::Command::CurrentTask(None)) + } else { + Command::Async(r#async::Command::CurrentTask(Some(s.to_string()))) + } + }), + ))) + .boxed(); + let oracle = op_w_arg(ORACLE_COMMAND) .ignore_then(text::ident().padded().then(text::ident().or_not())) .map(|(name, subcmd)| { @@ -449,6 +481,7 @@ impl Command { command(SHARED_LIB_COMMAND, shared_lib), command(ORACLE_COMMAND, oracle), command(WATCH_COMMAND, watchpoint), + command(ASYNC_COMMAND, r#async), )) } @@ -975,6 +1008,42 @@ fn test_parser() { )); }, }, + TestCase { + inputs: vec!["async backtrace", " async bt "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Async(r#async::Command::ShortBacktrace) + )); + }, + }, + TestCase { + inputs: vec!["async backtrace all", " async bt all "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Async(r#async::Command::FullBacktrace) + )); + }, + }, + TestCase { + inputs: vec!["async task", " async task "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Async(r#async::Command::CurrentTask(None)) + )); + }, + }, + TestCase { + inputs: vec!["async task abc.*", " async task abc.* "], + command_matcher: |result| { + assert!(matches!( + result.unwrap(), + Command::Async(r#async::Command::CurrentTask(Some(s))) if s == "abc.*" + )); + }, + }, TestCase { inputs: vec!["oracle tokio", " oracle tokio "], command_matcher: |result| { diff --git a/src/ui/console/async.rs b/src/ui/console/async.rs new file mode 100644 index 0000000..2f15bf1 --- /dev/null +++ b/src/ui/console/async.rs @@ -0,0 +1,198 @@ +use crate::debugger::r#async::AsyncBacktrace; +use crate::debugger::r#async::AsyncFnFutureState; +use crate::debugger::r#async::Future; +use crate::debugger::r#async::TaskBacktrace; +use crate::ui::console::print::style::{ + AsyncTaskView, ErrorView, FutureFunctionView, FutureTypeView, +}; +use crate::ui::console::print::ExternalPrinter; +use crossterm::style::Stylize; +use nix::errno::Errno; +use nix::libc; +use nix::sys::time::TimeSpec; +use std::mem::MaybeUninit; +use std::ops::Sub; +use std::time::Duration; + +fn print_future(num: u32, future: &Future, printer: &ExternalPrinter) { + match future { + Future::AsyncFn(fn_fut) => { + printer.println(format!( + "#{num} async fn {}", + FutureFunctionView::from(&fn_fut.async_fn) + )); + match fn_fut.state { + AsyncFnFutureState::Suspend(await_num) => { + printer.println(format!("\tsuspended at await point {}", await_num)); + } + AsyncFnFutureState::Panicked => { + printer.println("\tpanicked!"); + } + AsyncFnFutureState::Returned => { + printer.println("\talready resolved"); + } + AsyncFnFutureState::Unresumed => { + printer.println("\tjust created"); + } + } + } + Future::Custom(custom_fut) => { + printer.println(format!( + "#{num} future {}", + FutureTypeView::from(custom_fut.name.to_string()) + )); + } + Future::TokioSleep(sleep_fut) => { + fn now_timespec() -> Result { + let mut t = MaybeUninit::uninit(); + let res = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, t.as_mut_ptr()) }; + if res == -1 { + return Err(Errno::last()); + } + let t = unsafe { t.assume_init() }; + Ok(TimeSpec::new(t.tv_sec, t.tv_nsec)) + } + + pub fn diff_from_now(i: (i64, u32)) -> (std::cmp::Ordering, Duration) { + let now = now_timespec().expect("broken system clock"); + let this = TimeSpec::new(i.0, i.1 as i64); + if this < now { + (std::cmp::Ordering::Less, Duration::from(now.sub(this))) + } else { + (std::cmp::Ordering::Greater, Duration::from(this.sub(now))) + } + } + + let render = match diff_from_now(sleep_fut.instant) { + (std::cmp::Ordering::Less, d) => { + format!("already happened {} seconds ago ", d.as_secs()) + } + (std::cmp::Ordering::Greater, d) => { + format!("{} seconds from now", d.as_secs()) + } + _ => unreachable!(), + }; + + printer.println(format!("#{num} sleep future, sleeping {render}",)); + } + Future::UnknownFuture => { + printer.println(format!("#{num} undefined future",)); + } + } +} + +fn print_task(task: &TaskBacktrace, printer: &ExternalPrinter) { + let task_descr = format!("Task id: {}", task.task_id).bold(); + printer.println(AsyncTaskView::from(task_descr)); + + for (i, fut) in task.futures.iter().enumerate() { + print_future(i as u32, fut, printer); + } +} + +pub fn print_backtrace(backtrace: &mut AsyncBacktrace, printer: &ExternalPrinter) { + backtrace.workers.sort_by_key(|w| w.thread.number); + backtrace.block_threads.sort_by_key(|pt| pt.thread.number); + + for bt in &backtrace.block_threads { + let block_thread_header = format!( + "Thread #{} (pid: {}) block on:", + bt.thread.number, bt.thread.pid, + ); + if bt.in_focus { + printer.println(block_thread_header.bold()); + } else { + printer.println(block_thread_header); + } + + for (i, fut) in bt.bt.futures.iter().enumerate() { + print_future(i as u32, fut, printer); + } + } + + printer.println(""); + + for worker in &backtrace.workers { + let worker_header = format!( + "Async worker #{} (pid: {}, local queue length: {})", + worker.thread.number, + worker.thread.pid, + worker.queue.len(), + ); + if worker.in_focus { + printer.println(worker_header.bold()); + } else { + printer.println(worker_header); + } + + if let Some(active_task_idx) = worker.active_task { + let active_task = backtrace + .tasks + .get(active_task_idx as usize) + .or(worker.active_task_standby.as_ref()); + + if let Some(active_task) = active_task { + let task_descr = format!("Active task: {}", active_task.task_id).bold(); + printer.println(AsyncTaskView::from(task_descr)); + + for (i, fut) in active_task.futures.iter().enumerate() { + print_future(i as u32, fut, printer); + } + } + } + } +} + +pub fn print_backtrace_full(backtrace: &mut AsyncBacktrace, printer: &ExternalPrinter) { + print_backtrace(backtrace, printer); + + printer.println(""); + printer.println("Known tasks:"); + + for task in backtrace.tasks.iter() { + print_task(task, printer); + } +} + +pub fn print_task_ex(backtrace: &AsyncBacktrace, printer: &ExternalPrinter, regex: Option<&str>) { + if let Some(regex) = regex { + let re = regex::Regex::new(regex).unwrap(); + + let tasks = &backtrace.tasks; + for task in tasks.iter() { + if let Some(Future::AsyncFn(f)) = task.futures.first() { + if re.find(&f.async_fn).is_some() { + print_task(task, printer); + } + } + } + } else { + // print current task + + let mb_active_block_thread = backtrace.block_threads.iter().find(|t| t.in_focus); + let active_task = if let Some(bt) = mb_active_block_thread { + &bt.bt + } else { + let mb_active_worker = backtrace.workers.iter().find(|t| t.in_focus); + let Some(active_worker) = mb_active_worker else { + printer.println(ErrorView::from("no active worker found")); + return; + }; + let active_task_id = active_worker.active_task; + let mb_active_task = if let Some(active_task_id) = active_task_id { + backtrace.tasks.iter().find(|t| t.task_id == active_task_id) + } else { + active_worker.active_task_standby.as_ref() + }; + + let Some(active_task) = mb_active_task else { + printer.println(ErrorView::from("no active task found for current worker")); + return; + }; + + active_task + }; + + print_task(active_task, printer); + } +} diff --git a/src/ui/console/editor.rs b/src/ui/console/editor.rs index 335979e..991b1c2 100644 --- a/src/ui/console/editor.rs +++ b/src/ui/console/editor.rs @@ -1,6 +1,8 @@ use crate::ui::command::parser::{ - ARG_ALL_KEY, ARG_COMMAND, BACKTRACE_ALL_SUBCOMMAND, BACKTRACE_COMMAND, BACKTRACE_COMMAND_SHORT, - BREAK_COMMAND, BREAK_COMMAND_SHORT, CONTINUE_COMMAND, CONTINUE_COMMAND_SHORT, FRAME_COMMAND, + ARG_ALL_KEY, ARG_COMMAND, ASYNC_COMMAND, ASYNC_COMMAND_BACKTRACE_SUBCOMMAND, + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT, ASYNC_COMMAND_TASK_SUBCOMMAND, + BACKTRACE_ALL_SUBCOMMAND, BACKTRACE_COMMAND, BACKTRACE_COMMAND_SHORT, BREAK_COMMAND, + BREAK_COMMAND_SHORT, CONTINUE_COMMAND, CONTINUE_COMMAND_SHORT, FRAME_COMMAND, FRAME_COMMAND_INFO_SUBCOMMAND, FRAME_COMMAND_SWITCH_SUBCOMMAND, HELP_COMMAND, HELP_COMMAND_SHORT, MEMORY_COMMAND, MEMORY_COMMAND_READ_SUBCOMMAND, MEMORY_COMMAND_SHORT, MEMORY_COMMAND_WRITE_SUBCOMMAND, ORACLE_COMMAND, REGISTER_COMMAND, @@ -413,6 +415,17 @@ pub fn create_editor( SOURCE_COMMAND_FUNCTION_SUBCOMMAND.to_string(), ], }, + CommandHint { + short: None, + long: ASYNC_COMMAND.to_string(), + subcommands: vec![ + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND.to_string(), + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT.to_string(), + ASYNC_COMMAND_TASK_SUBCOMMAND.to_string(), + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND.to_string() + " all", + ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT.to_string() + " all", + ], + }, CommandHint { short: None, long: ORACLE_COMMAND.to_string(), diff --git a/src/ui/console/help.rs b/src/ui/console/help.rs index 52317fd..f125a88 100644 --- a/src/ui/console/help.rs +++ b/src/ui/console/help.rs @@ -23,6 +23,7 @@ reg, register read|write|info -- read, write, or view debugged pro thread info|current|switch -- show list of threads or current (in focus) thread or set thread in focus sharedlib info -- show list of shared libraries source asm|fn| -- show source code or assembly instructions for current (in focus) function +async backtrace|backtrace all|task -- commands for async rust oracle <>| -- execute a specific oracle h, help <>| -- show help tui -- change ui mode to tui @@ -275,6 +276,17 @@ source asm - show assembly of function in focus source - show line in focus with lines up and down of this line "; +pub const HELP_ASYNC: &str = "\ +\x1b[32;1masync\x1b[0m +Commands for async rust (currently for tokio runtime only). + +Available subcomands: +async backtrace - show state of async workers and blocking threads +async backtrace all - show state of async workers and blocking threads, show info about all running tasks +async task - show active task (active task means a task that is running on the thread that is currently in focus) if `async_fn_regex` parameter is empty, +or show task list with async functions matched by regular expression. +"; + pub const HELP_TUI: &str = "\ \x1b[32;1mtui\x1b[0m Change ui mode to terminal ui. @@ -327,6 +339,7 @@ impl Helper { Some(parser::THREAD_COMMAND) => HELP_THREAD, Some(parser::SHARED_LIB_COMMAND) => HELP_SHARED_LIB, Some(parser::SOURCE_COMMAND) => HELP_SOURCE, + Some(parser::ASYNC_COMMAND) => HELP_ASYNC, Some(parser::ORACLE_COMMAND) => self.oracle_help.get_or_insert_with(|| { let mut help = HELP_ORACLE.to_string(); let oracles = debugger.all_oracles(); diff --git a/src/ui/console/mod.rs b/src/ui/console/mod.rs index 68ee2d3..318eae6 100644 --- a/src/ui/console/mod.rs +++ b/src/ui/console/mod.rs @@ -1,3 +1,23 @@ +use std::io::{BufRead, BufReader}; +use std::process::exit; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::mpsc::{Receiver, SyncSender}; +use std::sync::{mpsc, Arc, Mutex, Once}; +use std::thread; +use std::time::Duration; + +use crossterm::style::{Color, Stylize}; +use nix::sys::signal::{kill, Signal}; +use nix::unistd::Pid; +use rustyline::error::ReadlineError; +use rustyline::history::MemHistory; +use rustyline::Editor; +use timeout_readwrite::TimeoutReader; + +use debugger::Error; +use r#break::Command as BreakpointCommand; + use crate::debugger; use crate::debugger::process::{Child, Installed}; use crate::debugger::variable::dqe::{Dqe, Selector}; @@ -7,6 +27,7 @@ use crate::ui::command::backtrace::Handler as BacktraceHandler; use crate::ui::command::frame::ExecutionResult as FrameResult; use crate::ui::command::frame::Handler as FrameHandler; use crate::ui::command::memory::Handler as MemoryHandler; +use crate::ui::command::r#async::Command as AsyncCommand; use crate::ui::command::r#break::ExecutionResult; use crate::ui::command::r#break::Handler as BreakpointHandler; use crate::ui::command::r#continue::Handler as ContinueHandler; @@ -32,27 +53,14 @@ use crate::ui::console::print::style::{ KeywordView, }; use crate::ui::console::print::ExternalPrinter; +use crate::ui::console::r#async::print_backtrace; +use crate::ui::console::r#async::print_backtrace_full; +use crate::ui::console::r#async::print_task_ex; use crate::ui::console::variable::render_variable; use crate::ui::DebugeeOutReader; use crate::ui::{command, supervisor}; -use crossterm::style::{Color, Stylize}; -use debugger::Error; -use nix::sys::signal::{kill, Signal}; -use nix::unistd::Pid; -use r#break::Command as BreakpointCommand; -use rustyline::error::ReadlineError; -use rustyline::history::MemHistory; -use rustyline::Editor; -use std::io::{BufRead, BufReader}; -use std::process::exit; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; -use std::sync::mpsc::{Receiver, SyncSender}; -use std::sync::{mpsc, Arc, Mutex, Once}; -use std::thread; -use std::time::Duration; -use timeout_readwrite::TimeoutReader; +mod r#async; mod editor; pub mod file; mod help; @@ -716,6 +724,22 @@ impl AppLoop { } } }, + Command::Async(cmd) => { + let mut handler = command::r#async::Handler::new(&mut self.debugger); + let mut backtrace = handler.handle(&cmd)?; + + match cmd { + AsyncCommand::ShortBacktrace => { + print_backtrace(&mut backtrace, &self.printer); + } + AsyncCommand::FullBacktrace => { + print_backtrace_full(&mut backtrace, &self.printer); + } + AsyncCommand::CurrentTask(regex) => { + print_task_ex(&backtrace, &self.printer, regex.as_deref()); + } + } + } Command::Oracle(name, subcmd) => match self.debugger.get_oracle(&name) { None => self .printer diff --git a/src/ui/console/print.rs b/src/ui/console/print.rs index c1db9a6..5565315 100644 --- a/src/ui/console/print.rs +++ b/src/ui/console/print.rs @@ -114,4 +114,8 @@ pub mod style { view_struct!(AsmInstructionView, Color::DarkRed); view_struct!(AsmOperandsView, Color::DarkGreen); view_struct!(ErrorView, Color::DarkRed); + + view_struct!(AsyncTaskView, Color::Green); + view_struct!(FutureFunctionView, Color::Yellow); + view_struct!(FutureTypeView, Color::Magenta); } diff --git a/src/version.rs b/src/version.rs index 38bbfcd..b63b5a3 100644 --- a/src/version.rs +++ b/src/version.rs @@ -5,7 +5,7 @@ use once_cell::sync; use regex::Regex; /// Compiler SemVer version. -#[derive(PartialEq, PartialOrd)] +#[derive(PartialEq, PartialOrd, Debug)] pub struct Version(pub (u32, u32, u32)); impl Version { diff --git a/tests/debugger/main.rs b/tests/debugger/main.rs index b05c416..1a13782 100644 --- a/tests/debugger/main.rs +++ b/tests/debugger/main.rs @@ -6,6 +6,7 @@ mod multithreaded; mod signal; mod steps; mod symbol; +mod tokio; mod variables; mod watchpoint; @@ -47,6 +48,7 @@ const SHARED_LIB_APP: &str = "./examples/target/debug/calc_bin"; const SLEEPER_APP: &str = "./examples/target/debug/sleeper"; const FIZZBUZZ_APP: &str = "./examples/target/debug/fizzbuzz"; const CALCULATIONS_APP: &str = "./examples/target/debug/calculations"; +const TOKIO_TICKER_APP: &str = "./examples/target/debug/tokioticker"; #[test] #[serial] diff --git a/tests/debugger/tokio.rs b/tests/debugger/tokio.rs new file mode 100644 index 0000000..30bba2b --- /dev/null +++ b/tests/debugger/tokio.rs @@ -0,0 +1,21 @@ +use crate::common::TestHooks; +use crate::{prepare_debugee_process, TOKIO_TICKER_APP}; +use bugstalker::debugger::DebuggerBuilder; +use serial_test::serial; + +#[test] +#[serial] +fn test_async0() { + let process = prepare_debugee_process(TOKIO_TICKER_APP, &[]); + let builder = DebuggerBuilder::new().with_hooks(TestHooks::default()); + let mut debugger = builder.build(process).unwrap(); + + debugger.set_breakpoint_at_line("main.rs", 6).unwrap(); + debugger.start_debugee().unwrap(); + + let async_bt = debugger.async_backtrace().unwrap(); + assert!(!async_bt.workers.is_empty()); + assert_eq!(async_bt.block_threads.len(), 0); + assert!(async_bt.workers.iter().any(|w| w.active_task.is_some())); + assert!(!async_bt.tasks.is_empty()); +} diff --git a/tests/debugger/variables.rs b/tests/debugger/variables.rs index 1556714..d02ecdd 100644 --- a/tests/debugger/variables.rs +++ b/tests/debugger/variables.rs @@ -2536,7 +2536,7 @@ fn test_read_static_in_fn_variable() { // brkpt in function where static is declared debugger.set_breakpoint_at_line("vars.rs", 504).unwrap(); // brkpt outside function where static is declared - debugger.set_breakpoint_at_line("vars.rs", 570).unwrap(); + debugger.set_breakpoint_at_line("vars.rs", 577).unwrap(); debugger.start_debugee().unwrap(); assert_eq!(info.line.take(), Some(504)); @@ -2546,7 +2546,7 @@ fn test_read_static_in_fn_variable() { assert_scalar(inner_static.value(), "u32", Some(SupportedScalar::U32(1))); debugger.continue_debugee().unwrap(); - assert_eq!(info.line.take(), Some(570)); + assert_eq!(info.line.take(), Some(577)); read_var_dqe!(debugger, Dqe::Variable(Selector::by_name("INNER_STATIC", false)) => inner_static); assert_idents!(inner_static => "vars::inner_static::INNER_STATIC"); diff --git a/tests/integration/test_async.py b/tests/integration/test_async.py new file mode 100644 index 0000000..52047cd --- /dev/null +++ b/tests/integration/test_async.py @@ -0,0 +1,80 @@ +import unittest +from helper import Debugger +import socket +import threading +import time +import signal + + +def send_tcp_request(): + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect(("localhost", 8080)) + client.send("hello, bs!".encode()) + client.close() + + +class CommandTestCase(unittest.TestCase): + def setUp(self): + self.debugger = Debugger(path='./examples/target/debug/tokio_tcp') + + def test_runtime_info_1(self): + """Stop async runtime and assert futures state""" + self.debugger.cmd_re('run', r'Listening on: .*:8080') + + thread = threading.Thread(target=send_tcp_request) + thread.start() + time.sleep(7) + + self.debugger.debugee_process().send_signal(signal.SIGINT) + self.debugger.cmd_re( + 'async backtrace', + r'Thread .* block on:', + 'async fn tokio_tcp::main', + 'Async worker', + 'Async worker', + 'Async worker' + ) + self.debugger.cmd_re( + 'async backtrace all', + r'Thread .* block on:', + 'async fn tokio_tcp::main', + 'Async worker', + 'Async worker', + 'Async worker', + '#0 async fn tokio_tcp::main::{async_block#0}', + 'suspended at await point 2', + '#1 future tokio::sync::oneshot::Receiver', + '#0 async fn tokio_tcp::main::{async_block#0}::{async_block#1}', + 'suspended at await point 0', + '#1 sleep future, sleeping', + ) + # switch to worker thread (hope that thread 2 is a worker) + self.debugger.cmd('thread switch 2') + self.debugger.cmd('async task', 'no active task found for current worker') + + # there should be two task with "main" in their names + self.debugger.cmd('async task .*main.*', 'Task id', 'Task id') + + def test_runtime_info_2(self): + """Stop async runtime at breakpoint and assert futures state""" + self.debugger.cmd('break main.rs:54') + self.debugger.cmd_re('run', r'Listening on: .*:8080') + + thread = threading.Thread(target=send_tcp_request) + thread.start() + time.sleep(6) + + self.debugger.cmd_re( + 'async backtrace', + 'Thread .* block on', + '#0 async fn tokio_tcp::main', + 'Async worker', + 'Active task', + '#0 async fn tokio_tcp::main::{async_block#0}' + ) + self.debugger.cmd( + 'async task', + '#0 async fn tokio_tcp::main::{async_block#0}', + 'suspended at await point 1', + '#1 sleep future, sleeping' + )