1use anyhow::{format_err, Error};
6use fidl_fuchsia_feedback::{
7 Annotation, CrashReporterMarker, CrashReporterProxy, FileReportResults,
8};
9use fuchsia_component::client::connect_to_protocol;
10use futures::channel::mpsc;
11use futures::stream::StreamExt;
12use std::cell::RefCell;
13use std::rc::Rc;
14use {fidl_fuchsia_feedback as fidl_feedback, fuchsia_async as fasync};
15
16#[macro_export]
17macro_rules! send_report {
18 ($rpt:expr,$error:expr) => {
21 match $rpt {
22 Some(reporter) => {
23 if let Err(error) = reporter.file_crash_report($error, None).await {
24 println!("Failed to send crash report: {}", error);
25 }
26 }
27 None => {
28 println!("Crash reporter not available.");
29 }
30 }
31 };
32 ($rpt:expr,$error:expr,$ctx:expr) => {
35 match $rpt {
36 Some(reporter) => {
37 if let Err(error) = reporter.file_crash_report($error, Some($ctx.to_string())).await
38 {
39 println!("Failed to send crash report: {}", error);
40 }
41 }
42 None => {
43 println!("Crash reporter not available.");
44 }
45 }
46 };
47}
48
49#[derive(thiserror::Error, Debug)]
50pub enum RecoveryError {
51 #[error("fuchsia-recovery-generic-error")]
52 GenericError(),
53
54 #[error("fuchsia-recovery-ota-failure-error")]
55 OtaFailureError(),
56
57 #[error("fuchsia-recovery-factory-reset-policy-failure")]
58 FdrPolicyError(),
59
60 #[error("fuchsia-recovery-factory-reset-failure")]
61 FdrResetError(),
62
63 #[error("fuchsia-recovery-wifi-connection-error")]
64 WifiConnectionError(),
65
66 #[error("fuchsia-recovery-wifi-connection-success")]
67 WifiConnectionSuccess(),
68
69 #[error("fuchsia-recovery-reports-exceed-limit")]
70 OutOfSpace(),
71
72 #[cfg(test)]
73 #[error("{}", .0)]
74 TestingError(String),
75}
76
77const SIGNATURE_MAX_LENGTH: usize = 128;
78const ANNOTATION_MAX_LENGTH: usize = 1024;
79const MAX_PENDING_CRASH_REPORTS: usize = 5;
80
81type ProxyFn = Box<dyn Fn() -> Result<CrashReporterProxy, Error>>;
82
83pub struct CrashReportBuilder {
85 proxy_fn: ProxyFn,
86 max_pending_crash_reports: usize,
87}
88
89impl CrashReportBuilder {
90 pub fn new() -> Self {
91 Self {
92 proxy_fn: Box::new(default_proxy_fn),
93 max_pending_crash_reports: MAX_PENDING_CRASH_REPORTS,
94 }
95 }
96
97 #[cfg(test)]
98 pub fn with_proxy_fn(mut self, proxy: ProxyFn) -> Self {
99 self.proxy_fn = proxy;
100 self
101 }
102
103 #[cfg(test)]
104 pub fn with_max_pending_crash_reports(mut self, max: usize) -> Self {
105 self.max_pending_crash_reports = max;
106 self
107 }
108
109 pub fn build(self) -> Result<Rc<CrashReporter>, Error> {
111 let (channel, receiver) = mpsc::channel(self.max_pending_crash_reports);
112 CrashReporter::begin_crash_report_sender(self.proxy_fn, receiver);
113 Ok(Rc::new(CrashReporter { crash_report_sender: RefCell::new(channel) }))
114 }
115}
116
117pub fn default_proxy_fn() -> Result<CrashReporterProxy, Error> {
118 connect_to_protocol::<CrashReporterMarker>()
119}
120
121pub struct CrashReporter {
122 crash_report_sender: RefCell<mpsc::Sender<ErrorReportMessage>>,
124}
125
126pub struct ErrorReportMessage {
127 error: RecoveryError,
128 context: Option<String>,
129}
130
131impl CrashReporter {
132 const DEFAULT_PROGRAM_NAME: &'static str = "recovery";
133
134 pub async fn file_crash_report(
136 &self,
137 error: RecoveryError,
138 context: Option<String>,
139 ) -> Result<(), RecoveryError> {
140 let message = ErrorReportMessage { error, context };
141 match self.crash_report_sender.borrow_mut().try_send(message) {
142 Ok(()) => Ok(()),
143 Err(e) if e.is_full() => Err(RecoveryError::OutOfSpace()),
144 Err(_) => Err(RecoveryError::GenericError()),
145 }
146 }
147
148 fn begin_crash_report_sender(
152 proxy_fn: ProxyFn,
153 mut receive_channel: mpsc::Receiver<ErrorReportMessage>,
154 ) {
155 fasync::Task::local(async move {
156 while let Some(msg) = receive_channel.next().await {
157 let ctx_string = match msg.context {
158 Some(ctx) => ctx.to_string(),
159 None => "".to_string(),
160 };
161 match Self::send_crash_report(&proxy_fn, msg.error, ctx_string).await {
162 Err(e) => eprintln!("Failed to send crash report: {:?}", e),
163 Ok(_) => (),
164 }
165 }
166 })
167 .detach();
168 }
169
170 async fn send_crash_report(
172 proxy_fn: &ProxyFn,
173 error: RecoveryError,
174 context: String,
175 ) -> Result<FileReportResults, Error> {
176 let mut signature = error.to_string();
177 let mut ctx = context.clone();
178 ctx.truncate(ANNOTATION_MAX_LENGTH);
179 signature.truncate(SIGNATURE_MAX_LENGTH);
180 let report = fidl_feedback::CrashReport {
181 program_name: Some(CrashReporter::DEFAULT_PROGRAM_NAME.to_string()),
182 crash_signature: Some(signature),
183 annotations: Some(vec![Annotation { key: "context".into(), value: ctx }]),
184 is_fatal: Some(false),
185 ..Default::default()
186 };
187 let result =
188 proxy_fn()?.file_report(report).await.map_err(|e| format_err!("IPC error: {}", e))?;
189 result.map_err(|e| format_err!("Service error: {:?}", e))
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use assert_matches::assert_matches;
197 use futures::TryStreamExt;
198
199 fn gen_string(c: char, length: usize) -> String {
201 let mut count: usize = 0;
202 let mut res = String::new();
203 loop {
204 res.push(c);
205 count += 1;
206 if count == length {
207 return res;
208 }
209 }
210 }
211
212 #[fasync::run_singlethreaded(test)]
213 async fn test_crash_report_content() {
214 let received_error = RecoveryError::FdrResetError();
215
216 let (proxy, mut stream) = fidl::endpoints::create_proxy_and_stream::<
218 fidl_fuchsia_feedback::CrashReporterMarker,
219 >();
220
221 let crash_reporter = CrashReportBuilder::new()
222 .with_proxy_fn(Box::new(move || Ok(proxy.clone())))
223 .build()
224 .unwrap();
225
226 crash_reporter
227 .file_crash_report(received_error, Some("test context".into()))
228 .await
229 .unwrap();
230
231 if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder: _, report })) =
233 stream.try_next().await
234 {
235 assert_eq!(
236 report,
237 fidl_feedback::CrashReport {
238 program_name: Some("recovery".to_string()),
239 crash_signature: Some("fuchsia-recovery-factory-reset-failure".to_string()),
240 is_fatal: Some(false),
241 annotations: Some(vec![Annotation {
242 key: "context".into(),
243 value: "test context".into()
244 },]),
245 ..Default::default()
246 }
247 );
248 } else {
249 panic!("Did not receive a crash report");
250 }
251 }
252
253 #[fasync::run_singlethreaded(test)]
254 async fn test_crash_report_string_limits() {
255 let signature = gen_string('A', SIGNATURE_MAX_LENGTH + 10);
256 let context = gen_string('Z', ANNOTATION_MAX_LENGTH + 10);
257 let received_error = RecoveryError::TestingError(signature);
258
259 let (proxy, mut stream) = fidl::endpoints::create_proxy_and_stream::<
261 fidl_fuchsia_feedback::CrashReporterMarker,
262 >();
263
264 let crash_reporter = CrashReportBuilder::new()
265 .with_proxy_fn(Box::new(move || Ok(proxy.clone())))
266 .build()
267 .unwrap();
268
269 crash_reporter.file_crash_report(received_error, Some(context)).await.unwrap();
270
271 if let Ok(Some(fidl_feedback::CrashReporterRequest::FileReport { responder: _, report })) =
273 stream.try_next().await
274 {
275 assert_eq!(
276 report,
277 fidl_feedback::CrashReport {
278 program_name: Some("recovery".to_string()),
279 crash_signature: Some(gen_string('A', SIGNATURE_MAX_LENGTH)),
280 is_fatal: Some(false),
281 annotations: Some(vec![Annotation {
282 key: "context".into(),
283 value: gen_string('Z', ANNOTATION_MAX_LENGTH)
284 },]),
285 ..Default::default()
286 }
287 );
288 } else {
289 panic!("Did not receive a crash report");
290 }
291 }
292
293 #[test]
294 fn test_crash_pending_reports() {
295 let mut exec = fasync::TestExecutor::new();
296 let (proxy, _stream) = fidl::endpoints::create_proxy_and_stream::<
297 fidl_fuchsia_feedback::CrashReporterMarker,
298 >();
299
300 let crash_reporter = CrashReportBuilder::new()
301 .with_proxy_fn(Box::new(move || Ok(proxy.clone())))
302 .with_max_pending_crash_reports(1)
303 .build()
304 .unwrap();
305
306 exec.run_singlethreaded(async {
308 assert_matches!(
310 crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
311 Ok(())
312 );
313
314 assert_matches!(
316 crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
317 Ok(())
318 );
319
320 assert_matches!(
322 crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
323 Err(RecoveryError::OutOfSpace())
324 );
325 });
326 }
327}