diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6f2300..ec6e13b 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-dev + uses: ConorMacBride/install-package@v1 + with: + apt: libunwind-dev + - name: Environment run: | cargo --version @@ -69,6 +74,10 @@ jobs: - name: Install Protoc uses: arduino/setup-protoc@v2 + - name: Install libunwind-dev + run: | + sudo apt update && sudo apt install -y libunwind-dev + - 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..2969c79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,11 +41,18 @@ wildmatch = "1" glob = "0.3" shell-words = "1" +[target.'cfg(unix)'.dependencies] +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" +[target.'cfg(unix)'.dev-dependencies] +rstack-self = "0.3.0" +rstack-launcher = { version = "0.1.0", path = "test/rstack-launcher" } + [profile.release] debug = true \ No newline at end of file diff --git a/src/connection.rs b/src/connection.rs index 443a052..3003e62 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,4 +1,6 @@ +use http::{HeaderMap, HeaderValue}; use snafu::{ResultExt, Snafu}; +use std::convert::TryFrom; use std::net::SocketAddr; use std::sync::mpsc::Sender; use std::sync::Arc; @@ -124,6 +126,58 @@ impl Streams } } +/// When available, identifies the thread in the calling or client process. +/// The client should reports its process id with the proxide-client-process-id" header and +/// the thread id with the "proxide-client-thread-id" header. +/// This enables the proxide proxy to capture client's callstack when it is making the call if the proxide +/// and the client are running on the same host. +pub struct ClientThreadId +{ + process_id: u32, + thread_id: i64, +} + +impl ClientThreadId +{ + pub fn process_id(&self) -> u32 + { + self.process_id + } + + pub fn thread_id(&self) -> i64 + { + self.thread_id + } +} + +impl TryFrom<&MessageData> for ClientThreadId +{ + type Error = (); + + fn try_from(value: &MessageData) -> std::result::Result + { + ClientThreadId::try_from(&value.headers) + } +} + +impl TryFrom<&HeaderMap> for ClientThreadId +{ + type Error = (); + + fn try_from(value: &HeaderMap) -> std::result::Result + { + let process_id: Option = number_or_none(&value.get("proxide-client-process-id")); + let thread_id: Option = number_or_none(&value.get("proxide-client-thread-id")); + match (process_id, thread_id) { + (Some(process_id), Some(thread_id)) => Ok(ClientThreadId { + process_id, + thread_id, + }), + _ => Err(()), + } + } +} + /// Handles a single client connection. /// /// The connection handling is split into multiple functions, but the functions are chained in a @@ -311,3 +365,17 @@ where log::info!("Exit"); }); } + +fn number_or_none(header: &Option<&HeaderValue>) -> Option +where + N: std::str::FromStr, +{ + if let Some(value) = header { + value + .to_str() + .map(|s| N::from_str(s).map(|n| Some(n)).unwrap_or(None)) + .unwrap_or(None) + } else { + None + } +} diff --git a/src/connection/http2.rs b/src/connection/http2.rs index 512b85b..cf18658 100644 --- a/src/connection/http2.rs +++ b/src/connection/http2.rs @@ -8,10 +8,15 @@ use h2::{ use http::{HeaderMap, Request, Response}; use log::error; use snafu::ResultExt; +use std::convert::TryFrom; use std::net::SocketAddr; +use std::pin::Pin; 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; use super::*; @@ -87,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 { @@ -100,6 +106,7 @@ where client_request, client_response, server_stream, + processing_control.clone(), &ui, )?; @@ -141,16 +148,29 @@ pub struct ProxyRequest client_response: SendResponse, server_request: SendStream, server_response: ResponseFuture, + request_processor: ProcessingFuture, +} + +/// Manages the asynchronous auxiliary processing of requests. +struct ProcessingControl +{ + callstack_capture_limiter: Semaphore, +} + +struct ProcessingFuture +{ + inner: JoinHandle<()>, } 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 { @@ -191,6 +211,10 @@ impl ProxyRequest })) .unwrap(); + // 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, processing_control, ui); + let server_request = Request::from_parts(client_head, ()); // Set up a server request. @@ -208,6 +232,7 @@ impl ProxyRequest client_response, server_request, server_response, + request_processor, }) } @@ -265,6 +290,7 @@ impl ProxyRequest let mut client_response = self.client_response; let server_response = self.server_response; let connection_uuid = self.connection_uuid; + let request_processor = self.request_processor; let ui_temp = ui.clone(); let response_future = async move { let ui = ui_temp; @@ -293,6 +319,11 @@ impl ProxyRequest scenario: "sending response", })?; + // Ensure the request processor has finished before we send the response to the client. + // Callstack capturing process inside the request processor may capture incorrect data if + // the client is given the final answer from the server as it no longer has to wait for the response. + request_processor.await; + // The server might have sent all the details in the headers, at which point there is // no body present. Check for this scenario here. if response_body.is_end_stream() { @@ -440,3 +471,155 @@ fn is_fatal_error(r: &Result) -> bool }, } } + +impl ProcessingFuture +{ + fn spawn( + uuid: Uuid, + client_head: &http::request::Parts, + processing_control: Arc, + ui: &Sender, + ) -> Self + { + let mut tasks: JoinSet>> = + JoinSet::new(); + + // Task which attempts to capture client's callstack. + 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, + processing_control.clone(), + ui_clone, + )); + } + + Self { + inner: tokio::spawn(async move { + while let Some(result) = tasks.join_next().await { + match result { + Ok(_) => {} + Err(e) => { + // TODO: Send the error to UI. + eprintln!("{}", e); + error!("{}", e); + } + } + } + }), + } + } + + async fn capture_client_callstack( + uuid: Uuid, + client_thread_id: ClientThreadId, + processing_control: Arc, + ui: Sender, + ) -> std::result::Result<(), Box> + { + // 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. + #[cfg(target_os = "linux")] + capture_client_callstack_rstack(uuid, client_thread_id, ui).await?; + + #[cfg(not(target_os = "linux"))] + capture_client_callstack_unsupported(uuid, client_thread_id, ui).await?; + + Ok(()) + } + Err(TryAcquireError::NoPermits) => { + ui.send(SessionEvent::ClientCallstackProcessed( + ClientCallstackProcessedEvent { + uuid, + callstack: ClientCallstack::Throttled, + }, + ))?; + Ok(()) + } + Err(e) => Err(Box::from(e)), + } + } +} + +impl Future for ProcessingFuture +{ + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll + { + match Pin::new(&mut self.inner).poll(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } +} + +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)), + }, + ))?; + } + + #[cfg(not(test))] + { + capture_client_callstack_unsupported(uuid, client_thread_id, ui).await?; + } + Ok(()) + } +} + +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 5f37324..b9bc9cc 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; @@ -89,6 +90,11 @@ pub struct ProxyFilter fn main() { + // Launched to capture stack? + if env::args_os().len() == 2 && env::args_os().any(|p| p == "child") { + return; + } + match proxide_main() { Ok(_) => (), Err(e) => { @@ -419,6 +425,7 @@ mod test use log::SetLoggerError; use serial_test::serial; use std::io::{ErrorKind, Write}; + use std::ops::Add; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -426,8 +433,10 @@ mod test use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; + use tokio::time::Instant; use crate::session::events::SessionEvent; + use crate::session::ClientCallstack; use crate::ConnectionOptions; lazy_static! { @@ -533,6 +542,86 @@ mod test .expect("Waiting for proxide to stop failed."); } + #[tokio::test] + #[serial] + async fn proxide_receives_client_callstack_ui_message() + { + // Logging must be enabled to detect errors inside proxide. + // Failure to monitor logs may cause the test to hang as errors that stop processing get silently ignored. + let mut error_monitor = get_error_monitor().expect("Acquiring error monitor failed."); + + // Server + let server = GrpcServer::start() + .await + .expect("Starting test server failed."); + + // Proxide + let options = get_proxide_options(&server); + let (abort_tx, abort_rx) = tokio::sync::oneshot::channel::<()>(); + let (ui_tx, ui_rx_std) = std::sync::mpsc::channel(); + let proxide_port = u16::from_str(&options.listen_port.to_string()).unwrap(); + let proxide = tokio::spawn(crate::launch_proxide(options, abort_rx, ui_tx)); + + // Message generator and tester. + let tester = grpc_tester::GrpcTester::with_proxide( + server, + proxide_port, + grpc_tester::Args { + period: std::time::Duration::from_secs(0), + tasks: 1, + }, + ) + .await + .expect("Starting tester failed."); + let mut message_rx = async_from_sync(ui_rx_std); + + // 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: 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 ), + } { + // 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") + } + } + + // Ensure the ui channel was not closed prematurely. + #[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."); + proxide + .await + .expect("Waiting for proxide to stop failed.") + .expect("Waiting for proxide to stop failed."); + server.stop().expect("Stopping server failed"); + } + /// Gets options for launching proxide. fn get_proxide_options(server: &GrpcServer) -> Arc { diff --git a/src/session.rs b/src/session.rs index 83f3ce1..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; @@ -56,6 +57,7 @@ pub struct RequestData pub start_timestamp: DateTime, pub end_timestamp: Option>, + pub client_callstack: Option, pub status: Status, } @@ -126,6 +128,29 @@ impl MessageData } } +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +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 { pub fn push(&mut self, uuid: Uuid, item: T) diff --git a/src/session/callstack.rs b/src/session/callstack.rs new file mode 100644 index 0000000..f8cf044 --- /dev/null +++ b/src/session/callstack.rs @@ -0,0 +1,131 @@ +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 + } +} + +#[cfg(target_os = "linux")] +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(Frame::from).collect(), + } + } +} + +#[cfg(target_os = "linux")] +impl From<&rstack::Frame> for Frame +{ + fn from(value: &rstack::Frame) -> Self + { + Frame { + symbols: value.symbol().iter().map(|s| Symbol::from(*s)).collect(), + } + } +} + +#[cfg(target_os = "linux")] +impl From<&rstack::Symbol> for Symbol +{ + fn from(value: &rstack::Symbol) -> Self + { + Self { + name: value.name().to_string(), + } + } +} + +#[cfg(all(target_os = "linux", 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(Frame::from).collect(), + } + } +} + +#[cfg(all(target_os = "linux", test))] +impl From<&rstack_self::Frame> for Frame +{ + fn from(value: &rstack_self::Frame) -> Self + { + Self { + symbols: value.symbols().iter().map(Symbol::from).collect(), + } + } +} +#[cfg(all(target_os = "linux", 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/session/events.rs b/src/session/events.rs index 2334d04..7a77242 100644 --- a/src/session/events.rs +++ b/src/session/events.rs @@ -14,6 +14,7 @@ pub enum SessionEvent MessageDone(MessageDoneEvent), RequestDone(RequestDoneEvent), ConnectionDone(ConnectionDoneEvent), + ClientCallstackProcessed(ClientCallstackProcessedEvent), } #[derive(Serialize, Deserialize, Debug)] @@ -84,6 +85,13 @@ pub struct ConnectionDoneEvent pub timestamp: SystemTime, } +#[derive(Serialize, Deserialize, Debug)] +pub struct ClientCallstackProcessedEvent +{ + pub uuid: Uuid, + pub callstack: ClientCallstack, +} + pub enum SessionChange { NewConnection @@ -110,6 +118,10 @@ pub enum SessionChange { connection: Uuid }, + Callstack + { + request: Uuid + }, } impl Session @@ -124,6 +136,7 @@ impl Session SessionEvent::MessageDone(e) => self.on_message_done(e), SessionEvent::RequestDone(e) => self.on_request_done(e), SessionEvent::ConnectionDone(e) => self.on_connection_done(e), + SessionEvent::ClientCallstackProcessed(e) => self.on_client_callstack_processed(e), } } @@ -154,6 +167,7 @@ impl Session status: Status::InProgress, start_timestamp: e.timestamp.into(), end_timestamp: None, + client_callstack: None, }, request_msg: MessageData::new(RequestPart::Request) .with_headers(e.headers) @@ -247,4 +261,18 @@ impl Session vec![] } } + + fn on_client_callstack_processed( + &mut self, + e: ClientCallstackProcessedEvent, + ) -> Vec + { + let request = self.requests.get_mut_by_uuid(e.uuid); + if let Some(request) = request { + request.request_data.client_callstack = Some(e.callstack); + vec![SessionChange::Callstack { request: e.uuid }] + } else { + vec![] + } + } } diff --git a/src/ui.rs b/src/ui.rs index 4023de5..24ddd55 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -92,8 +92,7 @@ pub fn main( state.draw(&mut terminal).context(IoError {})?; let mut redraw_pending = false; loop { - let e = ui_rx.recv().unwrap(); - + let e = ui_rx.recv().expect("Receiving UI events failed."); if let UiEvent::Redraw = e { redraw_pending = false; state.draw(&mut terminal).context(IoError {})?; diff --git a/src/ui/state.rs b/src/ui/state.rs index 92248a9..fab4eaa 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -257,7 +257,7 @@ impl ProxideUi let help_text = if let Some(cmd) = &self.input_command { format!("{}\n{}{}", cmd.help, cmd.prompt, cmd.input) } else { - let view = self.ui_stack.last_mut().unwrap(); + let view = self.ui_stack.last_mut().expect("Empty UI stack."); view.help_text(&self.context, self.context.size) }; diff --git a/src/ui/sub_views/details_pane.rs b/src/ui/sub_views/details_pane.rs index 9827382..d0c0a75 100644 --- a/src/ui/sub_views/details_pane.rs +++ b/src/ui/sub_views/details_pane.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use tui::layout::{Constraint, Direction, Layout}; use tui::text::{Span, Spans, Text}; use tui::widgets::Paragraph; @@ -6,7 +7,7 @@ use uuid::Uuid; use crate::ui::prelude::*; use crate::session::{EncodedRequest, RequestPart}; -use crate::ui::views::MessageView; +use crate::ui::views::{CallstackView, MessageView}; #[derive(Clone, Default)] pub struct DetailsPane; @@ -22,6 +23,7 @@ impl DetailsPane match key.code { KeyCode::Char('q') => self.create_message_view(req, RequestPart::Request), KeyCode::Char('e') => self.create_message_view(req, RequestPart::Response), + KeyCode::Char('s') => self.create_callstack_view(req), _ => None, } } else { @@ -61,11 +63,21 @@ impl DetailsPane c.x -= 1; c.width += 2; c.height += 1; + let vertical_chunks: Vec = + if crate::connection::ClientThreadId::try_from(&request.request_msg).is_ok() { + Layout::default() + .direction(Direction::Vertical) + .margin(0) + .constraints([Constraint::Percentage(80), Constraint::Percentage(20)].as_ref()) + .split(block.inner(c)) + } else { + Vec::from([block.inner(c)]) + }; let req_resp_chunks = Layout::default() .direction(Direction::Horizontal) .margin(0) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(block.inner(c)); + .split(vertical_chunks[0]); f.render_widget(block, chunk); @@ -114,6 +126,16 @@ impl DetailsPane offset: 0, } .draw(ctx, f, req_resp_chunks[1]); + + // The right side view is split vertically only if the client included its process id and thread id in the request + // enabling the callstack capture. + if vertical_chunks.len() > 1 { + CallstackView { + request: request.request_data.uuid, + offset: 0, + } + .draw(ctx, f, vertical_chunks[1]); + } } fn create_message_view( @@ -128,4 +150,17 @@ impl DetailsPane offset: 0, }))) } + + fn create_callstack_view(&mut self, req: &EncodedRequest) + -> Option> + { + if crate::connection::ClientThreadId::try_from(&req.request_msg).is_ok() { + Some(HandleResult::PushView(Box::new(CallstackView { + request: req.request_data.uuid, + offset: 0, + }))) + } else { + None + } + } } diff --git a/src/ui/toast.rs b/src/ui/toast.rs index a730170..d545230 100644 --- a/src/ui/toast.rs +++ b/src/ui/toast.rs @@ -51,7 +51,9 @@ impl PartialEq for FutureEvent { fn eq(&self, other: &Self) -> bool { - self.instant.eq(&other.instant) + // clippy reports an "unconditional recursion"false positive here in the pipeline with: + // "self.instant.eq(&other.instant)" + PartialEq::::eq(&self.instant, &other.instant) } } diff --git a/src/ui/views.rs b/src/ui/views.rs index b319503..5f88b49 100644 --- a/src/ui/views.rs +++ b/src/ui/views.rs @@ -6,6 +6,9 @@ pub use main_view::MainView; mod message_view; pub use message_view::MessageView; +mod callstack_view; +pub use callstack_view::CallstackView; + pub trait View { fn draw(&mut self, ctx: &UiContext, f: &mut Frame, chunk: Rect); diff --git a/src/ui/views/callstack_view.rs b/src/ui/views/callstack_view.rs new file mode 100644 index 0000000..cfff488 --- /dev/null +++ b/src/ui/views/callstack_view.rs @@ -0,0 +1,111 @@ +use super::prelude::*; +use crate::session::ClientCallstack; +use crossterm::event::KeyCode; +use std::convert::TryFrom; +use tui::widgets::{Paragraph, Wrap}; +use uuid::Uuid; + +pub struct CallstackView +{ + pub request: Uuid, + pub offset: u16, +} + +impl CallstackView {} + +impl View for CallstackView +{ + fn draw(&mut self, ctx: &UiContext, f: &mut Frame, chunk: Rect) + { + let request = match ctx.data.requests.get_by_uuid(self.request) { + Some(r) => r, + None => return, + }; + + let client_thread = match crate::connection::ClientThreadId::try_from(&request.request_msg) + { + Ok(thread_id) => thread_id, + Err(_) => return, + }; + + let title = format!( + "Client call[s]tack, Process: {}, Thread: {}", + client_thread.process_id(), + client_thread.thread_id() + ); + let message: String = match &request.request_data.client_callstack { + Some(ClientCallstack::Unsupported) => { + "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) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((self.offset, 0)); + f.render_widget(request_data, chunk); + } + + fn on_input(&mut self, _ctx: &UiContext, e: &CTEvent, size: Rect) -> Option> + { + match e { + CTEvent::Key(key) => match key.code { + KeyCode::Char('k') | KeyCode::Up => self.offset = self.offset.saturating_sub(1), + KeyCode::Char('j') | KeyCode::Down => self.offset = self.offset.saturating_add(1), + KeyCode::PageDown => self.offset = self.offset.saturating_add(size.height - 5), + KeyCode::PageUp => self.offset = self.offset.saturating_sub(size.height - 5), + KeyCode::F(12) => { + return None; + } + _ => return None, + }, + _ => return None, + }; + Some(HandleResult::Update) + } + + fn on_change(&mut self, _ctx: &UiContext, change: &SessionChange) -> bool + { + match change { + SessionChange::NewConnection { .. } => false, + SessionChange::Connection { .. } => false, + SessionChange::NewRequest { .. } => false, + SessionChange::Request { .. } => false, + SessionChange::NewMessage { .. } => false, + SessionChange::Message { .. } => false, + SessionChange::Callstack { request } => *request == self.request, + } + } + + fn help_text(&self, _state: &UiContext, _size: Rect) -> String + { + format!( + "{}\n{}", + "[Up/Down, j/k, PgUp/PgDn]: Scroll; [F12]: Export to file", "[Esc]: Back to main view" + ) + } +} + +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/src/ui/views/main_view.rs b/src/ui/views/main_view.rs index 7539f89..870952b 100644 --- a/src/ui/views/main_view.rs +++ b/src/ui/views/main_view.rs @@ -132,6 +132,7 @@ impl View for MainView .selected(&ctx.data.requests) .map(|r| r.request_data.uuid == *req) .unwrap_or(false), + SessionChange::Callstack { .. } => false, } } diff --git a/src/ui/views/message_view.rs b/src/ui/views/message_view.rs index 62070f0..e528147 100644 --- a/src/ui/views/message_view.rs +++ b/src/ui/views/message_view.rs @@ -155,6 +155,7 @@ impl View for MessageView | SessionChange::Message { request, part } => { *part == self.part && *request == self.request } + SessionChange::Callstack { .. } => false, } } 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..15017b6 --- /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 + +[target.'cfg(unix)'.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..ad5064f --- /dev/null +++ b/test/rstack-child/src/main.rs @@ -0,0 +1,13 @@ +fn main() { + #[cfg(target_os = "linux")] + { + let err = rstack_self::child(); + eprintln!("{:?}", err); + err.expect("Capturing callstack with rstack-self failed."); + } + + #[cfg(not(target_os = "linux"))] + { + panic!("Unsupported operating system."); + } +} 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..41f0b8f --- /dev/null +++ b/test/rstack-launcher/Cargo.toml @@ -0,0 +1,15 @@ +[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] +os-id = "3.0.1" + +[target.'cfg(unix)'.dependencies] +rstack-self = "0.3.0" + +[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..9e663f4 --- /dev/null +++ b/test/rstack-launcher/build.rs @@ -0,0 +1,36 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +fn main() +{ + #[cfg(target_os = "linux")] + { + 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..dae838c --- /dev/null +++ b/test/rstack-launcher/src/lib.rs @@ -0,0 +1,93 @@ +use std::fmt::{Display, Formatter}; +use std::process::Command; + +#[cfg(target_os = "linux")] +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, + + /// Unsuportted operating system. + UnsupportedOperatingSystem, +} + +/// 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 +{ + #[cfg(target_os = "linux")] + { + 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), + } + } + + #[cfg(not(target_os = "linux"))] + Err(Error::UnsupportedOperatingSystem) +} + +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."), + Error::UnsupportedOperatingSystem => { + write!(f, "Capture not supported on the current operatins system.") + } + } + } +} + +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; + } + } +}