Skip to main content

recovery_util/
crash.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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    // Send a crash report with the provided RecoveryError
20    // Option<Rc<CrashReporter>>, RecoveryError
21    ($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    // Send a crash report with the provided RecoveryError and additional error context
34    // Option<Rc<CrashReporter>>, RecoveryError, Error
35    ($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
84/// A builder for setting up a crash reporter.
85pub 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    /// Set up the crash report sender that runs asynchronously
111    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    /// The channel to send new crash report requests to the async crash report sender.
124    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    /// Attempt to send a crash report with the given error and an optional context.
136    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    /// Spawn an infinite future that receives crash report signatures over the channel and uses the
150    /// proxy to send a File FIDL request to the CrashReporter service with the specified
151    /// signatures.
152    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    /// Send a File request to the CrashReporter service with the specified crash report signature.
172    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    // Generate a string with a specific size for testing.
201    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        // Set up a proxy to receive error messages
218        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        // Verify the fake service receives the crash report with expected data
233        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        // Set up a proxy to receive error messages
261        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        // Verify the fake service receives the crash report with expected data
273        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        // The request stream is never serviced here, so we can verify the number of pending requests.
308        exec.run_singlethreaded(async {
309            // Ensure the first report is successfully queued
310            assert_matches!(
311                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
312                Ok(())
313            );
314
315            // Ensure the second report is queued as the first is being processed.
316            assert_matches!(
317                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
318                Ok(())
319            );
320
321            // Ensure the third report is rejected due to the maximum pending limit.
322            assert_matches!(
323                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
324                Err(RecoveryError::OutOfSpace())
325            );
326        });
327    }
328}