diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2017-10-26 14:52:14 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2017-10-26 14:52:14 +0200 |
| commit | a9379bf7c5628abdac284857c200e9f19346ef29 (patch) | |
| tree | d9b47faa7938452c318fdeca928bafab837400ed | |
| parent | b9e33f5bb1b76d95174ef59077a9851af9f646c2 (diff) | |
| parent | b0e3b9b872f3bfe3420a3e4a2063923b51f3d9f7 (diff) | |
| download | mullvadvpn-a9379bf7c5628abdac284857c200e9f19346ef29.tar.xz mullvadvpn-a9379bf7c5628abdac284857c200e9f19346ef29.zip | |
Merge branch 'problem-report'
| -rw-r--r-- | mullvad-daemon/Cargo.toml | 4 | ||||
| -rw-r--r-- | mullvad-daemon/src/bin/problem-report.rs | 200 |
2 files changed, 204 insertions, 0 deletions
diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 5f4d48ee58..6dbd3430b0 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -10,6 +10,10 @@ build = "build.rs" name = "mullvadd" path = "src/main.rs" +[[bin]] +name = "problem-report" +path = "src/bin/problem-report.rs" + [dependencies] app_dirs = "1.1" chrono = { version = "0.4", features = ["serde"] } diff --git a/mullvad-daemon/src/bin/problem-report.rs b/mullvad-daemon/src/bin/problem-report.rs new file mode 100644 index 0000000000..a4b5107dec --- /dev/null +++ b/mullvad-daemon/src/bin/problem-report.rs @@ -0,0 +1,200 @@ +//! # License +//! +//! Copyright (C) 2017 Amagicom AB +//! +//! This program is free software: you can redistribute it and/or modify it under the terms of the +//! GNU General Public License as published by the Free Software Foundation, either version 3 of +//! the License, or (at your option) any later version. + +#[macro_use] +extern crate clap; +#[macro_use] +extern crate error_chain; + +use error_chain::ChainedError; +use std::cmp::min; +use std::fmt; +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; + +/// Maximum number of bytes to read from each log file +const LOG_MAX_READ_BYTES: usize = 5 * 1024 * 1024; + +/// Field delimeter in generated problem report +const LOG_DELIMITER: &'static str = "===================="; + +error_chain!{ + errors { + WriteReportError(path: PathBuf) { + description("Error writing the problem report file") + display("Error writing the problem report file: {}", path.to_string_lossy()) + } + ReadLogError(path: PathBuf) { + description("Error reading the contents of log file") + display("Error reading the contents of log file: {}", path.to_string_lossy()) + } + } +} + +quick_main!(run); + +fn run() -> Result<()> { + let app = clap::App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + .setting(clap::AppSettings::SubcommandRequired) + .subcommand( + clap::SubCommand::with_name("collect") + .about("Collect problem report") + .arg( + clap::Arg::with_name("output") + .long("output") + .short("o") + .takes_value(true) + .help("The destination path for saving the collected report.") + .required(true), + ) + .arg( + clap::Arg::with_name("logs") + .help("The paths to log files to include in the problem report.") + .multiple(true) + .takes_value(true) + .required(false), + ), + ) + .subcommand( + clap::SubCommand::with_name("send") + .about("Send collected problem report") + .arg( + clap::Arg::with_name("report") + .long("report") + .short("r") + .help("The path to previously collected report file.") + .takes_value(true) + .required(true), + ) + .arg( + clap::Arg::with_name("email") + .long("email") + .short("e") + .help("Reporter's email") + .takes_value(true) + .required(false), + ) + .arg( + clap::Arg::with_name("message") + .long("message") + .short("m") + .help("Reporter's message") + .takes_value(true) + .required(false), + ), + ); + + let matches = app.get_matches(); + + if let Some(collect_matches) = matches.subcommand_matches("collect") { + let log_paths = values_t!(collect_matches.values_of("logs"), String) + .unwrap_or(Vec::new()) + .iter() + .map(PathBuf::from) + .collect(); + let output_path = value_t_or_exit!(collect_matches.value_of("output"), String); + collect_report(log_paths, PathBuf::from(output_path)) + } else if let Some(send_matches) = matches.subcommand_matches("send") { + let report_path = value_t_or_exit!(send_matches.value_of("report"), String); + let user_email = value_t!(send_matches.value_of("email"), String).unwrap_or(String::new()); + let user_message = + value_t!(send_matches.value_of("message"), String).unwrap_or(String::new()); + send_problem_report(user_email, user_message, PathBuf::from(report_path)) + } else { + unreachable!("No sub command given"); + } +} + +fn collect_report(log_paths: Vec<PathBuf>, save_path: PathBuf) -> Result<()> { + let mut problem_report = ProblemReport::default(); + for log_path in log_paths.into_iter() { + problem_report.add_file_log(log_path); + } + write_problem_report(&save_path, problem_report) + .chain_err(|| ErrorKind::WriteReportError(save_path.clone())) +} + +fn send_problem_report( + _user_email: String, + _user_message: String, + _report_path: PathBuf, +) -> Result<()> { + // TODO: Implement submission to master + Ok(()) +} + +fn write_problem_report(path: &Path, problem_report: ProblemReport) -> io::Result<()> { + let mut file = File::create(path)?; + let mut permissions = file.metadata()?.permissions(); + permissions.set_readonly(true); + file.set_permissions(permissions)?; + file.write(problem_report.to_string().as_bytes())?; + Ok(()) +} + +#[derive(Debug, Default)] +struct ProblemReport { + logs: Vec<(String, String)>, +} + +impl ProblemReport { + /// Attach file log to this report + /// Unlike `try_add_file_log` this method uses the error description + /// instead of log contents if error occurred when reading log file. + fn add_file_log(&mut self, path: PathBuf) { + if let Err(e) = self.try_add_file_log(&path) + .chain_err(|| ErrorKind::ReadLogError(path.clone())) + { + self.logs.push(( + path.to_string_lossy().into_owned(), + e.display_chain().to_string(), + )); + } + } + + /// Try reading log from file source and attach it to this report + fn try_add_file_log(&mut self, path: &Path) -> io::Result<()> { + Ok(self.logs.push(( + path.to_string_lossy().into_owned(), + Self::read_log_file(path, LOG_MAX_READ_BYTES)?, + ))) + } + + /// Private helper to safely read the given number of bytes off the tail of UTF-8 log file + /// and return it as a string + fn read_log_file(path: &Path, max_bytes: usize) -> io::Result<String> { + let mut file = File::open(path)?; + let file_size = file.metadata()?.len(); + + if file_size > max_bytes as u64 { + file.seek(SeekFrom::Start(file_size - max_bytes as u64))?; + } + + let capacity = min(file_size, max_bytes as u64) as usize; + let mut buffer = Vec::with_capacity(capacity); + file.take(max_bytes as u64).read_to_end(&mut buffer)?; + Ok(String::from_utf8_lossy(&buffer).into_owned()) + } +} + +impl fmt::Display for ProblemReport { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + for &(ref label, ref content) in &self.logs { + writeln!(fmt, "{}", LOG_DELIMITER)?; + writeln!(fmt, "Log: {}", label)?; + writeln!(fmt, "{}", LOG_DELIMITER)?; + fmt.write_str(content)?; + writeln!(fmt)?; + } + Ok(()) + } +} |
