From 5626c716cca4be77febe86180d43f07af48e49e4 Mon Sep 17 00:00:00 2001 From: fluxie Date: Mon, 15 Jan 2024 14:15:38 +0200 Subject: [PATCH] Implemented callstack capture flow for a unit test This adds parts of the infrastructure required to display the callstack in the ui. And adds a test to verify the basic capturing concept works. The implementation requires utilizing rstack_self crate to the capture which in turn required helper crates to wrap its functionality for ease of use. --- .github/workflows/ci.yml | 9 + .gitignore | 2 +- .idea/proxide.iml | 4 + Cargo.lock | 120 ++++++++++++ Cargo.toml | 3 + src/connection/http2.rs | 125 +++++++++++-- src/main.rs | 36 +++- src/session.rs | 17 ++ src/session/callstack.rs | 128 +++++++++++++ src/ui/views/callstack_view.rs | 31 +++- test/rstack-child/Cargo.lock | 260 ++++++++++++++++++++++++++ test/rstack-child/Cargo.toml | 9 + test/rstack-child/src/main.rs | 5 + test/rstack-launcher/Cargo.lock | 312 ++++++++++++++++++++++++++++++++ test/rstack-launcher/Cargo.toml | 13 ++ test/rstack-launcher/build.rs | 33 ++++ test/rstack-launcher/src/lib.rs | 80 ++++++++ 17 files changed, 1165 insertions(+), 22 deletions(-) create mode 100644 src/session/callstack.rs create mode 100644 test/rstack-child/Cargo.lock create mode 100644 test/rstack-child/Cargo.toml create mode 100644 test/rstack-child/src/main.rs create mode 100644 test/rstack-launcher/Cargo.lock create mode 100644 test/rstack-launcher/Cargo.toml create mode 100644 test/rstack-launcher/build.rs create mode 100644 test/rstack-launcher/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6f2300..d277111 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,11 @@ jobs: - name: Install Protoc uses: arduino/setup-protoc@v2 + - name: Install libunwind-ptrace + uses: ConorMacBride/install-package@v1 + with: + apt: libunwind-ptrace + - name: Environment run: | cargo --version @@ -69,6 +74,10 @@ jobs: - name: Install Protoc uses: arduino/setup-protoc@v2 + - name: Install libunwind-ptrace + run: | + sudo apt update && sudo apt install -y libunwind-ptrace + - name: Environment run: | cargo --version diff --git a/.gitignore b/.gitignore index 7d4d3e2..324ecc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +**/target **/*.rs.bk .vs packages diff --git a/.idea/proxide.iml b/.idea/proxide.iml index 1651999..9828d78 100644 --- a/.idea/proxide.iml +++ b/.idea/proxide.iml @@ -4,8 +4,12 @@ + + + + diff --git a/Cargo.lock b/Cargo.lock index 5a1c41f..fe059ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" + [[package]] name = "anyhow" version = "1.0.75" @@ -229,6 +235,15 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -557,6 +572,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "escargot" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768064bd3a0e2bedcba91dc87ace90beea91acc41b6a01a3ca8e9aa8827461bf" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -575,6 +602,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.40", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "futures" version = "0.3.29" @@ -1248,6 +1302,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + [[package]] name = "portpicker" version = "0.1.1" @@ -1371,6 +1431,9 @@ dependencies = [ "protofish", "rcgen", "rmp-serde", + "rstack", + "rstack-launcher", + "rstack-self", "rustls", "serde", "serde_json", @@ -1526,6 +1589,42 @@ dependencies = [ "serde", ] +[[package]] +name = "rstack" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7df9d3ebd4f17b52e6134efe2fa20021c80688cbe823d481a729a993b730493" +dependencies = [ + "cfg-if", + "libc", + "log", + "unwind", +] + +[[package]] +name = "rstack-launcher" +version = "0.1.0" +dependencies = [ + "escargot", + "os-id", + "rstack-self", +] + +[[package]] +name = "rstack-self" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd5030da3aba0ec731502f74ec38e63798eea6bc8b8ba5972129afe3eababd2" +dependencies = [ + "antidote", + "backtrace", + "bincode", + "lazy_static", + "libc", + "rstack", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2108,6 +2207,27 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unwind" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38290439f8459ba56c4bf15fc776463f495fefc4f0112f87a1a075540441b083" +dependencies = [ + "foreign-types", + "libc", + "unwind-sys", +] + +[[package]] +name = "unwind-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a81ba64bc45243d442e9bb2a362f303df152b5078c56ce4a0dc7d813c8df91" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index df08256..d6949ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,12 +40,15 @@ base64 = "0.11" wildmatch = "1" glob = "0.3" shell-words = "1" +rstack = "0.3.3" [dev-dependencies] portpicker = "0.1.1" grpc-tester = { version = "0.1.0", path = "test/rust_grpc"} serial_test = "2.0.0" lazy_static = "1.4.0" +rstack-launcher = { version = "0.1.0", path = "test/rstack-launcher" } +rstack-self = "0.3.0" [profile.release] debug = true \ No newline at end of file diff --git a/src/connection/http2.rs b/src/connection/http2.rs index c5a1c5f..543f675 100644 --- a/src/connection/http2.rs +++ b/src/connection/http2.rs @@ -15,6 +15,7 @@ use std::sync::mpsc::Sender; use std::task::{Context, Poll}; use std::time::SystemTime; use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::sync::{Semaphore, SemaphorePermit, TryAcquireError}; use tokio::task::{JoinHandle, JoinSet}; use uuid::Uuid; @@ -91,6 +92,7 @@ where // The client_connection will produce individual HTTP request that we'll accept. // These requests will be handled in parallel by spawning them into their own // tasks. + let processing_control = ProcessingControl::new(); while let Some(request) = client_connection.accept().await { let (client_request, client_response) = request.context(H2Error {}).context(ClientError { @@ -104,6 +106,7 @@ where client_request, client_response, server_stream, + processing_control.clone(), &ui, )?; @@ -148,6 +151,12 @@ pub struct ProxyRequest request_processor: ProcessingFuture, } +/// Manages the asynchronous auxiliary processing of requests. +struct ProcessingControl +{ + callstack_capture_limiter: Semaphore, +} + struct ProcessingFuture { inner: JoinHandle<()>, @@ -155,12 +164,13 @@ struct ProcessingFuture impl ProxyRequest { - pub fn new( + fn new( connection_uuid: Uuid, authority: Option, client_request: Request, client_response: SendResponse, server_stream: &mut client::SendRequest, + processing_control: Arc, ui: &Sender, ) -> Result { @@ -203,7 +213,7 @@ impl ProxyRequest // Request processor supports asynchronous message processing while the proxide is busy proxying data between // the client and the server. - let request_processor = ProcessingFuture::spawn(uuid, &client_head, ui); + let request_processor = ProcessingFuture::spawn(uuid, &client_head, processing_control, ui); let server_request = Request::from_parts(client_head, ()); @@ -464,7 +474,12 @@ fn is_fatal_error(r: &Result) -> bool impl ProcessingFuture { - fn spawn(uuid: Uuid, client_head: &http::request::Parts, ui: &Sender) -> Self + fn spawn( + uuid: Uuid, + client_head: &http::request::Parts, + processing_control: Arc, + ui: &Sender, + ) -> Self { let mut tasks: JoinSet>> = JoinSet::new(); @@ -473,7 +488,10 @@ impl ProcessingFuture if let Ok(thread_id) = crate::connection::ClientThreadId::try_from(&client_head.headers) { let ui_clone = ui.clone(); tasks.spawn(ProcessingFuture::capture_client_callstack( - uuid, thread_id, ui_clone, + uuid, + thread_id, + processing_control.clone(), + ui_clone, )); } @@ -495,18 +513,33 @@ impl ProcessingFuture async fn capture_client_callstack( uuid: Uuid, - _client_thread_id: ClientThreadId, + client_thread_id: ClientThreadId, + processing_control: Arc, ui: Sender, ) -> std::result::Result<(), Box> { - // TODO: Try to capture the callstack - ui.send(SessionEvent::ClientCallstackProcessed( - ClientCallstackProcessedEvent { - uuid, - callstack: ClientCallstack::Unsupported, - }, - ))?; - Ok(()) + // Capturing the callstacks is a very expensive operation + // Capturing is throttled with the semaphore in processing_control + match processing_control.try_request_capture_callstack_permit() { + Ok(_) => { + // TODO Callstack capture: Add support for other operating systems. + if cfg!(target_os = "linux") { + Ok(capture_client_callstack_rstack(uuid, client_thread_id, ui).await?) + } else { + Ok(capture_client_callstack_unsupported(uuid, client_thread_id, ui).await?) + } + } + Err(TryAcquireError::NoPermits) => { + ui.send(SessionEvent::ClientCallstackProcessed( + ClientCallstackProcessedEvent { + uuid, + callstack: ClientCallstack::Throttled, + }, + ))?; + Ok(()) + } + Err(e) => Err(Box::from(e)), + } } } @@ -522,3 +555,69 @@ impl Future for ProcessingFuture } } } + +impl ProcessingControl +{ + fn new() -> Arc + { + let parallel_callstack_capture_limit = if cfg!(not(test)) { 5 } else { 1 }; + Arc::new(Self { + callstack_capture_limiter: Semaphore::new(parallel_callstack_capture_limit), + }) + } + + /// Requests permissions to capture a cleint callstack. + fn try_request_capture_callstack_permit(&self) -> Result, TryAcquireError> + { + self.callstack_capture_limiter.try_acquire() + } +} + +#[cfg(target_os = "linux")] +async fn capture_client_callstack_rstack( + uuid: Uuid, + client_thread_id: ClientThreadId, + ui: Sender, +) -> std::result::Result<(), Box> +{ + if client_thread_id.process_id != std::process::id() { + capture_client_callstack_unsupported(uuid, client_thread_id, ui).await + } else { + // The caller requested trace from the process itself. + // This should only happen in unit tests. + // Process cannot capture callstack from itself which is why the operation is delegated to rstack_launcher + // helper library available in tests. + + #[cfg(test)] + { + let thread = rstack_launcher::capture_self(client_thread_id.thread_id)?; + ui.send(SessionEvent::ClientCallstackProcessed( + ClientCallstackProcessedEvent { + uuid, + callstack: ClientCallstack::Callstack(callstack::Thread::from(&thread)), + }, + ))?; + return Ok(()); + } + + #[cfg(not(test))] + { + return Ok(capture_client_callstack_unsupported(uuid, client_thread_id, ui).await?); + } + } +} + +async fn capture_client_callstack_unsupported( + uuid: Uuid, + _client_thread_id: ClientThreadId, + ui: Sender, +) -> std::result::Result<(), Box> +{ + ui.send(SessionEvent::ClientCallstackProcessed( + ClientCallstackProcessedEvent { + uuid, + callstack: ClientCallstack::Unsupported, + }, + ))?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index c373346..e031d40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use crossterm::{ }; use log::error; use snafu::{ResultExt, Snafu}; +use std::env; use std::fs::File; use std::io::stdout; use std::io::Read; @@ -16,6 +17,7 @@ use std::net::SocketAddr; use std::path::Path; use std::sync::mpsc::Sender; use std::sync::Arc; +use std::time::Duration; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::oneshot; @@ -89,6 +91,13 @@ pub struct ProxyFilter fn main() { + std::thread::sleep(Duration::from_secs(60)); + + // Launched to capture stack? + if env::args_os().len() == 2 && env::args_os().any(|p| p == "child") { + return; + } + match proxide_main() { Ok(_) => (), Err(e) => { @@ -430,6 +439,7 @@ mod test use tokio::time::Instant; use crate::session::events::SessionEvent; + use crate::session::ClientCallstack; use crate::ConnectionOptions; lazy_static! { @@ -570,15 +580,22 @@ mod test // UI channel should be constantly receiving client callstack events. // The generator includes the process id and the thread id in the messages it sends. - let mut client_callstack_received = false; + let mut client_callstack_received: Option = None; let timeout_at = Instant::now().add(Duration::from_secs(30)); while let Some(message) = tokio::select! { result = message_rx.recv() => result, _t = tokio::time::sleep( Duration::from_secs( 30 ) ) => panic!( "Timeout" ), error = error_monitor.recv() => panic!( "{:?}", error ), } { - if let SessionEvent::ClientCallstackProcessed(..) = message { - client_callstack_received = true; + // Try to collect a valid callstack. + // The capture process has a throttling mechanism which may skip some captures. + if let SessionEvent::ClientCallstackProcessed(event) = message { + match event.callstack { + ClientCallstack::Callstack(thread) => client_callstack_received = Some(thread), + ClientCallstack::Throttled => {} + ClientCallstack::Unsupported => break, + ClientCallstack::Error(error) => panic!("{:?}", error), + } break; } else if Instant::now() > timeout_at { panic!("Timeout") @@ -586,7 +603,18 @@ mod test } // Ensure the ui channel was not closed prematurely. - assert!(client_callstack_received); + #[cfg(target_os = "linux")] + { + let client_callstack_received = + client_callstack_received.expect("Client callstack unavailable."); + assert_eq!(client_callstack_received.name(), "grpc-generator"); + } + + // Verify callstack 1with tne new supported OS as well. + #[cfg(not(target_os = "linux"))] + { + assert!(client_callstack_received.is_none()); + } let mut server = tester.stop_generator().expect("Stopping generator failed."); abort_tx.send(()).expect("Stopping proxide failed."); diff --git a/src/session.rs b/src/session.rs index 971a2ed..09da084 100644 --- a/src/session.rs +++ b/src/session.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::net::SocketAddr; use uuid::Uuid; +pub mod callstack; pub mod events; pub mod serialization; @@ -132,6 +133,22 @@ pub enum ClientCallstack { /// Proxide does not support callstack capture on the current platform/operating system. Unsupported, + + /// The maximum number of parallel callstack captures was reached. + Throttled, + + /// Captured client thread with its callstack. + Callstack(crate::session::callstack::Thread), + + /// An error occurred during the capture process. + Error(ClientCallstackError), +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum ClientCallstackError +{ + /// An internal error to proxide occurred while capturing or processing the callstack. + Internal(String), } impl IndexedVec diff --git a/src/session/callstack.rs b/src/session/callstack.rs new file mode 100644 index 0000000..d9ba88c --- /dev/null +++ b/src/session/callstack.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; + +/// UI visualization types for callstack captures. + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Thread +{ + /// Identity of the thread. + id: i64, + + /// Name of the thread. + name: String, + + /// Captured stack frames of the thread. + frames: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Frame +{ + symbols: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Symbol +{ + name: String, +} + +impl Thread +{ + pub fn id(&self) -> i64 + { + self.id + } + + pub fn name(&self) -> &str + { + &self.name + } + + pub fn frames(&self) -> &[Frame] + { + &self.frames + } +} + +impl Frame +{ + pub fn symbols(&self) -> &[Symbol] + { + &self.symbols + } +} + +impl Symbol +{ + pub fn name(&self) -> &str + { + &self.name + } +} + +impl From<&rstack::Thread> for Thread +{ + fn from(value: &rstack::Thread) -> Self + { + Self { + id: value.id() as i64, + name: value.name().unwrap_or("").to_string(), + frames: value.frames().iter().map(|f| Frame::from(f)).collect(), + } + } +} + +impl From<&rstack::Frame> for Frame +{ + fn from(value: &rstack::Frame) -> Self + { + Frame { + symbols: value.symbol().iter().map(|s| Symbol::from(*s)).collect(), + } + } +} + +impl From<&rstack::Symbol> for Symbol +{ + fn from(value: &rstack::Symbol) -> Self + { + Self { + name: value.name().to_string(), + } + } +} + +#[cfg(test)] +impl From<&rstack_self::Thread> for Thread +{ + fn from(value: &rstack_self::Thread) -> Self + { + Self { + id: value.id() as i64, + name: value.name().to_string(), + frames: value.frames().iter().map(|f| Frame::from(f)).collect(), + } + } +} + +#[cfg(test)] +impl From<&rstack_self::Frame> for Frame +{ + fn from(value: &rstack_self::Frame) -> Self + { + Self { + symbols: value.symbols().iter().map(|s| Symbol::from(s)).collect(), + } + } +} +#[cfg(test)] +impl From<&rstack_self::Symbol> for Symbol +{ + fn from(value: &rstack_self::Symbol) -> Self + { + Symbol { + name: value.name().expect("Name missing").to_string(), + } + } +} diff --git a/src/ui/views/callstack_view.rs b/src/ui/views/callstack_view.rs index ed843a9..cfff488 100644 --- a/src/ui/views/callstack_view.rs +++ b/src/ui/views/callstack_view.rs @@ -33,11 +33,18 @@ impl View for CallstackView client_thread.process_id(), client_thread.thread_id() ); - let message = match request.request_data.client_callstack { + let message: String = match &request.request_data.client_callstack { Some(ClientCallstack::Unsupported) => { - "Callstack unavailable:\n* Unsupported operating system." - } - None => ".. (Pending)", + "Callstack unavailable:\n* Unsupported operating system.".to_string() + }, + Some(ClientCallstack::Throttled) => { + "Callstack unavailable:\n* The maximum number of parallel callstack capture operations was reached.".to_string() + }, + Some(ClientCallstack::Callstack( thread)) => message_from_thread( thread ), + Some(ClientCallstack::Error(error)) => { + format!("{:?}", error) + }, + None => ".. (Pending)".to_string(), }; let block = create_block(&title); let request_data = Paragraph::new(message) @@ -86,3 +93,19 @@ impl View for CallstackView ) } } + +fn message_from_thread(thread: &crate::session::callstack::Thread) -> String +{ + let title = format!("{} ({})", thread.name(), thread.id()); + let callstack = thread + .frames() + .iter() + .flat_map(|f| f.symbols()) + .map(|s| s.name()) + .fold(String::default(), |mut acc, name| { + acc.push_str(name); + acc.push('\n'); + acc + }); + format!("{}\n\n{}", title, callstack) +} diff --git a/test/rstack-child/Cargo.lock b/test/rstack-child/Cargo.lock new file mode 100644 index 0000000..6d855fd --- /dev/null +++ b/test/rstack-child/Cargo.lock @@ -0,0 +1,260 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "proc-macro2" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rstack" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7df9d3ebd4f17b52e6134efe2fa20021c80688cbe823d481a729a993b730493" +dependencies = [ + "cfg-if", + "libc", + "log", + "unwind", +] + +[[package]] +name = "rstack-child" +version = "0.1.0" +dependencies = [ + "rstack-self", +] + +[[package]] +name = "rstack-self" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd5030da3aba0ec731502f74ec38e63798eea6bc8b8ba5972129afe3eababd2" +dependencies = [ + "antidote", + "backtrace", + "bincode", + "lazy_static", + "libc", + "rstack", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unwind" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38290439f8459ba56c4bf15fc776463f495fefc4f0112f87a1a075540441b083" +dependencies = [ + "foreign-types", + "libc", + "unwind-sys", +] + +[[package]] +name = "unwind-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a81ba64bc45243d442e9bb2a362f303df152b5078c56ce4a0dc7d813c8df91" +dependencies = [ + "libc", + "pkg-config", +] diff --git a/test/rstack-child/Cargo.toml b/test/rstack-child/Cargo.toml new file mode 100644 index 0000000..393e9b2 --- /dev/null +++ b/test/rstack-child/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rstack-child" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rstack-self = "0.3.0" \ No newline at end of file diff --git a/test/rstack-child/src/main.rs b/test/rstack-child/src/main.rs new file mode 100644 index 0000000..8a49be1 --- /dev/null +++ b/test/rstack-child/src/main.rs @@ -0,0 +1,5 @@ +fn main() { + let err = rstack_self::child(); + eprintln!("{:?}", err); + err.expect("Capturing callstack with rstack-self failed."); +} diff --git a/test/rstack-launcher/Cargo.lock b/test/rstack-launcher/Cargo.lock new file mode 100644 index 0000000..6088009 --- /dev/null +++ b/test/rstack-launcher/Cargo.lock @@ -0,0 +1,312 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "escargot" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768064bd3a0e2bedcba91dc87ace90beea91acc41b6a01a3ca8e9aa8827461bf" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "os-id" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510856ec55c552d86db0d675df95c32b87f28cfe1cdc47d3eba2342c39a0a5f6" +dependencies = [ + "libc", +] + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "proc-macro2" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rstack" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7df9d3ebd4f17b52e6134efe2fa20021c80688cbe823d481a729a993b730493" +dependencies = [ + "cfg-if", + "libc", + "log", + "unwind", +] + +[[package]] +name = "rstack-launcher" +version = "0.1.0" +dependencies = [ + "escargot", + "os-id", + "rstack-self", +] + +[[package]] +name = "rstack-self" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd5030da3aba0ec731502f74ec38e63798eea6bc8b8ba5972129afe3eababd2" +dependencies = [ + "antidote", + "backtrace", + "bincode", + "lazy_static", + "libc", + "rstack", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unwind" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38290439f8459ba56c4bf15fc776463f495fefc4f0112f87a1a075540441b083" +dependencies = [ + "foreign-types", + "libc", + "unwind-sys", +] + +[[package]] +name = "unwind-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a81ba64bc45243d442e9bb2a362f303df152b5078c56ce4a0dc7d813c8df91" +dependencies = [ + "libc", + "pkg-config", +] diff --git a/test/rstack-launcher/Cargo.toml b/test/rstack-launcher/Cargo.toml new file mode 100644 index 0000000..1a0f0ee --- /dev/null +++ b/test/rstack-launcher/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rstack-launcher" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rstack-self = "0.3.0" +os-id = "3.0.1" + +[build-dependencies] +escargot = "0.5.8" diff --git a/test/rstack-launcher/build.rs b/test/rstack-launcher/build.rs new file mode 100644 index 0000000..12c24ea --- /dev/null +++ b/test/rstack-launcher/build.rs @@ -0,0 +1,33 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +fn main() +{ + let out_dir = std::env::var("OUT_DIR").expect("Output directory unavailable."); + let run = escargot::CargoBuild::new() + .current_release() + .current_target() + .manifest_path("../rstack-child/Cargo.toml") + .target_dir(&out_dir) + .run() + .expect("Compiling rstack-child failed."); + + let child_template = r#" + fn launch_child() -> rstack_self::Result { + let exe = "PATH"; + Ok(rstack_self::trace(&mut Command::new(exe))?) + } + "#; + let child_template = child_template.replace( + "PATH", + run.path().to_str().expect("Unexpected characters in path."), + ); + + let dest_path = Path::new(&out_dir).join("child.rs"); + let mut f = File::create(&dest_path).expect("Opening child.rs failed."); + f.write_all(child_template.as_bytes()) + .expect("Writing child.rs failed."); + + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test/rstack-launcher/src/lib.rs b/test/rstack-launcher/src/lib.rs new file mode 100644 index 0000000..154d189 --- /dev/null +++ b/test/rstack-launcher/src/lib.rs @@ -0,0 +1,80 @@ +use std::fmt::{Display, Formatter}; +use std::process::Command; + +include!(concat!(env!("OUT_DIR"), "/child.rs")); + +/// An error in launching the child. +#[derive(Debug)] +pub enum Error +{ + /// The error originates from rstack_self. + Rstack(rstack_self::Error), + + /// The specified thread was not available. + ThreadNotFound, +} + +/// The result type returned by methods in this crate. +pub type Result = std::result::Result; + +/// Captures the callstack of a thread of the calling process. +pub fn capture_self(thread_id: i64) -> Result +{ + let trace: rstack_self::Trace = launch_child()?; + match trace + .threads() + .into_iter() + .find(|&t| t.id() as i64 == thread_id) + { + Some(thread) => Ok(thread.clone()), + None => Err(Error::ThreadNotFound), + } +} + +impl From for Error +{ + fn from(value: rstack_self::Error) -> Self + { + Self::Rstack(value) + } +} + +impl Display for Error +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result + { + match self { + Error::Rstack(error) => Ok(error.fmt(f)?), + Error::ThreadNotFound => write!(f, "Specified thread unavailable."), + } + } +} + +impl std::error::Error for Error {} + +#[cfg(test)] +mod test +{ + use crate::capture_self; + + #[test] + fn capturing_callstack_succeeds() + { + let thread_id = get_current_native_thread_id(); + let callstack = capture_self(thread_id).expect("Capturing self failed"); + assert_eq!(callstack.id() as i64, thread_id); + assert_eq!(callstack.name(), "test::capturing"); + } + + /// Gets the current native thread id. + fn get_current_native_thread_id() -> i64 + { + #[cfg(not(target_os = "windows"))] + return os_id::thread::get_raw_id() as i64; + + #[cfg(target_os = "windows")] + unsafe { + return windows::Win32::System::Threading::GetCurrentThreadId() as i64; + } + } +}