blob: a4682ccdf29698847aea25f7f205ff5e4225ff3c [file] [log] [blame]
// 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.
// Triggers a snapshot via FIDL
use log::{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().
const CRASH_PROGRAM_NAME: &str = "triage_detect";
#[derive(Debug)]
pub struct SnapshotRequest {
signature: String,
}
impl SnapshotRequest {
pub fn new(signature: String) -> SnapshotRequest {
SnapshotRequest { signature }
}
}
// Code shamelessly stolen and slightly adapted from
// garnet/bin/power_manager/src/crash_report_handler.rs
use anyhow::{anyhow, format_err, Error};
//use async_trait::async_trait;
use fidl_fuchsia_feedback as fidl_feedback;
use fuchsia_async as fasync;
use futures::channel::mpsc;
use futures::stream::StreamExt;
use std::cell::RefCell;
use std::rc::Rc;
/// CrashReportHandler
///
/// 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.
///
/// Path to the CrashReporter service.
const CRASH_REPORTER_SVC: &'static str = "/svc/fuchsia.feedback.CrashReporter";
/// Path to the CrashReportingProductRegister service.
const CRASH_REGISTER_SVC: &'static str = "/svc/fuchsia.feedback.CrashReportingProductRegister";
/// 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 {
proxy: Option<fidl_feedback::CrashReporterProxy>,
max_pending_crash_reports: usize,
}
// This function is from fuchsia-mirror/garnet/bin/power_manager/src/utils.rs
/// Create and connect a FIDL proxy to the service at `path`
fn connect_proxy<T: fidl::endpoints::ServiceMarker>(
path: &String,
) -> Result<T::Proxy, anyhow::Error> {
let (proxy, server) = fidl::endpoints::create_proxy::<T>()
.map_err(|e| anyhow::format_err!("Failed to create proxy: {}", e))?;
fdio::service_connect(path, server.into_channel())
.map_err(|s| anyhow::format_err!("Failed to connect to service at {}: {}", path, s))?;
Ok(proxy)
}
/// 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() {
log::error!("{}: {}", $log_prefix, e);
}
};
}
impl CrashReportHandlerBuilder {
pub fn new() -> Self {
Self { proxy: None, max_pending_crash_reports: MAX_PENDING_CRASH_REPORTS }
}
#[cfg(test)]
pub fn with_proxy(mut self, proxy: fidl_feedback::CrashReporterProxy) -> Self {
self.proxy = Some(proxy);
self
}
#[cfg(test)]
pub fn with_max_pending_crash_reports(mut self, max: usize) -> Self {
self.max_pending_crash_reports = max;
self
}
pub 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_proxy::<fidl_feedback::CrashReportingProductRegisterMarker>(
&CRASH_REGISTER_SVC.to_string(),
)?;
let product_config = fidl_feedback::CrashReportingProduct {
name: Some(CRASH_PRODUCT_NAME.to_string()),
..fidl_feedback::CrashReportingProduct::EMPTY
};
config_proxy.upsert(&CRASH_PROGRAM_NAME.to_string(), product_config)?;
}
// Connect to the CrashReporter service if a proxy wasn't specified
let proxy = if self.proxy.is_some() {
self.proxy.unwrap()
} else {
connect_proxy::<fidl_feedback::CrashReporterMarker>(&CRASH_REPORTER_SVC.to_string())?
};
// Set up the crash report sender that runs asynchronously
let (channel, receiver) = mpsc::channel(self.max_pending_crash_reports);
CrashReportHandler::begin_crash_report_sender(proxy, receiver);
Ok(Rc::new(CrashReportHandler {
channel_size: self.max_pending_crash_reports,
crash_report_sender: RefCell::new(channel),
}))
}
}
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,
}
impl CrashReportHandler {
/// 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 and detach a future 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(
proxy: fidl_feedback::CrashReporterProxy,
mut receive_channel: mpsc::Receiver<SnapshotRequest>,
) {
fasync::Task::local(async move {
while let Some(request) = receive_channel.next().await {
log_if_err!(
Self::send_crash_report(&proxy, request).await,
"Failed to file crash report"
);
}
error!("Crash reporter task ended. Crash reports will no longer be filed. This should not happen.")
})
.detach();
}
/// Send a File request to the CrashReporter service with the specified crash report signature.
async fn send_crash_report(
proxy: &fidl_feedback::CrashReporterProxy,
payload: SnapshotRequest,
) -> Result<(), Error> {
warn!("Filing crash report, signature '{}'", payload.signature);
let report = fidl_feedback::CrashReport {
program_name: Some(CRASH_PROGRAM_NAME.to_string()),
crash_signature: Some(payload.signature),
..fidl_feedback::CrashReport::EMPTY
};
let result = proxy.file(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 futures::TryStreamExt;
use matches::assert_matches;
/// Tests that the node responds to the FileCrashReport message and that the expected crash
/// report is received by the CrashReporter service.
#[fasync::run_singlethreaded(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 crash_report_handler =
CrashReportHandlerBuilder::new().with_proxy(proxy).build().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::File { responder: _, report })) =
stream.try_next().await
{
assert_eq!(
report,
fidl_feedback::CrashReport {
program_name: Some(CRASH_PROGRAM_NAME.to_string()),
crash_signature: Some(crash_report_signature.to_string()),
..fidl_feedback::CrashReport::EMPTY
}
);
} else {
panic!("Did not receive a crash report");
}
}
/// Tests that the number of pending crash reports is correctly bounded.
#[test]
fn test_crash_report_pending_reports() {
let mut exec = fasync::Executor::new().unwrap();
// 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 crash_report_handler = CrashReportHandlerBuilder::new()
.with_proxy(proxy)
.with_max_pending_crash_reports(1)
.build()
.unwrap();
// Run most of the test logic inside a top level future for better ergonomics
exec.run_singlethreaded(async {
// 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::File { responder, report })) =
stream.try_next().await
{
// Send a reply to allow the node to process the next crash report
let _ = responder.send(&mut Ok(()));
assert_eq!(
report,
fidl_feedback::CrashReport {
program_name: Some(CRASH_PROGRAM_NAME.to_string()),
crash_signature: Some("TestCrash1".to_string()),
..fidl_feedback::CrashReport::EMPTY
}
);
} else {
panic!("Did not receive a crash report");
}
// Verify the signature of the second crash report
if let Ok(Some(fidl_feedback::CrashReporterRequest::File { responder, report })) =
stream.try_next().await
{
// Send a reply to allow the node to process the next crash report
let _ = responder.send(&mut Ok(()));
assert_eq!(
report,
fidl_feedback::CrashReport {
program_name: Some(CRASH_PROGRAM_NAME.to_string()),
crash_signature: Some("TestCrash2".to_string()),
..fidl_feedback::CrashReport::EMPTY
}
);
} else {
panic!("Did not receive a crash report");
}
});
// Verify there are no more crash reports. Use `run_until_stalled` because `next` is
// expected to block until a new crash report is ready, which shouldn't happen here.
assert!(exec.run_until_stalled(&mut stream.next()).is_pending());
}
}