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::{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    // Send a crash report with the provided RecoveryError
19    // Option<Rc<CrashReporter>>, RecoveryError
20    ($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    // Send a crash report with the provided RecoveryError and additional error context
33    // Option<Rc<CrashReporter>>, RecoveryError, Error
34    ($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
83/// A builder for setting up a crash reporter.
84pub 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    /// Set up the crash report sender that runs asynchronously
110    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    /// The channel to send new crash report requests to the async crash report sender.
123    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    /// Attempt to send a crash report with the given error and an optional context.
135    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    /// Spawn an infinite future that receives crash report signatures over the channel and uses the
149    /// proxy to send a File FIDL request to the CrashReporter service with the specified
150    /// signatures.
151    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    /// Send a File request to the CrashReporter service with the specified crash report signature.
171    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    // Generate a string with a specific size for testing.
200    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        // Set up a proxy to receive error messages
217        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        // Verify the fake service receives the crash report with expected data
232        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        // Set up a proxy to receive error messages
260        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        // Verify the fake service receives the crash report with expected data
272        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        // The request stream is never serviced here, so we can verify the number of pending requests.
307        exec.run_singlethreaded(async {
308            // Ensure the first report is successfully queued
309            assert_matches!(
310                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
311                Ok(())
312            );
313
314            // Ensure the second report is queued as the first is being processed.
315            assert_matches!(
316                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
317                Ok(())
318            );
319
320            // Ensure the third report is rejected due to the maximum pending limit.
321            assert_matches!(
322                crash_reporter.file_crash_report(RecoveryError::FdrResetError(), None).await,
323                Err(RecoveryError::OutOfSpace())
324            );
325        });
326    }
327}