| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{anyhow, format_err, Error}; |
| use fidl_fuchsia_feedback as fidl_feedback; |
| use fuchsia_async as fasync; |
| use fuchsia_component::client::connect_to_protocol; |
| use futures::channel::mpsc; |
| use futures::stream::StreamExt; |
| use injectable_time::TimeSource; |
| use std::cell::RefCell; |
| use std::rc::Rc; |
| use tracing::{error, warn}; |
| |
| // Name of the crash-report product we're filing against. |
| const CRASH_PRODUCT_NAME: &'static str = "FuchsiaDetect"; |
| // CRASH_PROGRAM_NAME serves two purposes: |
| // 1) It is sent with the crash report. It may show up on the server as |
| // "process type". |
| // 2) The on-device crash reporting program associates this string with the |
| // "product" CRASH_PRODUCT_NAME we're requesting to file against, so we |
| // only have to send the program name and not the product name with each |
| // crash report request. |
| // This association is registered via a call to |
| // CrashReportingProductRegister.upsert_with_ack(). |
| const CRASH_PROGRAM_NAME: &str = "triage_detect"; |
| |
| #[derive(Debug)] |
| pub struct SnapshotRequest { |
| signature: String, |
| } |
| |
| impl SnapshotRequest { |
| pub fn new(signature: String) -> SnapshotRequest { |
| SnapshotRequest { signature } |
| } |
| } |
| |
| /// The maximum number of pending crash report requests. This is needed because the FIDL API to file |
| /// a crash report does not return until the crash report has been fully generated, which can take |
| /// many seconds. Supporting pending crash reports means Detect can file |
| /// a new crash report for any other reason within that window, but the CrashReportHandler will |
| /// handle rate limiting to the CrashReporter service. |
| const MAX_PENDING_CRASH_REPORTS: usize = 10; |
| |
| /// A builder for constructing the CrashReportHandler node. |
| pub struct CrashReportHandlerBuilder<T: TimeSource> { |
| proxy: Option<fidl_feedback::CrashReporterProxy>, |
| max_pending_crash_reports: usize, |
| time_source: T, |
| } |
| |
| /// Logs an error message if the passed in `result` is an error. |
| #[macro_export] |
| macro_rules! log_if_err { |
| ($result:expr, $log_prefix:expr) => { |
| if let Err(e) = $result.as_ref() { |
| tracing::error!("{}: {}", $log_prefix, e); |
| } |
| }; |
| } |
| |
| impl<T> CrashReportHandlerBuilder<T> |
| where |
| T: TimeSource + 'static, |
| { |
| pub fn new(time_source: T) -> Self { |
| Self { time_source, max_pending_crash_reports: MAX_PENDING_CRASH_REPORTS, proxy: None } |
| } |
| |
| pub async fn build(self) -> Result<Rc<CrashReportHandler>, Error> { |
| // Proxy is only pre-set for tests. If a proxy was not specified, |
| // this is a good time to configure for our crash reporting product. |
| if matches!(self.proxy, None) { |
| let config_proxy = |
| connect_to_protocol::<fidl_feedback::CrashReportingProductRegisterMarker>()?; |
| let product_config = fidl_feedback::CrashReportingProduct { |
| name: Some(CRASH_PRODUCT_NAME.to_string()), |
| ..Default::default() |
| }; |
| config_proxy.upsert_with_ack(&CRASH_PROGRAM_NAME.to_string(), &product_config).await?; |
| } |
| // Connect to the CrashReporter service if a proxy wasn't specified |
| let proxy = |
| self.proxy.unwrap_or(connect_to_protocol::<fidl_feedback::CrashReporterMarker>()?); |
| Ok(Rc::new(CrashReportHandler::new( |
| proxy, |
| self.time_source, |
| self.max_pending_crash_reports, |
| ))) |
| } |
| } |
| |
| #[cfg(test)] |
| impl<T> CrashReportHandlerBuilder<T> |
| where |
| T: TimeSource, |
| { |
| fn with_proxy(mut self, proxy: fidl_feedback::CrashReporterProxy) -> Self { |
| self.proxy = Some(proxy); |
| self |
| } |
| |
| fn with_max_pending_crash_reports(mut self, max: usize) -> Self { |
| self.max_pending_crash_reports = max; |
| self |
| } |
| } |
| |
| /// CrashReportHandler |
| /// Triggers a snapshot via FIDL |
| /// |
| /// Summary: Provides a mechanism for filing crash reports. |
| /// |
| /// FIDL dependencies: |
| /// - fuchsia.feedback.CrashReporter: CrashReportHandler uses this protocol to communicate |
| /// with the CrashReporter service in order to file crash reports. |
| /// - fuchsia.feedback.CrashReportingProductRegister: CrashReportHandler uses this protocol |
| /// to communicate with the CrashReportingProductRegister service in order to configure |
| /// the crash reporting product it will be filing on. |
| pub struct CrashReportHandler { |
| /// The channel to send new crash report requests to the asynchronous crash report sender |
| /// future. The maximum pending crash reports are implicitly enforced by the channel length. |
| crash_report_sender: RefCell<mpsc::Sender<SnapshotRequest>>, |
| channel_size: usize, |
| _server_task: fasync::Task<()>, |
| } |
| |
| impl CrashReportHandler { |
| fn new<T>(proxy: fidl_feedback::CrashReporterProxy, time_source: T, channel_size: usize) -> Self |
| where |
| T: TimeSource + 'static, |
| { |
| // Set up the crash report sender that runs asynchronously |
| let (channel, receiver) = mpsc::channel(channel_size); |
| let server_task = Self::begin_crash_report_sender(proxy, receiver, time_source); |
| Self { channel_size, crash_report_sender: RefCell::new(channel), _server_task: server_task } |
| } |
| |
| /// Handle a FileCrashReport message by sending the specified crash report signature over the |
| /// channel to the crash report sender. |
| pub fn request_snapshot(&self, request: SnapshotRequest) -> Result<(), Error> { |
| // Try to send the crash report signature over the channel. If the channel is full, return |
| // an error |
| match self.crash_report_sender.borrow_mut().try_send(request) { |
| Ok(()) => Ok(()), |
| Err(e) if e.is_full() => { |
| warn!("Too many crash reports pending: {e}"); |
| Err(anyhow!("Pending crash reports exceeds max ({})", self.channel_size)) |
| } |
| Err(e) => { |
| warn!("Error sending crash report: {e}"); |
| Err(anyhow!("{e}")) |
| } |
| } |
| } |
| |
| /// Spawn a Task that receives crash report signatures over the channel and uses |
| /// the proxy to send a File FIDL request to the CrashReporter service with the specified |
| /// signatures. |
| fn begin_crash_report_sender<T>( |
| proxy: fidl_feedback::CrashReporterProxy, |
| mut receive_channel: mpsc::Receiver<SnapshotRequest>, |
| time_source: T, |
| ) -> fasync::Task<()> |
| where |
| T: TimeSource + 'static, |
| { |
| fasync::Task::local(async move { |
| while let Some(request) = receive_channel.next().await { |
| log_if_err!( |
| Self::send_crash_report(&proxy, request, &time_source).await, |
| "Failed to file crash report" |
| ); |
| } |
| error!("Crash reporter task ended. Crash reports will no longer be filed. This should not happen.") |
| }) |
| } |
| |
| /// Send a File request to the CrashReporter service with the specified crash report signature. |
| async fn send_crash_report<T: TimeSource>( |
| proxy: &fidl_feedback::CrashReporterProxy, |
| payload: SnapshotRequest, |
| time_source: &T, |
| ) -> Result<fidl_feedback::FileReportResults, Error> { |
| warn!("Filing crash report, signature '{}'", payload.signature); |
| let report = fidl_feedback::CrashReport { |
| program_name: Some(CRASH_PROGRAM_NAME.to_string()), |
| program_uptime: Some(time_source.now()), |
| crash_signature: Some(payload.signature), |
| is_fatal: Some(false), |
| ..Default::default() |
| }; |
| |
| let result = proxy.file_report(report).await.map_err(|e| format_err!("IPC error: {e}"))?; |
| result.map_err(|e| format_err!("Service error: {e:?}")) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use assert_matches::assert_matches; |
| use futures::TryStreamExt; |
| use injectable_time::{FakeTime, IncrementingFakeTime}; |
| |
| /// Tests that the node responds to the FileCrashReport message and that the expected crash |
| /// report is received by the CrashReporter service. |
| #[fuchsia::test] |
| async fn test_crash_report_content() { |
| // The crash report signature to use and verify against |
| let crash_report_signature = "TestCrashReportSignature"; |
| |
| // Set up the CrashReportHandler node |
| let (proxy, mut stream) = |
| fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>() |
| .unwrap(); |
| let fake_time = FakeTime::new(); |
| fake_time.set_ticks(9876); |
| let crash_report_handler = |
| CrashReportHandlerBuilder::new(fake_time).with_proxy(proxy).build().await.unwrap(); |
| |
| // File a crash report |
| crash_report_handler |
| .request_snapshot(SnapshotRequest::new(crash_report_signature.to_string())) |
| .unwrap(); |
| |
| // Verify the fake service receives the crash report with expected data |
| if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder: _, report })) = |
| stream.try_next().await |
| { |
| assert_eq!( |
| report, |
| fidl_feedback::CrashReport { |
| program_name: Some(CRASH_PROGRAM_NAME.to_string()), |
| program_uptime: Some(9876), |
| crash_signature: Some(crash_report_signature.to_string()), |
| is_fatal: Some(false), |
| ..Default::default() |
| } |
| ); |
| } else { |
| panic!("Did not receive a crash report"); |
| } |
| } |
| |
| /// Tests that the number of pending crash reports is correctly bounded. |
| #[fuchsia::test] |
| async fn test_crash_report_pending_reports() { |
| // Set up the proxy/stream and node outside of the large future used below. This way we can |
| // still poll the stream after the future completes. |
| let (proxy, mut stream) = |
| fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>() |
| .unwrap(); |
| let fake_time = IncrementingFakeTime::new(1000, std::time::Duration::from_nanos(1000)); |
| let crash_report_handler = CrashReportHandlerBuilder::new(fake_time) |
| .with_proxy(proxy) |
| .with_max_pending_crash_reports(1) |
| .build() |
| .await |
| .unwrap(); |
| |
| // Set up the CrashReportHandler node. The request stream is never serviced, so when the |
| // node makes the FIDL call to file the crash report, the call will block indefinitely. |
| // This lets us test the pending crash report counts. |
| |
| // The first FileCrashReport should succeed |
| assert_matches!( |
| crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash1".to_string())), |
| Ok(()) |
| ); |
| |
| // The second FileCrashReport should also succeed because since the first is now in |
| // progress, this is now the first "pending" report request |
| assert_matches!( |
| crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash2".to_string())), |
| Ok(()) |
| ); |
| |
| // Since the first request has not completed, and there is already one pending request, |
| // this request should fail |
| assert_matches!( |
| crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash3".to_string())), |
| Err(_) |
| ); |
| |
| // Verify the signature of the first crash report |
| if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder, report })) = |
| stream.try_next().await |
| { |
| // Send a reply to allow the node to process the next crash report |
| let _ = responder.send(Ok(&fidl_feedback::FileReportResults::default())); |
| assert_eq!( |
| report, |
| fidl_feedback::CrashReport { |
| program_name: Some(CRASH_PROGRAM_NAME.to_string()), |
| program_uptime: Some(1000), |
| crash_signature: Some("TestCrash1".to_string()), |
| is_fatal: Some(false), |
| ..Default::default() |
| } |
| ); |
| } else { |
| panic!("Did not receive a crash report"); |
| } |
| |
| // Verify the signature of the second crash report |
| if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder, report })) = |
| stream.try_next().await |
| { |
| // Send a reply to allow the node to process the next crash report |
| let _ = responder.send(Ok(&fidl_feedback::FileReportResults::default())); |
| assert_eq!( |
| report, |
| fidl_feedback::CrashReport { |
| program_name: Some(CRASH_PROGRAM_NAME.to_string()), |
| program_uptime: Some(2000), |
| crash_signature: Some("TestCrash2".to_string()), |
| is_fatal: Some(false), |
| ..Default::default() |
| } |
| ); |
| } else { |
| panic!("Did not receive a crash report"); |
| } |
| } |
| } |