detect/snapshot.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
// 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 fuchsia_component::client::connect_to_protocol;
use futures::channel::mpsc;
use futures::stream::StreamExt;
use injectable_time::TimeSource;
use std::cell::RefCell;
use tracing::{error, warn};
use {fidl_fuchsia_feedback as fidl_feedback, fuchsia_async as fasync};
// Name of the crash-report product we're filing against.
const CRASH_PRODUCT_NAME: &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<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 self.proxy.is_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, &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(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>();
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>();
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");
}
}
}