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