Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  logic.rs

  Sprache: Rust
 

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


//! Business logic for the crash reporter.

use crate::std::{
    cell::RefCell,
    path::{Path, PathBuf},
    sync::{
        atomic::{AtomicBool, Ordering::Relaxed},
        Arc,
    },
};
use crate::{
    async_task::{async_scoped_thread, block_on, AsyncTask},
    config::Config,
    memory_test::child::Memtest,
    net,
    settings::Settings,
    std,
    ui::{ReportCrashUI, ReportCrashUIState, SubmitState},
};
use anyhow::Context;

pub mod annotations;

/// The main crash reporting logic.
pub struct ReportCrash {
    pub settings: RefCell<Settings>,
    config: Arc<Config>,
    extra: serde_json::Value,
    settings_file: PathBuf,
    attempted_to_send: AtomicBool,
    ui: Option<AsyncTask<ReportCrashUIState>>,
    memtest: RefCell<Option<Memtest>>,
}

fn modify_extra_for_report(extra: &mut serde_json::Value) {
    if let Some(map) = extra.as_object_mut() {
        map.retain(|k, _| annotations::send_in_report(k));
    }

    extra["SubmittedFrom"] = "Client".into();
    extra["Throttleable"] = "1".into();
}

pub fn sha256_hash_file(path: &Path) -> crate::std::io::Result<String> {
    let hash = {
        use sha2::{Digest, Sha256};
        let mut file = std::fs::File::open(path)?;
        let mut hasher = Sha256::new();
        std::io::copy(&mut file, &mut hasher)?;
        hasher.finalize()
    };

    let mut s = String::with_capacity(hash.len() * 2);
    for byte in hash {
        use crate::std::fmt::Write;
        write!(s, "{:02x}", byte).unwrap();
    }

    Ok(s)
}

impl ReportCrash {
    pub fn new(config: Arc<Config>, extra: serde_json::Value) -> anyhow::Result<Self> {
        let settings_file = config.data_dir().join("crashreporter_settings.json");
        let settings: Settings = match std::fs::File::open(&settings_file) {
            Err(e) => {
                if e.kind() != std::io::ErrorKind::NotFound {
                    log::warn!(
                        "failed to open settings file ({}): {e}",
                        settings_file.display()
                    );
                }
                Default::default()
            }
            Ok(f) => match Settings::from_reader(f) {
                Err(e) => {
                    log::warn!(
                        "failed to read settings file ({}): {e}",
                        settings_file.display()
                    );
                    Default::default()
                }
                Ok(s) => s,
            },
        };
        log::debug!("loaded settings: {settings:?}");

        Ok(ReportCrash {
            config,
            extra,
            settings_file,
            settings: settings.into(),
            attempted_to_send: Default::default(),
            ui: None,
            memtest: None.into(),
        })
    }

    /// Returns whether an attempt was made to send the report.
    pub fn run(mut self) -> anyhow::Result<bool> {
        self.memtest_according_to_settings();
        self.set_log_file();
        self.set_extra_context();
        let hash = self.compute_minidump_hash();
        self.send_crash_ping();
        if let Err(e) = self.update_events_file(hash.as_deref()) {
            log::warn!("failed to update events file: {e:#}");
        }
        self.check_eol_version()?;

        if !self.config.auto_submit {
            self.run_ui();
        } else {
            anyhow::ensure!(
                block_on(self.try_send()).unwrap_or(false),
                "failed to send report"
            );
        }

        Ok(self.attempted_to_send.load(Relaxed))
    }

    /// Set the log file based on the current configured paths.
    ///
    /// This is the earliest that this can occur as the configuration data dir may be set based on
    /// fields in the extra file.
    fn set_log_file(&self) {
        if let Some(log_target) = &self.config.log_target {
            log_target.set_file(&self.config.data_dir().join("submit.log"));
        }
    }

    /// Set the process type and crash time, if not already set.
    ///
    /// NOTE: this assumes that the crashreporter client (in the default mode) is only used to send
    /// main process crashes.
    fn set_extra_context(&mut self) {
        let Some(obj) = self.extra.as_object_mut() else {
            log::error!("expected extra data to be an object");
            return;
        };
        obj.entry("ProcessType").or_insert("main".into());
        if !obj.contains_key("CrashTime") {
            if let Ok(time) = crate::std::time::SystemTime::now()
                .duration_since(crate::std::time::SystemTime::UNIX_EPOCH)
            {
                obj.insert("CrashTime".into(), time.as_secs().to_string().into());
            }
        }
    }

    /// Compute the SHA256 hash of the minidump file contents, set it in the extra file, and return
    /// it as a hex string.
    fn compute_minidump_hash(&mut self) -> Option<String> {
        match sha256_hash_file(self.config.dump_file()) {
            Ok(hash) => {
                self.extra["MinidumpSha256Hash"] = hash.clone().into();
                Some(hash)
            }
            Err(e) => {
                log::warn!("failed to compute minidump hash: {e:#}");
                None
            }
        }
    }

    /// Send crash pings to legacy telemetry and Glean.
    fn send_crash_ping(&self) {
        net::ping::CrashPing {
            extra: &self.extra,
            reason: Some("crash"),
        }
        .send()
    }

    /// Update the events file with information about the crash ping, minidump hash, and
    /// stacktraces.
    fn update_events_file(&self, minidump_hash: Option<&str>) -> anyhow::Result<()> {
        use crate::std::io::{BufRead, Error, ErrorKind, Write};
        struct EventsFile {
            event_version: String,
            time: String,
            uuid: String,
            pub data: serde_json::Value,
        }

        impl EventsFile {
            pub fn parse(mut reader: impl BufRead) -> std::io::Result<Self> {
                let mut lines = (&mut reader).lines();

                let mut read_field = move |name: &str| -> std::io::Result<String> {
                    lines.next().transpose()?.ok_or_else(|| {
                        Error::new(ErrorKind::InvalidData, format!("missing {name} field"))
                    })
                };

                let event_version = read_field("event version")?;
                let time = read_field("time")?;
                let uuid = read_field("uuid")?;
                let data = serde_json::from_reader(reader)?;
                Ok(EventsFile {
                    event_version,
                    time,
                    uuid,
                    data,
                })
            }

            pub fn write(&selfmut writer: impl Write) -> std::io::Result<()> {
                writeln!(writer, "{}"self.event_version)?;
                writeln!(writer, "{}"self.time)?;
                writeln!(writer, "{}"self.uuid)?;
                serde_json::to_writer(writer, &self.data)?;
                Ok(())
            }
        }

        let Some(events_dir) = &self.config.events_dir else {
            log::warn!("not updating the events file; no events directory configured");
            return Ok(());
        };

        let event_path = events_dir.join(self.config.local_dump_id().as_ref());

        // Read events file.
        let file = std::fs::File::open(&event_path)
            .with_context(|| format!("failed to open event file at {}", event_path.display()))?;

        let mut events_file =
            EventsFile::parse(std::io::BufReader::new(file)).with_context(|| {
                format!(
                    "failed to parse events file contents in {}",
                    event_path.display()
                )
            })?;

        // Update events file fields.
        if let Some(hash) = minidump_hash {
            events_file.data["MinidumpSha256Hash"] = hash.into();
        }
        events_file.data["StackTraces"] = self.extra["StackTraces"].clone();

        // Write altered events file.
        let file = std::fs::File::create(&event_path).with_context(|| {
            format!("failed to truncate event file at {}", event_path.display())
        })?;

        events_file
            .write(file)
            .with_context(|| format!("failed to write event file at {}", event_path.display()))
    }

    /// Check whether the version of the software that generated the crash is EOL.
    fn check_eol_version(&self) -> anyhow::Result<()> {
        if let Some(version) = self.extra["Version"].as_str() {
            if self.config.version_eol_file(version).exists() {
                self.config.delete_files();
                anyhow::bail!(self.config.string("crashreporter-error-version-eol"));
            }
        }
        Ok(())
    }

    /// Save the current settings.
    fn save_settings(&self) {
        let result: anyhow::Result<()> = (|| {
            Ok(self
                .settings
                .borrow()
                .to_writer(std::fs::File::create(&self.settings_file)?)?)
        })();
        if let Err(e) = result {
            log::error!("error while saving settings: {e}");
        }
    }

    /// Handle a response from submitting a crash report.
    ///
    /// Returns the crash ID to use for the recorded submission event. Errors in this function may
    /// result in None being returned to consider the crash report submission as a failure even
    /// though the server did provide a response.
    fn handle_crash_report_response(
        &self,
        response: net::report::Response,
    ) -> anyhow::Result<Option<String>> {
        if let Some(version) = response.stop_sending_reports_for {
            // Create the EOL version file. The content seemingly doesn't matter, but we mimic what
            // was written by the old crash reporter.
            if let Err(e) = std::fs::write(self.config.version_eol_file(&version), "1\n") {
                log::warn!("failed to write EOL file: {e}");
            }
        }

        if response.discarded {
            log::debug!("response indicated that the report was discarded");
            return Ok(None);
        }

        let Some(crash_id) = response.crash_id else {
            log::debug!("response did not provide a crash id");
            return Ok(None);
        };

        // Write the id to the `submitted` directory
        let submitted_dir = self.config.submitted_crash_dir();
        std::fs::create_dir_all(&submitted_dir).with_context(|| {
            format!(
                "failed to create submitted crash directory {}",
                submitted_dir.display()
            )
        })?;

        let crash_id_file = submitted_dir.join(format!("{crash_id}.txt"));

        let mut file = std::fs::File::create(&crash_id_file).with_context(|| {
            format!(
                "failed to create submitted crash file for {crash_id} ({})",
                crash_id_file.display()
            )
        })?;

        // Shadow `std::fmt::Write` to use the correct trait below.
        use crate::std::io::Write;

        if let Err(e) = writeln!(
            &mut file,
            "{}",
            self.config
                .build_string("crashreporter-crash-identifier")
                .arg("id", &crash_id)
                .get()
        ) {
            log::warn!(
                "failed to write to submitted crash file ({}) for {crash_id}: {e}",
                crash_id_file.display()
            );
        }

        if let Some(url) = response.view_url {
            if let Err(e) = writeln!(
                &mut file,
                "{}",
                self.config
                    .build_string("crashreporter-crash-details")
                    .arg("url", url)
                    .get()
            ) {
                log::warn!(
                    "failed to write view url to submitted crash file ({}) for {crash_id}: {e}",
                    crash_id_file.display()
                );
            }
        }

        Ok(Some(crash_id))
    }

    /// Write the submission event.
    ///
    /// A `None` crash_id indicates that the submission failed.
    fn write_submission_event(&self, crash_id: Option<String>) -> anyhow::Result<()> {
        let Some(events_dir) = &self.config.events_dir else {
            // If there's no events dir, don't do anything.
            return Ok(());
        };

        let local_id = self.config.local_dump_id();
        let event_path = events_dir.join(format!("{local_id}-submission"));

        let unix_epoch_seconds = std::time::SystemTime::now()
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .expect("system time is before the unix epoch")
            .as_secs();
        std::fs::write(
            &event_path,
            format!(
                "crash.submission.1\n{unix_epoch_seconds}\n{local_id}\n{}\n{}",
                crash_id.is_some(),
                crash_id.as_deref().unwrap_or("")
            ),
        )
        .with_context(|| format!("failed to write event to {}", event_path.display()))
    }

    /// Restart the program.
    fn restart_process(&self) {
        self.config.restart_process();
    }

    /// Run the crash reporting UI.
    fn run_ui(&mut self) {
        use crate::std::{sync::mpsc, thread};

        let (logic_send, logic_recv) = mpsc::channel();
        let logic_remote_queue = AsyncTask::new(move |f| {
            // This is best-effort: ignore errors.
            let _ = logic_send.send(f);
        });

        let crash_ui = ReportCrashUI::new(
            &*self.settings.borrow(),
            self.config.clone(),
            logic_remote_queue.weak(),
        );

        // Set the UI remote queue.
        let crash_ui_async_task = crash_ui.async_task();
        struct PanicHandler(AsyncTask<ReportCrashUIState>);
        impl Drop for PanicHandler {
            fn drop(&mut self) {
                self.0.push(|_| panic!("logic thread panicked"));
            }
        }
        let logic_panic_handler = PanicHandler(crash_ui_async_task.weak());
        self.ui = Some(crash_ui_async_task);

        // Spawn a separate thread to handle all interactions with `self`. This prevents blocking
        // the UI for any reason.

        // Use a barrier to ensure both threads are live before either starts (ensuring they
        // can immediately queue work for each other).
        let barrier = std::sync::Barrier::new(2);
        let barrier = &barrier;
        thread::scope(move |s| {
            // Move `logic_remote_queue` into this scope so that it will drop when the scope
            // completes (which will drop the `mpsc::Sender` and cause the logic thread to complete
            // and join when the UI finishes so the scope can exit).
            let _logic_remote_queue = logic_remote_queue;
            s.spawn(move || {
                let _logic_panic_handler = logic_panic_handler;
                barrier.wait();
                while let Ok(f) = logic_recv.recv() {
                    f(self);
                }

                // Clear the UI remote queue, using it after this point is an error. This also
                // prevents the panic handler from engaging.
                //
                // NOTE we do this here because the compiler can't reason about `self` being safely
                // accessible after `thread::scope` returns. This is effectively the same result
                // since the above loop will only exit when `logic_send` is dropped at the end of
                // the scope.
                self.ui = None;

                // Save settings after UI is closed
                self.save_settings();
            });

            barrier.wait();
            crash_ui.run()
        });
    }

    /// Overwrite the extra file with the given content.
    fn update_extra_file(&self, content: &serde_json::Value) -> anyhow::Result<()> {
        let Some(path) = self.config.extra_file() else {
            return Ok(());
        };
        let f = std::fs::File::create(&path)
            .with_context(|| format!("failed to truncate {}", path.display().to_string()))?;
        serde_json::to_writer(f, content)
            .with_context(|| format!("failed to overwrite {}", path.display().to_string()))
    }

    /// Start or stop a memtest if settings dictate it.
    fn memtest_according_to_settings(&self) {
        let memtest_enabled = self.config.run_memtest && self.settings.borrow().test_hardware;
        let mut memtest = self.memtest.borrow_mut();
        if memtest_enabled && memtest.is_none() {
            *memtest = Memtest::spawn();
        } else if !memtest_enabled && memtest.is_some() {
            *memtest = None;
        }
    }
}

// These methods may interact with `self.ui`.
impl ReportCrash {
    /// Update the submission details shown in the UI.
    pub fn update_details(&self) {
        use crate::std::fmt::Write;

        let mut extra = self.current_extra_data();
        modify_extra_for_report(&mut extra);

        let mut details = String::new();
        let mut entries: Vec<_> = extra.as_object().unwrap().into_iter().collect();
        entries.sort_unstable_by_key(|(k, _)| *k);
        for (key, value) in entries {
            let _ = write!(details, "{key}: ");
            if let Some(v) = value.as_str() {
                details.push_str(v);
            } else {
                match serde_json::to_string(value) {
                    Ok(s) => details.push_str(&s),
                    Err(e) => {
                        let _ = write!(details, "<serialization error: {e}>");
                    }
                }
            }
            let _ = writeln!(details);
        }
        let _ = writeln!(
            details,
            "{}",
            self.config.string("crashreporter-report-info")
        );

        self.ui().push(move |ui| *ui.details.borrow_mut() = details);
    }

    /// Restart the application and send the crash report.
    pub async fn restart(&self) {
        // Get the program restarted before sending the report.
        self.restart_process();
        let result = self.try_send().await;
        self.close_window(result.is_some()).await;
    }

    /// Quit and send the crash report.
    pub async fn quit(&self) {
        let result = self.try_send().await;
        self.close_window(result.is_some()).await;
    }

    async fn close_window(&self, report_sent: bool) {
        if report_sent && !self.config.auto_submit && !cfg!(test) {
            // Add a delay to allow the user to see the result.
            async_scoped_thread(|| std::thread::sleep(std::time::Duration::from_secs(5))).await;
        }

        self.ui().push(|r| r.close_window.fire(&()));
    }

    /// Try to send the report.
    ///
    /// This function may be called without a UI active (if auto_submit is true), so it will not
    /// panic if `self.ui` is unset.
    ///
    /// Returns whether the report was received (regardless of whether the response was processed
    /// successfully), if a report could be sent at all (based on the configuration).
    async fn try_send(&self) -> Option<bool> {
        // Whether the user wants to submit the report or not, we record that we attempted a send
        // (so to speak), confirming that we got to the point of user input. This will retain the
        // crash files rather than deleting them. E.g., the user may want to submit it later through
        // `about:crashes`.
        self.attempted_to_send.store(true, Relaxed);

        let send_report = self.settings.borrow().submit_report;

        if !send_report {
            log::trace!("not sending report due to user setting");
            return None;
        }

        // Potentially start/stop a memtest now, in case the settings have changed since launch.
        self.memtest_according_to_settings();

        // Incorporate user input into the extra data (which is acknowledged by "submit report"
        // being enabled).
        let mut extra = self.current_extra_data();

        // Store memtest output. The previous `memtest_according_to_settings()` ensures that we
        // only add the output if the setting is enabled.
        if let Some(memtest) = self.memtest.borrow_mut().take() {
            if let Some(ui) = &self.ui {
                ui.push(|r| *r.submit_state.borrow_mut() = SubmitState::WaitingHardwareTests);
            }

            match memtest.collect_output_for_submission() {
                Err(e) => log::error!("couldn't get memtest output: {e:#}"),
                Ok(s) => extra["MemtestOutput"] = s.into(),
            }
        }

        if let Some(ui) = &self.ui {
            ui.push(|r| *r.submit_state.borrow_mut() = SubmitState::InProgress);
        }

        // Update the extra file, since we may not be deleting it per the `attempted_to_send`
        // update, and it now matches exactly what we'll be sending.
        if let Err(e) = self.update_extra_file(&extra) {
            log::error!("failed to update extra file: {e:#}");
            // We can proceed in the event of an error; this is best-effort and serves as
            // insurance in case submission fails.
        }

        modify_extra_for_report(&mut extra);

        // The extra contents cannot change beyond this point.
        let extra = extra;

        // TODO? load proxy info from libgconf on linux

        let Some(url) = &self.config.report_url else {
            log::warn!("not sending report due to missing report url");
            return None;
        };

        let Some(url) = url.to_str() else {
            log::warn!(
                "not sending report due to provided url containing invalid utf8 characters: {url:?}"
            );
            return None;
        };

        // Send the report to the server.
        let memory_file = self.config.memory_file();
        let report = net::report::CrashReport {
            extra: &extra,
            dump_file: self.config.dump_file(),
            memory_file: memory_file.as_deref(),
            url,
        };

        let report_response = async_scoped_thread(|| report.send())
            .await
            .map(Some)
            .unwrap_or_else(|e| {
                log::error!("failed to send report: {e:#}");
                None
            });

        let report_received = report_response.is_some();
        let crash_id = report_response.and_then(|response| {
            self.handle_crash_report_response(response)
                .unwrap_or_else(|e| {
                    log::error!("failed to handle crash report response: {e}");
                    None
                })
        });

        // If the response could be handled (indicated by the returned crash id), clean up by
        // deleting the minidump files.
        if crash_id.is_some() {
            self.config.delete_files();
        }

        if let Err(e) = self.write_submission_event(crash_id) {
            log::warn!("failed to write submission event: {e}");
        }

        // Indicate whether the report was sent successfully, regardless of whether the response
        // was processed successfully.
        //
        // FIXME: this is how the old crash reporter worked, but we might want to change this
        // behavior.
        if let Some(ui) = &self.ui {
            ui.push(move |r| {
                *r.submit_state.borrow_mut() = if report_received {
                    SubmitState::Success
                } else {
                    SubmitState::Failure
                }
            });
        }

        Some(report_received)
    }

    /// Form the extra data, taking into account user input.
    fn current_extra_data(&self) -> serde_json::Value {
        let include_address = self.settings.borrow().include_url;
        let comment = if !self.config.auto_submit {
            self.ui().wait(|r| r.comment.get())
        } else {
            Default::default()
        };

        let mut extra = self.extra.clone();

        if !comment.is_empty() {
            extra["Comments"] = comment.into();
        }

        if !include_address {
            extra.as_object_mut().unwrap().remove("URL");
        }

        extra
    }

    fn ui(&self) -> &AsyncTask<ReportCrashUIState> {
        self.ui.as_ref().expect("UI remote queue missing")
    }
}

Messung V0.5 in Prozent
C=90 H=98 G=94

¤ Dauer der Verarbeitung: 0.8 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002