Skip to content

Commit

Permalink
feature: introduce support for different tokio
Browse files Browse the repository at this point in the history
library versions
  • Loading branch information
godzie44 committed Nov 17, 2024
1 parent e2577e8 commit a5da053
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 62 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ build-test:
cargo build --features "int_test"

build-examples:
cd examples ; cargo build -p calc_lib ; cargo build
cd examples; \
cargo build -p calc_lib; \
$(SHLIB_SO_PATH) cargo build; \
cargo build -p tokio_tcp --bin tokio_tcp_1_40 --no-default-features --features tokio_v_1_40; \
cargo build -p tokio_tcp --bin tokio_tcp_1_41 --no-default-features --features tokio_v_1_41

build-all: build build-examples

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ supported).
There is also `oracle tokio`, but it adds some overhead to your program and
is not very informative unlike the commands presented below.

Supported `tokio` versions:

- 1.40
- 1.41

### Async backtrace

[demo async backtrace](https://github.com/godzie44/BugStalker/blob/master/doc/demo_async_bt.gif)
Expand Down
4 changes: 2 additions & 2 deletions examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion examples/tokio_tcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ workspace = "./.."
publish = false

[dependencies]
tokio = { version = "1.40.0", features = ["full"] }
tokio_1_40 = { package = "tokio", version = "1.40.0", optional = true, features = [
"full",
] }
tokio_1_41 = { package = "tokio", version = "1.41.0", optional = true, features = [
"full",
] }
rand = "0.8"
log = "0.4.20"

[features]
tokio_v_1_40 = ["tokio_1_40"]
tokio_v_1_41 = ["tokio_1_41"]
default = ["tokio_v_1_40"]

[[bin]]
name = "tokio_tcp_1_40"
path = "src/main.rs"

[[bin]]
name = "tokio_tcp_1_41"
path = "src/main.rs"
5 changes: 5 additions & 0 deletions examples/tokio_tcp/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(feature = "tokio_v_1_40")]
use tokio_1_40 as tokio;
#[cfg(feature = "tokio_v_1_41")]
use tokio_1_41 as tokio;

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

Expand Down
10 changes: 8 additions & 2 deletions src/debugger/async/context.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use crate::debugger::Debugger;

use super::tokio::TokioVersion;

pub struct TokioAnalyzeContext<'a> {
debugger: &'a mut Debugger,
_tokio_version: TokioVersion,
}

impl<'a> TokioAnalyzeContext<'a> {
pub fn new(debugger: &'a mut Debugger) -> Self {
Self { debugger }
pub fn new(debugger: &'a mut Debugger, tokio_version: TokioVersion) -> Self {
Self {
debugger,
_tokio_version: tokio_version,
}
}

pub fn debugger_mut(&mut self) -> &mut Debugger {
Expand Down
4 changes: 3 additions & 1 deletion src/debugger/async/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod context;
mod future;
mod park;
pub mod tokio;
mod types;
mod worker;

Expand Down Expand Up @@ -276,12 +277,13 @@ fn task_from_header<'a>(

impl Debugger {
pub fn async_backtrace(&mut self) -> Result<AsyncBacktrace, Error> {
let tokio_version = self.debugee.tokio_version();
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 analyze_context = TokioAnalyzeContext::new(self, tokio_version.unwrap_or_default());
let mut backtrace = AsyncBacktrace {
workers: vec![],
block_threads: vec![],
Expand Down
32 changes: 32 additions & 0 deletions src/debugger/async/tokio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use core::str;

use crate::{version::Version, version_specialized};

version_specialized!(TokioVersion, "Tokio SemVer version");

impl Default for TokioVersion {
fn default() -> Self {
// the first supported version is default
TokioVersion(Version((1, 40, 0)))
}
}

/// Temporary function, parse tokio version from static string found in `rodata`` section.
///
/// WAITFORFIX: https://github.com/tokio-rs/tokio/issues/6950
pub fn extract_version_naive(rodata: &[u8]) -> Option<TokioVersion> {
const TOKIO_V_TPL: &str = "tokio-1.";

let tpl = TOKIO_V_TPL.as_bytes();
let pos = rodata.windows(tpl.len()).position(|w| w == tpl)?;

// get next number between dots
let mut pos = pos + tpl.len();
let mut minor = vec![];
while rodata[pos] != b'.' || pos >= rodata.len() {
minor.push(rodata[pos]);
pos += 1;
}
let minor = str::from_utf8(&minor).ok()?;
Some(TokioVersion(Version((1, minor.parse().ok()?, 0))))
}
19 changes: 19 additions & 0 deletions src/debugger/debugee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod tracer;
pub use registry::RegionInfo;
pub use rendezvous::RendezvousError;

use super::r#async::tokio::{self, TokioVersion};
use crate::debugger::address::{GlobalAddress, RelocatedAddress};
use crate::debugger::breakpoint::{Breakpoint, BrkptType};
use crate::debugger::debugee::disasm::Disassembler;
Expand Down Expand Up @@ -120,6 +121,8 @@ pub struct Debugee {
disassembly: Disassembler,
/// Loaded libthread_db.
libthread_db: Arc<thread_db::Lib>,
/// Version of tokio runtime, if exist.
tokio_version: Option<TokioVersion>,
}

impl Debugee {
Expand All @@ -140,6 +143,11 @@ impl Debugee {
);
parse_dependencies_into_registry(&mut registry, deps.unwrap_or_default().into_iter(), true);

let tokio_ver: Option<TokioVersion> = object
.section_by_name(".rodata")
.and_then(|sect| sect.data().ok())
.and_then(tokio::extract_version_naive);

Ok(Self {
execution_status: ExecutionStatus::Unload,
path: path.into(),
Expand All @@ -152,6 +160,7 @@ impl Debugee {
dwarf_registry: registry,
disassembly: Disassembler::new()?,
libthread_db: Arc::new(thread_db::Lib::try_load()?),
tokio_version: tokio_ver,
})
}

Expand All @@ -175,6 +184,10 @@ impl Debugee {
.sections()
.filter_map(|section| Some((section.name().ok()?.to_string(), section.address())))
.collect();
let tokio_ver: Option<TokioVersion> = object
.section_by_name(".rodata")
.and_then(|sect| sect.data().ok())
.and_then(tokio::extract_version_naive);

let mut debugee = Self {
execution_status: ExecutionStatus::InProgress,
Expand All @@ -195,6 +208,7 @@ impl Debugee {
dwarf_registry: registry,
disassembly: Disassembler::new()?,
libthread_db: Arc::new(thread_db::Lib::try_load()?),
tokio_version: tokio_ver,
};

debugee.attach_libthread_db();
Expand All @@ -218,9 +232,14 @@ impl Debugee {
dwarf_registry: self.dwarf_registry.extend(proc),
disassembly: Disassembler::new().expect("infallible"),
libthread_db: self.libthread_db.clone(),
tokio_version: self.tokio_version,
}
}

pub fn tokio_version(&self) -> Option<TokioVersion> {
self.tokio_version
}

pub fn execution_status(&self) -> ExecutionStatus {
self.execution_status
}
Expand Down
122 changes: 67 additions & 55 deletions tests/integration/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,80 @@ def send_tcp_request():
client.close()


class CommandTestCase(unittest.TestCase):
def setUp(self):
self.debugger = Debugger(path='./examples/target/debug/tokio_tcp')
SUPPORTED_TOKIO_V = [
"1_40",
"1_41",
];

def tokio_binaries():
binaries = []
for v in SUPPORTED_TOKIO_V:
binaries.append(f"tokio_tcp_{v}")
return binaries

class CommandTestCase(unittest.TestCase):
def test_runtime_info_1(self):
"""Stop async runtime and assert futures state"""
self.debugger.cmd_re('run', r'Listening on: .*:8080')
for binary in tokio_binaries():
self.debugger = Debugger(path=f"./examples/target/debug/{binary}")
self.debugger.cmd_re('run', r'Listening on: .*:8080')

thread = threading.Thread(target=send_tcp_request)
thread.start()
time.sleep(7)
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<i32>',
'#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')
self.debugger.debugee_process().send_signal(signal.SIGINT)
self.debugger.cmd_re(
'async backtrace',
r'Thread .* block on:',
f'async fn {binary}::main',
'Async worker',
'Async worker',
'Async worker'
)
self.debugger.cmd_re(
'async backtrace all',
r'Thread .* block on:',
f'async fn {binary}::main',
'Async worker',
'Async worker',
'Async worker',
f'#0 async fn {binary}::main::{{async_block#0}}',
'suspended at await point 2',
'#1 future tokio::sync::oneshot::Receiver<i32>',
f'#0 async fn {binary}::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')
# 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')

for binary in tokio_binaries():
self.debugger = Debugger(path=f"./examples/target/debug/{binary}")
self.debugger.cmd('break main.rs:59')
self.debugger.cmd_re('run', r'Listening on: .*:8080')

thread = threading.Thread(target=send_tcp_request)
thread.start()
time.sleep(6)
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'
)
self.debugger.cmd_re(
'async backtrace',
'Thread .* block on',
f'#0 async fn {binary}::main',
'Async worker',
'Active task',
f'#0 async fn {binary}::main::{{async_block#0}}'
)
self.debugger.cmd(
'async task',
f'#0 async fn {binary}::main::{{async_block#0}}',
'suspended at await point 1',
'#1 sleep future, sleeping'
)

0 comments on commit a5da053

Please sign in to comment.