1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! This module encapsulates the report of a failure event.
//!
//! A `Report` contains the metadata collected about the event
//! to construct a helpful error message.

use backtrace::Backtrace;
use serde_derive::Serialize;
use std::borrow::Cow;
use std::error::Error;
use std::fmt::Write as FmtWrite;
use std::mem;
use std::{env, fs::File, io::Write, path::Path, path::PathBuf};
use uuid::Uuid;

/// Method of failure.
#[derive(Debug, Serialize, Clone, Copy)]
pub enum Method {
  /// Failure caused by a panic.
  Panic,
}

/// Contains metadata about the crash like the backtrace and
/// information about the crate and operating system. Can
/// be used to be serialized and persisted or printed as
/// information to the user.
#[derive(Debug, Serialize)]
pub struct Report {
  name: String,
  operating_system: Cow<'static, str>,
  crate_version: String,
  explanation: String,
  cause: String,
  method: Method,
  backtrace: String,
}

impl Report {
  /// Create a new instance.
  pub fn new(
    name: &str,
    version: &str,
    method: Method,
    explanation: String,
    cause: String,
  ) -> Self {
    let operating_system = if cfg!(windows) {
      "windows".into()
    } else {
      let platform = os_type::current_platform();
      format!("unix:{:?}", platform.os_type).into()
    };

    //We skip 3 frames from backtrace library
    //Then we skip 3 frames for our own library
    //(including closure that we set as hook)
    //Then we skip 2 functions from Rust's runtime
    //that calls panic hook
    const SKIP_FRAMES_NUM: usize = 8;
    //We take padding for address and extra two letters
    //to padd after index.
    const HEX_WIDTH: usize = mem::size_of::<usize>() + 2;
    //Padding for next lines after frame's address
    const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;

    let mut backtrace = String::new();

    //Here we iterate over backtrace frames
    //(each corresponds to function's stack)
    //We need to print its address
    //and symbol(e.g. function name),
    //if it is available
    for (idx, frame) in Backtrace::new()
      .frames()
      .iter()
      .skip(SKIP_FRAMES_NUM)
      .enumerate()
    {
      let ip = frame.ip();
      let _ = write!(backtrace, "\n{:4}: {:2$?}", idx, ip, HEX_WIDTH);

      let symbols = frame.symbols();
      if symbols.is_empty() {
        let _ = write!(backtrace, " - <unresolved>");
        continue;
      }

      for (idx, symbol) in symbols.iter().enumerate() {
        //Print symbols from this address,
        //if there are several addresses
        //we need to put it on next line
        if idx != 0 {
          let _ = write!(backtrace, "\n{:1$}", "", NEXT_SYMBOL_PADDING);
        }

        if let Some(name) = symbol.name() {
          let _ = write!(backtrace, " - {}", name);
        } else {
          let _ = write!(backtrace, " - <unknown>");
        }

        //See if there is debug information with file name and line
        if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) {
          let _ = write!(
            backtrace,
            "\n{:3$}at {}:{}",
            "",
            file.display(),
            line,
            NEXT_SYMBOL_PADDING
          );
        }
      }
    }

    Self {
      crate_version: version.into(),
      name: name.into(),
      operating_system,
      method,
      explanation,
      cause,
      backtrace,
    }
  }

  /// Serialize the `Report` to a TOML string.
  pub fn serialize(&self) -> Option<String> {
    toml::to_string_pretty(&self).ok()
  }

  /// Write a file to disk.
  pub fn persist(&self) -> Result<PathBuf, Box<dyn Error + 'static>> {
    let uuid = Uuid::new_v4().to_hyphenated().to_string();
    let tmp_dir = env::temp_dir();
    let file_name = format!("report-{}.toml", &uuid);
    let file_path = Path::new(&tmp_dir).join(file_name);
    let mut file = File::create(&file_path)?;
    let toml = self.serialize().unwrap();
    file.write_all(toml.as_bytes())?;
    Ok(file_path)
  }
}