Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/crashreporter/client/app/src/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 21 kB image not shown  

Quelle  logic.rs   Sprache: unbekannt

 
/* 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::PathBuf,
    process::Command,
    sync::{
        atomic::{AtomicBool, Ordering::Relaxed},
        Arc, Mutex,
    },
};
use crate::{
    async_task::AsyncTask,
    config::Config,
    net,
    settings::Settings,
    std,
    ui::{ReportCrashUI, ReportCrashUIState, SubmitState},
};
use anyhow::Context;
use uuid::Uuid;

/// 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>>,
}

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 => {
                anyhow::bail!(
                    "failed to open settings file ({}): {e}",
                    settings_file.display()
                );
            }
            Err(_) => Default::default(),
            Ok(f) => Settings::from_reader(f)?,
        };
        Ok(ReportCrash {
            config,
            extra,
            settings_file,
            settings: settings.into(),
            attempted_to_send: Default::default(),
            ui: None,
        })
    }

    /// Returns whether an attempt was made to send the report.
    pub fn run(mut self) -> anyhow::Result<bool> {
        self.set_log_file();
        let hash = self.compute_minidump_hash().map(Some).unwrap_or_else(|e| {
            log::warn!("failed to compute minidump hash: {e:#}");
            None
        });
        let ping_uuid = self.send_crash_ping(hash.as_deref());
        if let Err(e) = self.update_events_file(hash.as_deref(), ping_uuid) {
            log::warn!("failed to update events file: {e:#}");
        }
        self.sanitize_extra();
        self.check_eol_version()?;
        if !self.config.auto_submit {
            self.run_ui();
        } else {
            anyhow::ensure!(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"));
        }
    }

    /// Compute the SHA256 hash of the minidump file contents, and return it as a hex string.
    fn compute_minidump_hash(&self) -> anyhow::Result<String> {
        let hash = {
            use sha2::{Digest, Sha256};
            let mut dump_file = std::fs::File::open(self.config.dump_file())?;
            let mut hasher = Sha256::new();
            std::io::copy(&mut dump_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)
    }

    /// Send crash pings to legacy telemetry and Glean.
    ///
    /// Returns the crash ping uuid used in legacy telemetry.
    fn send_crash_ping(&self, minidump_hash: Option<&str>) -> Option<Uuid> {
        net::ping::CrashPing {
            crash_id: self.config.local_dump_id().as_ref(),
            extra: &self.extra,
            ping_dir: self.config.ping_dir.as_deref(),
            minidump_hash,
            pingsender_path: self.config.installation_program_path("pingsender").as_ref(),
        }
        .send()
    }

    /// Remove unneeded entries from the extra file, and add some that indicate from where the data
    /// is being sent.
    fn sanitize_extra(&mut self) {
        if let Some(map) = self.extra.as_object_mut() {
            // Remove these entries, they don't need to be sent.
            map.remove("ProfileDirectory");
            map.remove("ServerURL");
            map.remove("StackTraces");
        }

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

    /// Update the events file with information about the crash ping, minidump hash, and
    /// stacktraces.
    fn update_events_file(
        &self,
        minidump_hash: Option<&str>,
        ping_uuid: Option<Uuid>,
    ) -> 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(&self, mut 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();
        }
        if let Some(uuid) = ping_uuid {
            events_file.data["CrashPingUUID"] = uuid.to_string().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) {
        if self.config.restart_command.is_none() {
            // The restart button should be hidden in this case, so this error should not occur.
            log::error!("no process configured for restart");
            return;
        }

        let mut cmd = Command::new(self.config.restart_command.as_ref().unwrap());
        cmd.args(&self.config.restart_args)
            .stdin(std::process::Stdio::null())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null());
        if let Some(xul_app_file) = &self.config.app_file {
            cmd.env("XUL_APP_FILE", xul_app_file);
        }
        log::debug!("restarting process: {:?}", cmd);
        if let Err(e) = cmd.spawn() {
            log::error!("failed to restart process: {e}");
        }
    }

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

        let (logic_send, logic_recv) = mpsc::channel();
        // Wrap work_send in an Arc so that it can be captured weakly by the work queue and
        // drop when the UI finishes, including panics (allowing the logic thread to exit).
        //
        // We need to wrap in a Mutex because std::mpsc::Sender isn't Sync (until rust 1.72).
        let logic_send = Arc::new(Mutex::new(logic_send));

        let weak_logic_send = Arc::downgrade(&logic_send);
        let logic_remote_queue = AsyncTask::new(move |f| {
            if let Some(logic_send) = weak_logic_send.upgrade() {
                // This is best-effort: ignore errors.
                let _ = logic_send.lock().unwrap().send(f);
            }
        });

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

        // Set the UI remote queue.
        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_send` 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_send = logic_send;
            s.spawn(move || {
                barrier.wait();
                while let Ok(f) = logic_recv.recv() {
                    f(self);
                }
                // Clear the UI remote queue, using it after this point is an error.
                //
                // 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;
            });

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

// 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 extra = self.current_extra_data();

        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 fn restart(&self) {
        self.save_settings();
        // Get the program restarted before sending the report.
        self.restart_process();
        let result = self.try_send();
        self.close_window(result.is_some());
    }

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

    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.
            std::thread::sleep(std::time::Duration::from_secs(5));
        }

        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).
    fn try_send(&self) -> Option<bool> {
        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;
        }

        // 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;
        };

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

        // Send the report to the server.
        let extra = self.current_extra_data();
        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 = report
            .send()
            .map(Some)
            .unwrap_or_else(|e| {
                log::error!("failed to initialize report transmission: {e}");
                None
            })
            .and_then(|sender| {
                // Normally we might want to do the following asynchronously since it will block,
                // however we don't really need the Logic thread to do anything else (the UI
                // becomes disabled from this point onward), so we just do it here. Same goes for
                // the `std::thread::sleep` in close_window() later on.
                sender.finish().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 report_received {
            // If the response could be handled (indicated by the returned crash id), clean up by
            // deleting the minidump files. Otherwise, prune old minidump files.
            if crash_id.is_some() {
                self.config.delete_files();
            } else {
                if let Err(e) = self.config.prune_files() {
                    log::warn!("failed to prune files: {e}");
                }
            }
        }

        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")
    }
}

[ Dauer der Verarbeitung: 0.34 Sekunden  (vorverarbeitet)  ]