1use anyhow::{Error, anyhow, format_err};
6use fuchsia_component::client::connect_to_protocol;
7use futures::channel::mpsc;
8use futures::stream::StreamExt;
9use injectable_time::TimeSource;
10use log::{error, warn};
11use std::cell::RefCell;
12use {fidl_fuchsia_feedback as fidl_feedback, fuchsia_async as fasync};
13
14const CRASH_PRODUCT_NAME: &str = "FuchsiaDetect";
16
17const CRASH_PROGRAM_NAME: &str = "triage_detect";
27
28#[derive(Debug)]
29pub struct SnapshotRequest {
30 signature: String,
31}
32
33impl SnapshotRequest {
34 pub fn new(signature: String) -> SnapshotRequest {
35 SnapshotRequest { signature }
36 }
37}
38
39const MAX_PENDING_CRASH_REPORTS: usize = 10;
45
46pub struct CrashReportHandlerBuilder<T: TimeSource> {
48 proxy: Option<fidl_feedback::CrashReporterProxy>,
49 max_pending_crash_reports: usize,
50 time_source: T,
51}
52
53#[macro_export]
55macro_rules! log_if_err {
56 ($result:expr, $log_prefix:expr) => {
57 if let Err(e) = $result.as_ref() {
58 log::error!("{}: {}", $log_prefix, e);
59 }
60 };
61}
62
63impl<T> CrashReportHandlerBuilder<T>
64where
65 T: TimeSource + 'static,
66{
67 pub fn new(time_source: T) -> Self {
68 Self { time_source, max_pending_crash_reports: MAX_PENDING_CRASH_REPORTS, proxy: None }
69 }
70
71 pub async fn build(self) -> Result<CrashReportHandler, Error> {
72 if self.proxy.is_none() {
75 let config_proxy =
76 connect_to_protocol::<fidl_feedback::CrashReportingProductRegisterMarker>()?;
77 let product_config = fidl_feedback::CrashReportingProduct {
78 name: Some(CRASH_PRODUCT_NAME.to_string()),
79 ..Default::default()
80 };
81 config_proxy.upsert_with_ack(CRASH_PROGRAM_NAME, &product_config).await?;
82 }
83 let proxy =
85 self.proxy.unwrap_or(connect_to_protocol::<fidl_feedback::CrashReporterMarker>()?);
86 Ok(CrashReportHandler::new(proxy, self.time_source, self.max_pending_crash_reports))
87 }
88}
89
90#[cfg(test)]
91impl<T> CrashReportHandlerBuilder<T>
92where
93 T: TimeSource,
94{
95 fn with_proxy(mut self, proxy: fidl_feedback::CrashReporterProxy) -> Self {
96 self.proxy = Some(proxy);
97 self
98 }
99
100 fn with_max_pending_crash_reports(mut self, max: usize) -> Self {
101 self.max_pending_crash_reports = max;
102 self
103 }
104}
105
106pub struct CrashReportHandler {
118 crash_report_sender: RefCell<mpsc::Sender<SnapshotRequest>>,
121 channel_size: usize,
122 _server_task: fasync::Task<()>,
123}
124
125impl CrashReportHandler {
126 fn new<T>(proxy: fidl_feedback::CrashReporterProxy, time_source: T, channel_size: usize) -> Self
127 where
128 T: TimeSource + 'static,
129 {
130 let (channel, receiver) = mpsc::channel(channel_size);
132 let server_task = Self::begin_crash_report_sender(proxy, receiver, time_source);
133 Self { channel_size, crash_report_sender: RefCell::new(channel), _server_task: server_task }
134 }
135
136 pub fn request_snapshot(&self, request: SnapshotRequest) -> Result<(), Error> {
139 match self.crash_report_sender.borrow_mut().try_send(request) {
142 Ok(()) => Ok(()),
143 Err(e) if e.is_full() => {
144 warn!("Too many crash reports pending: {e}");
145 Err(anyhow!("Pending crash reports exceeds max ({})", self.channel_size))
146 }
147 Err(e) => {
148 warn!("Error sending crash report: {e}");
149 Err(anyhow!("{e}"))
150 }
151 }
152 }
153
154 fn begin_crash_report_sender<T>(
158 proxy: fidl_feedback::CrashReporterProxy,
159 mut receive_channel: mpsc::Receiver<SnapshotRequest>,
160 time_source: T,
161 ) -> fasync::Task<()>
162 where
163 T: TimeSource + 'static,
164 {
165 fasync::Task::local(async move {
166 while let Some(request) = receive_channel.next().await {
167 log_if_err!(
168 Self::send_crash_report(&proxy, request, &time_source).await,
169 "Failed to file crash report"
170 );
171 }
172 error!(
173 "Crash reporter task ended. Crash reports will no longer be filed. This should not happen."
174 )
175 })
176 }
177
178 async fn send_crash_report<T: TimeSource>(
180 proxy: &fidl_feedback::CrashReporterProxy,
181 payload: SnapshotRequest,
182 time_source: &T,
183 ) -> Result<fidl_feedback::FileReportResults, Error> {
184 warn!("Filing crash report, signature '{}'", payload.signature);
185 let report = fidl_feedback::CrashReport {
186 program_name: Some(CRASH_PROGRAM_NAME.to_string()),
187 program_uptime: Some(time_source.now()),
188 crash_signature: Some(payload.signature),
189 is_fatal: Some(false),
190 ..Default::default()
191 };
192
193 let result = proxy.file_report(report).await.map_err(|e| format_err!("IPC error: {e}"))?;
194 result.map_err(|e| format_err!("Service error: {e:?}"))
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use assert_matches::assert_matches;
202 use futures::TryStreamExt;
203 use injectable_time::{FakeTime, IncrementingFakeTime};
204
205 #[fuchsia::test]
208 async fn test_crash_report_content() {
209 let crash_report_signature = "TestCrashReportSignature";
211
212 let (proxy, mut stream) =
214 fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>();
215 let fake_time = FakeTime::new();
216 fake_time.set_ticks(9876);
217 let crash_report_handler =
218 CrashReportHandlerBuilder::new(fake_time).with_proxy(proxy).build().await.unwrap();
219
220 crash_report_handler
222 .request_snapshot(SnapshotRequest::new(crash_report_signature.to_string()))
223 .unwrap();
224
225 if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder: _, report })) =
227 stream.try_next().await
228 {
229 assert_eq!(
230 report,
231 fidl_feedback::CrashReport {
232 program_name: Some(CRASH_PROGRAM_NAME.to_string()),
233 program_uptime: Some(9876),
234 crash_signature: Some(crash_report_signature.to_string()),
235 is_fatal: Some(false),
236 ..Default::default()
237 }
238 );
239 } else {
240 panic!("Did not receive a crash report");
241 }
242 }
243
244 #[fuchsia::test]
246 async fn test_crash_report_pending_reports() {
247 let (proxy, mut stream) =
250 fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>();
251 let fake_time = IncrementingFakeTime::new(1000, std::time::Duration::from_nanos(1000));
252 let crash_report_handler = CrashReportHandlerBuilder::new(fake_time)
253 .with_proxy(proxy)
254 .with_max_pending_crash_reports(1)
255 .build()
256 .await
257 .unwrap();
258
259 assert_matches!(
265 crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash1".to_string())),
266 Ok(())
267 );
268
269 assert_matches!(
272 crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash2".to_string())),
273 Ok(())
274 );
275
276 assert_matches!(
279 crash_report_handler.request_snapshot(SnapshotRequest::new("TestCrash3".to_string())),
280 Err(_)
281 );
282
283 if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder, report })) =
285 stream.try_next().await
286 {
287 let _ = responder.send(Ok(&fidl_feedback::FileReportResults::default()));
289 assert_eq!(
290 report,
291 fidl_feedback::CrashReport {
292 program_name: Some(CRASH_PROGRAM_NAME.to_string()),
293 program_uptime: Some(1000),
294 crash_signature: Some("TestCrash1".to_string()),
295 is_fatal: Some(false),
296 ..Default::default()
297 }
298 );
299 } else {
300 panic!("Did not receive a crash report");
301 }
302
303 if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder, report })) =
305 stream.try_next().await
306 {
307 let _ = responder.send(Ok(&fidl_feedback::FileReportResults::default()));
309 assert_eq!(
310 report,
311 fidl_feedback::CrashReport {
312 program_name: Some(CRASH_PROGRAM_NAME.to_string()),
313 program_uptime: Some(2000),
314 crash_signature: Some("TestCrash2".to_string()),
315 is_fatal: Some(false),
316 ..Default::default()
317 }
318 );
319 } else {
320 panic!("Did not receive a crash report");
321 }
322 }
323}