summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /toolkit/crashreporter
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/crashreporter')
-rw-r--r--toolkit/crashreporter/CrashAnnotations.yaml6
-rw-r--r--toolkit/crashreporter/CrashSubmit.sys.mjs93
-rw-r--r--toolkit/crashreporter/client/Throbber-small.avibin3584 -> 0 bytes
-rw-r--r--toolkit/crashreporter/client/Throbber-small.gifbin825 -> 0 bytes
-rw-r--r--toolkit/crashreporter/client/app/Cargo.toml66
-rw-r--r--toolkit/crashreporter/client/app/Makefile.in (renamed from toolkit/crashreporter/client/Makefile.in)10
-rw-r--r--toolkit/crashreporter/client/app/build.rs89
-rw-r--r--toolkit/crashreporter/client/app/macos_app_bundle/Info.plist (renamed from toolkit/crashreporter/client/macbuild/Contents/Info.plist)2
-rw-r--r--toolkit/crashreporter/client/app/macos_app_bundle/PkgInfo (renamed from toolkit/crashreporter/client/macbuild/Contents/PkgInfo)0
-rw-r--r--toolkit/crashreporter/client/app/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in (renamed from toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in)0
-rw-r--r--toolkit/crashreporter/client/app/macos_app_bundle/Resources/crashreporter.icns (renamed from toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns)bin61743 -> 61743 bytes
-rw-r--r--toolkit/crashreporter/client/app/moz.build7
-rw-r--r--toolkit/crashreporter/client/app/src/async_task.rs31
-rw-r--r--toolkit/crashreporter/client/app/src/config.rs527
-rw-r--r--toolkit/crashreporter/client/app/src/data.rs400
-rw-r--r--toolkit/crashreporter/client/app/src/lang/language_info.rs74
-rw-r--r--toolkit/crashreporter/client/app/src/lang/mod.rs89
-rw-r--r--toolkit/crashreporter/client/app/src/lang/omnijar.rs103
-rw-r--r--toolkit/crashreporter/client/app/src/logging.rs86
-rw-r--r--toolkit/crashreporter/client/app/src/logic.rs660
-rw-r--r--toolkit/crashreporter/client/app/src/main.rs229
-rw-r--r--toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs177
-rw-r--r--toolkit/crashreporter/client/app/src/net/libcurl.rs406
-rw-r--r--toolkit/crashreporter/client/app/src/net/mod.rs12
-rw-r--r--toolkit/crashreporter/client/app/src/net/report.rs276
-rw-r--r--toolkit/crashreporter/client/app/src/process.rs23
-rw-r--r--toolkit/crashreporter/client/app/src/settings.rs39
-rw-r--r--toolkit/crashreporter/client/app/src/std/env.rs45
-rw-r--r--toolkit/crashreporter/client/app/src/std/fs.rs559
-rw-r--r--toolkit/crashreporter/client/app/src/std/mock.rs254
-rw-r--r--toolkit/crashreporter/client/app/src/std/mock_stub.rs20
-rw-r--r--toolkit/crashreporter/client/app/src/std/mod.rs33
-rw-r--r--toolkit/crashreporter/client/app/src/std/net.rs5
-rw-r--r--toolkit/crashreporter/client/app/src/std/path.rs157
-rw-r--r--toolkit/crashreporter/client/app/src/std/process.rs201
-rw-r--r--toolkit/crashreporter/client/app/src/std/thread.rs45
-rw-r--r--toolkit/crashreporter/client/app/src/std/time.rs32
-rw-r--r--toolkit/crashreporter/client/app/src/test.rs1289
-rw-r--r--toolkit/crashreporter/client/app/src/thread_bound.rs41
-rw-r--r--toolkit/crashreporter/client/app/src/ui/crashreporter.pngbin0 -> 2001 bytes
-rw-r--r--toolkit/crashreporter/client/app/src/ui/gtk.rs841
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/mod.rs1122
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/objc.rs242
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/plist.rs44
-rw-r--r--toolkit/crashreporter/client/app/src/ui/mod.rs295
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/button.rs26
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/checkbox.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/hbox.rs34
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/label.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/mod.rs344
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/progress.rs19
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/scroll.rs17
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/textbox.rs27
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/vbox.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/window.rs46
-rw-r--r--toolkit/crashreporter/client/app/src/ui/test.rs270
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/font.rs56
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/gdi.rs43
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/layout.rs436
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/mod.rs949
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs33
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/twoway.rs36
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/widestring.rs36
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/window.rs302
-rw-r--r--toolkit/crashreporter/client/cocoabind/Cargo.toml12
-rw-r--r--toolkit/crashreporter/client/cocoabind/build.rs74
-rw-r--r--toolkit/crashreporter/client/cocoabind/src/lib.rs10
-rw-r--r--toolkit/crashreporter/client/crashreporter.cpp852
-rw-r--r--toolkit/crashreporter/client/crashreporter.exe.manifest42
-rw-r--r--toolkit/crashreporter/client/crashreporter.h158
-rw-r--r--toolkit/crashreporter/client/crashreporter.icobin25214 -> 0 bytes
-rwxr-xr-xtoolkit/crashreporter/client/crashreporter.rc143
-rw-r--r--toolkit/crashreporter/client/crashreporter_gtk_common.cpp361
-rw-r--r--toolkit/crashreporter/client/crashreporter_gtk_common.h50
-rw-r--r--toolkit/crashreporter/client/crashreporter_linux.cpp525
-rw-r--r--toolkit/crashreporter/client/crashreporter_osx.h108
-rw-r--r--toolkit/crashreporter/client/crashreporter_osx.mm805
-rw-r--r--toolkit/crashreporter/client/crashreporter_unix_common.cpp139
-rw-r--r--toolkit/crashreporter/client/crashreporter_win.cpp1266
-rw-r--r--toolkit/crashreporter/client/gtkbind/Cargo.toml8
-rw-r--r--toolkit/crashreporter/client/gtkbind/build.rs43
-rw-r--r--toolkit/crashreporter/client/gtkbind/src/lib.rs (renamed from toolkit/crashreporter/process_reader/src/process_reader.rs)7
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib100
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib18
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nibbin25518 -> 0 bytes
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib100
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib18
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nibbin27032 -> 0 bytes
-rw-r--r--toolkit/crashreporter/client/moz.build97
-rw-r--r--toolkit/crashreporter/client/ping.cpp324
-rw-r--r--toolkit/crashreporter/client/resource.h35
-rw-r--r--toolkit/crashreporter/docs/index.rst9
-rw-r--r--toolkit/crashreporter/moz.build9
-rw-r--r--toolkit/crashreporter/mozannotation_client/src/lib.rs3
-rw-r--r--toolkit/crashreporter/mozannotation_server/Cargo.toml11
-rw-r--r--toolkit/crashreporter/mozannotation_server/src/errors.rs74
-rw-r--r--toolkit/crashreporter/mozannotation_server/src/lib.rs137
-rw-r--r--toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs193
-rw-r--r--toolkit/crashreporter/mozwer-rust/Cargo.toml5
-rw-r--r--toolkit/crashreporter/mozwer-rust/lib.rs31
-rw-r--r--toolkit/crashreporter/mozwer/moz.build7
-rw-r--r--toolkit/crashreporter/nsExceptionHandler.cpp207
-rw-r--r--toolkit/crashreporter/process_reader/Cargo.toml14
-rw-r--r--toolkit/crashreporter/process_reader/src/error.rs42
-rw-r--r--toolkit/crashreporter/process_reader/src/lib.rs72
-rw-r--r--toolkit/crashreporter/process_reader/src/platform.rs (renamed from toolkit/crashreporter/mozannotation_server/src/process_reader.rs)6
-rw-r--r--toolkit/crashreporter/process_reader/src/platform/linux.rs (renamed from toolkit/crashreporter/mozannotation_server/src/process_reader/linux.rs)144
-rw-r--r--toolkit/crashreporter/process_reader/src/platform/macos.rs (renamed from toolkit/crashreporter/mozannotation_server/src/process_reader/macos.rs)137
-rw-r--r--toolkit/crashreporter/process_reader/src/platform/windows.rs (renamed from toolkit/crashreporter/process_reader/src/process_reader/windows.rs)113
-rw-r--r--toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs1
-rw-r--r--toolkit/crashreporter/test/nsTestCrasher.cpp2
-rw-r--r--toolkit/crashreporter/test/unit/head_win64cfi.js2
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_stack_overflow.js2
-rw-r--r--toolkit/crashreporter/test/unit/test_crashreporter_appmem.js2
-rw-r--r--toolkit/crashreporter/test/unit/test_event_files.js2
-rw-r--r--toolkit/crashreporter/test/unit/test_override_exception_handler.js2
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js2
-rwxr-xr-xtoolkit/crashreporter/tools/symbolstore.py86
118 files changed, 12004 insertions, 5976 deletions
diff --git a/toolkit/crashreporter/CrashAnnotations.yaml b/toolkit/crashreporter/CrashAnnotations.yaml
index 8f6bb2e8c2..61da061768 100644
--- a/toolkit/crashreporter/CrashAnnotations.yaml
+++ b/toolkit/crashreporter/CrashAnnotations.yaml
@@ -753,6 +753,12 @@ QuotaManagerShutdownTimeout:
type: string
ping: true
+QuotaManagerStorageIsNetworkResource:
+ description: >
+ On Windows, this indicates if QM's base dir lives on a network resource.
+ It is the direct result of the Win32 API function PathIsNetworkPath.
+ type: boolean
+
RDDProcessStatus:
description: >
Status of the RDD process, can be set to "Running" or "Destroyed"
diff --git a/toolkit/crashreporter/CrashSubmit.sys.mjs b/toolkit/crashreporter/CrashSubmit.sys.mjs
index 28647c5a83..72ff5dc30f 100644
--- a/toolkit/crashreporter/CrashSubmit.sys.mjs
+++ b/toolkit/crashreporter/CrashSubmit.sys.mjs
@@ -2,8 +2,6 @@
* 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/. */
-import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
-
const SUCCESS = "success";
const FAILED = "failed";
const SUBMITTING = "submitting";
@@ -13,81 +11,6 @@ const UUID_REGEX =
const SUBMISSION_REGEX =
/^bp-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-// TODO: this is still synchronous; need an async INI parser to make it async
-function parseINIStrings(path) {
- let file = new FileUtils.File(path);
- let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
- Ci.nsIINIParserFactory
- );
- let parser = factory.createINIParser(file);
- let obj = {};
- for (let key of parser.getKeys("Strings")) {
- obj[key] = parser.getString("Strings", key);
- }
- return obj;
-}
-
-// Since we're basically re-implementing (with async) part of the crashreporter
-// client here, we'll just steal the strings we need from crashreporter.ini
-async function getL10nStrings() {
- let path = PathUtils.join(
- Services.dirsvc.get("GreD", Ci.nsIFile).path,
- "crashreporter.ini"
- );
- let pathExists = await IOUtils.exists(path);
-
- if (!pathExists) {
- // we if we're on a mac
- let parentDir = PathUtils.parent(path);
- path = PathUtils.join(
- parentDir,
- "MacOS",
- "crashreporter.app",
- "Contents",
- "Resources",
- "crashreporter.ini"
- );
-
- let pathExists = await IOUtils.exists(path);
-
- if (!pathExists) {
- // This happens on Android where everything is in an APK.
- // Android users can't see the contents of the submitted files
- // anyway, so just hardcode some fallback strings.
- return {
- crashid: "Crash ID: %s",
- reporturl: "You can view details of this crash at %s",
- };
- }
- }
-
- let crstrings = parseINIStrings(path);
- let strings = {
- crashid: crstrings.CrashID,
- reporturl: crstrings.CrashDetailsURL,
- };
-
- path = PathUtils.join(
- Services.dirsvc.get("XCurProcD", Ci.nsIFile).path,
- "crashreporter-override.ini"
- );
- pathExists = await IOUtils.exists(path);
-
- if (pathExists) {
- crstrings = parseINIStrings(path);
-
- if ("CrashID" in crstrings) {
- strings.crashid = crstrings.CrashID;
- }
-
- if ("CrashDetailsURL" in crstrings) {
- strings.reporturl = crstrings.CrashDetailsURL;
- }
- }
-
- return strings;
-}
-
function getDir(name) {
let uAppDataPath = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
return PathUtils.join(uAppDataPath, "Crash Reports", name);
@@ -109,11 +32,17 @@ function getPendingMinidump(id) {
}
async function writeSubmittedReportAsync(crashID, viewURL) {
- let strings = await getL10nStrings();
- let data = strings.crashid.replace("%s", crashID);
-
+ // Since we're basically re-implementing (with async) part of the
+ // crashreporter client here, we'll use the strings we need from the
+ // crashreporter fluent file.
+ const l10n = new Localization(["crashreporter/crashreporter.ftl"]);
+ let data = await l10n.formatValue("crashreporter-crash-identifier", {
+ id: crashID,
+ });
if (viewURL) {
- data += "\n" + strings.reporturl.replace("%s", viewURL);
+ data +=
+ "\n" +
+ (await l10n.formatValue("crashreporter-crash-details", { url: viewURL }));
}
await writeFileAsync("submitted", `${crashID}.txt`, data);
@@ -259,7 +188,7 @@ Submitter.prototype = {
let manager = Services.crashmanager;
let submissionID = manager.generateSubmissionID();
- xhr.addEventListener("readystatechange", evt => {
+ xhr.addEventListener("readystatechange", () => {
if (xhr.readyState == 4) {
let ret =
xhr.status === 200 ? this.parseResponse(xhr.responseText) : {};
diff --git a/toolkit/crashreporter/client/Throbber-small.avi b/toolkit/crashreporter/client/Throbber-small.avi
deleted file mode 100644
index 640ea62c0e..0000000000
--- a/toolkit/crashreporter/client/Throbber-small.avi
+++ /dev/null
Binary files differ
diff --git a/toolkit/crashreporter/client/Throbber-small.gif b/toolkit/crashreporter/client/Throbber-small.gif
deleted file mode 100644
index cce32f20f4..0000000000
--- a/toolkit/crashreporter/client/Throbber-small.gif
+++ /dev/null
Binary files differ
diff --git a/toolkit/crashreporter/client/app/Cargo.toml b/toolkit/crashreporter/client/app/Cargo.toml
new file mode 100644
index 0000000000..381ed32f97
--- /dev/null
+++ b/toolkit/crashreporter/client/app/Cargo.toml
@@ -0,0 +1,66 @@
+[package]
+name = "crashreporter"
+version = "1.0.0"
+edition = "2021"
+
+[[bin]]
+name = "crashreporter"
+
+[dependencies]
+anyhow = "1.0"
+cfg-if = "1.0"
+env_logger = { version = "0.10", default-features = false }
+fluent = "0.16.0"
+intl-memoizer = "0.5"
+libloading = "0.7"
+log = "0.4.17"
+mozbuild = "0.1"
+mozilla-central-workspace-hack = { version = "0.1", features = ["crashreporter"], optional = true }
+once_cell = "1"
+phf = "0.11"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+sha2 = "0.10"
+time = { version = "0.3", features = ["formatting", "serde"] }
+unic-langid = { version = "0.9.1" }
+uuid = { version = "1", features = ["v4", "serde"] }
+zip = { version = "0.6", default-features = false }
+
+[target."cfg(target_os = \"macos\")".dependencies]
+block = "0.1"
+cocoa = { package = "cocoabind", path = "../cocoabind" }
+objc = "0.2"
+
+[target."cfg(target_os = \"linux\")".dependencies]
+gtk = { package = "gtkbind", path = "../gtkbind" }
+
+[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
+version = "0.52"
+features = [
+ "Win32_Foundation",
+ "Win32_Globalization",
+ "Win32_Graphics_Gdi",
+ "Win32_System_Com",
+ "Win32_System_LibraryLoader",
+ "Win32_System_SystemServices",
+ "Win32_System_Threading",
+ "Win32_UI_Controls",
+ "Win32_UI_Input_KeyboardAndMouse",
+ "Win32_UI_Shell",
+ "Win32_UI_WindowsAndMessaging"
+]
+
+[features]
+# Required for tests
+mock = []
+
+[build-dependencies]
+embed-manifest = "1.4"
+mozbuild = "0.1"
+phf_codegen = "0.11"
+yaml-rust = "0.4"
+
+[dev-dependencies]
+bytes = "1.4"
+tokio = { version = "1.29", features = ["rt", "net", "time", "sync"] }
+warp = { version = "0.3", default-features = false }
diff --git a/toolkit/crashreporter/client/Makefile.in b/toolkit/crashreporter/client/app/Makefile.in
index 2d3ef1bee9..39fa3c8fbb 100644
--- a/toolkit/crashreporter/client/Makefile.in
+++ b/toolkit/crashreporter/client/app/Makefile.in
@@ -3,17 +3,15 @@
# 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/.
-ifeq ($(OS_ARCH),WINNT)
-MOZ_WINCONSOLE = 0
-endif
+ifeq ($(OS_ARCH),Darwin)
include $(topsrcdir)/config/rules.mk
-ifeq ($(OS_ARCH),Darwin)
libs::
$(NSINSTALL) -D $(DIST)/bin/crashreporter.app
- rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/crashreporter.app
- $(call py_action,preprocessor crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings)
+ rsync --archive --cvs-exclude --exclude '*.in' $(srcdir)/macos_app_bundle/ $(DIST)/bin/crashreporter.app/Contents/
+ $(call py_action,preprocessor crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings)
$(NSINSTALL) -D $(DIST)/bin/crashreporter.app/Contents/MacOS
$(NSINSTALL) $(DIST)/bin/crashreporter $(DIST)/bin/crashreporter.app/Contents/MacOS
+
endif
diff --git a/toolkit/crashreporter/client/app/build.rs b/toolkit/crashreporter/client/app/build.rs
new file mode 100644
index 0000000000..98f20dc375
--- /dev/null
+++ b/toolkit/crashreporter/client/app/build.rs
@@ -0,0 +1,89 @@
+/* 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/. */
+
+use std::{env, path::Path};
+
+fn main() {
+ windows_manifest();
+ crash_ping_annotations();
+ set_mock_cfg();
+}
+
+fn windows_manifest() {
+ use embed_manifest::{embed_manifest, manifest, new_manifest};
+
+ if std::env::var_os("CARGO_CFG_WINDOWS").is_none() {
+ return;
+ }
+
+ // See https://docs.rs/embed-manifest/1.4.0/embed_manifest/fn.new_manifest.html for what the
+ // default manifest includes. The defaults include almost all of the settings used in the old
+ // crash reporter.
+ let manifest = new_manifest("CrashReporter")
+ // Use legacy active code page because GDI doesn't support per-process UTF8 (and older
+ // win10 may not support this setting anyway).
+ .active_code_page(manifest::ActiveCodePage::Legacy)
+ // GDI scaling is not enabled by default but we need it to make the GDI-drawn text look
+ // nice on high-DPI displays.
+ .gdi_scaling(manifest::Setting::Enabled);
+
+ embed_manifest(manifest).expect("unable to embed windows manifest file");
+
+ println!("cargo:rerun-if-changed=build.rs");
+}
+
+/// Generate crash ping annotation information from the yaml definition file.
+fn crash_ping_annotations() {
+ use std::fs::File;
+ use std::io::{BufWriter, Write};
+ use yaml_rust::{Yaml, YamlLoader};
+
+ let crash_annotations = Path::new("../../CrashAnnotations.yaml")
+ .canonicalize()
+ .unwrap();
+ println!("cargo:rerun-if-changed={}", crash_annotations.display());
+
+ let crash_ping_file = Path::new(&env::var("OUT_DIR").unwrap()).join("ping_annotations.rs");
+
+ let yaml = std::fs::read_to_string(crash_annotations).unwrap();
+ let Yaml::Hash(entries) = YamlLoader::load_from_str(&yaml)
+ .unwrap()
+ .into_iter()
+ .next()
+ .unwrap()
+ else {
+ panic!("unexpected crash annotations root type");
+ };
+
+ let ping_annotations = entries.into_iter().filter_map(|(k, v)| {
+ v["ping"]
+ .as_bool()
+ .unwrap_or(false)
+ .then(|| k.into_string().unwrap())
+ });
+
+ let mut phf_set = phf_codegen::Set::new();
+ for annotation in ping_annotations {
+ phf_set.entry(annotation);
+ }
+
+ let mut file = BufWriter::new(File::create(&crash_ping_file).unwrap());
+ writeln!(
+ &mut file,
+ "static PING_ANNOTATIONS: phf::Set<&'static str> = {};",
+ phf_set.build()
+ )
+ .unwrap();
+}
+
+/// Set the mock configuration option when tests are enabled or when the mock feature is enabled.
+fn set_mock_cfg() {
+ // Very inconveniently, there's no way to detect `cfg(test)` from build scripts. See
+ // https://github.com/rust-lang/cargo/issues/4789. This seems like an arbitrary and pointless
+ // limitation, and only complicates the evaluation of mock behavior. Because of this, we have a
+ // `mock` feature which is activated by `toolkit/library/rust/moz.build`.
+ if env::var_os("CARGO_FEATURE_MOCK").is_some() || mozbuild::config::MOZ_CRASHREPORTER_MOCK {
+ println!("cargo:rustc-cfg=mock");
+ }
+}
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Info.plist b/toolkit/crashreporter/client/app/macos_app_bundle/Info.plist
index 51d6c4de37..f1679a922e 100644
--- a/toolkit/crashreporter/client/macbuild/Contents/Info.plist
+++ b/toolkit/crashreporter/client/app/macos_app_bundle/Info.plist
@@ -24,8 +24,6 @@
<string>1.0</string>
<key>LSHasLocalizedDisplayName</key>
<true/>
- <key>NSMainNibFile</key>
- <string>MainMenu</string>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>NSPrincipalClass</key>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo b/toolkit/crashreporter/client/app/macos_app_bundle/PkgInfo
index cae6d0a58f..cae6d0a58f 100644
--- a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo
+++ b/toolkit/crashreporter/client/app/macos_app_bundle/PkgInfo
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in
index e08ce59eb6..e08ce59eb6 100644
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in
+++ b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/crashreporter.icns
index 341cd05a4d..341cd05a4d 100644
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns
+++ b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/crashreporter.icns
Binary files differ
diff --git a/toolkit/crashreporter/client/app/moz.build b/toolkit/crashreporter/client/app/moz.build
new file mode 100644
index 0000000000..6e5c19173f
--- /dev/null
+++ b/toolkit/crashreporter/client/app/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+RUST_PROGRAMS = ["crashreporter"]
diff --git a/toolkit/crashreporter/client/app/src/async_task.rs b/toolkit/crashreporter/client/app/src/async_task.rs
new file mode 100644
index 0000000000..839db5e562
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/async_task.rs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+//! Manage work across multiple threads.
+//!
+//! Each thread has thread-bound data which can be accessed in queued task functions.
+
+pub type TaskFn<T> = Box<dyn FnOnce(&T) + Send + 'static>;
+
+pub struct AsyncTask<T> {
+ send: Box<dyn Fn(TaskFn<T>) + Send + Sync>,
+}
+
+impl<T> AsyncTask<T> {
+ pub fn new<F: Fn(TaskFn<T>) + Send + Sync + 'static>(send: F) -> Self {
+ AsyncTask {
+ send: Box::new(send),
+ }
+ }
+
+ pub fn push<F: FnOnce(&T) + Send + 'static>(&self, f: F) {
+ (self.send)(Box::new(f));
+ }
+
+ pub fn wait<R: Send + 'static, F: FnOnce(&T) -> R + Send + 'static>(&self, f: F) -> R {
+ let (tx, rx) = std::sync::mpsc::sync_channel(0);
+ self.push(move |v| tx.send(f(v)).unwrap());
+ rx.recv().unwrap()
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/config.rs b/toolkit/crashreporter/client/app/src/config.rs
new file mode 100644
index 0000000000..4e919395e2
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/config.rs
@@ -0,0 +1,527 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! Application configuration.
+
+use crate::std::borrow::Cow;
+use crate::std::ffi::{OsStr, OsString};
+use crate::std::path::{Path, PathBuf};
+use crate::{lang, logging::LogTarget, std};
+use anyhow::Context;
+
+/// The number of the most recent minidump files to retain when pruning.
+const MINIDUMP_PRUNE_SAVE_COUNT: usize = 10;
+
+#[cfg(test)]
+pub mod test {
+ pub const MINIDUMP_PRUNE_SAVE_COUNT: usize = super::MINIDUMP_PRUNE_SAVE_COUNT;
+}
+
+const VENDOR_KEY: &str = "Vendor";
+const PRODUCT_KEY: &str = "ProductName";
+const DEFAULT_VENDOR: &str = "Mozilla";
+const DEFAULT_PRODUCT: &str = "Firefox";
+
+#[derive(Default)]
+pub struct Config {
+ /// Whether reports should be automatically submitted.
+ pub auto_submit: bool,
+ /// Whether all threads of the process should be dumped (versus just the crashing thread).
+ pub dump_all_threads: bool,
+ /// Whether to delete the dump files after submission.
+ pub delete_dump: bool,
+ /// The data directory.
+ pub data_dir: Option<PathBuf>,
+ /// The events directory.
+ pub events_dir: Option<PathBuf>,
+ /// The ping directory.
+ pub ping_dir: Option<PathBuf>,
+ /// The dump file.
+ ///
+ /// If missing, an error dialog is displayed.
+ pub dump_file: Option<PathBuf>,
+ /// The XUL_APP_FILE to define if restarting the application.
+ pub app_file: Option<OsString>,
+ /// The path to the application to use when restarting the crashed process.
+ pub restart_command: Option<OsString>,
+ /// The arguments to pass if restarting the application.
+ pub restart_args: Vec<OsString>,
+ /// The URL to which to send reports.
+ pub report_url: Option<OsString>,
+ /// The localized strings to use.
+ pub strings: Option<lang::LangStrings>,
+ /// The log target.
+ pub log_target: Option<LogTarget>,
+}
+
+pub struct ConfigStringBuilder<'a>(lang::LangStringBuilder<'a>);
+
+impl<'a> ConfigStringBuilder<'a> {
+ /// Set an argument for the string.
+ pub fn arg<V: Into<Cow<'a, str>>>(self, key: &'a str, value: V) -> Self {
+ ConfigStringBuilder(self.0.arg(key, value))
+ }
+
+ /// Get the localized string.
+ pub fn get(self) -> String {
+ self.0
+ .get()
+ .context("failed to get localized string")
+ .unwrap()
+ }
+}
+
+impl Config {
+ /// Return a configuration with no values set, and all bool values false.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Load a configuration from the application environment.
+ #[cfg_attr(mock, allow(unused))]
+ pub fn read_from_environment(&mut self) -> anyhow::Result<()> {
+ /// Most environment variables are prefixed with `MOZ_CRASHREPORTER_`.
+ macro_rules! ekey {
+ ( $name:literal ) => {
+ concat!("MOZ_CRASHREPORTER_", $name)
+ };
+ }
+
+ self.auto_submit = env_bool(ekey!("AUTO_SUBMIT"));
+ self.dump_all_threads = env_bool(ekey!("DUMP_ALL_THREADS"));
+ self.delete_dump = !env_bool(ekey!("NO_DELETE_DUMP"));
+ self.data_dir = env_path(ekey!("DATA_DIRECTORY"));
+ self.events_dir = env_path(ekey!("EVENTS_DIRECTORY"));
+ self.ping_dir = env_path(ekey!("PING_DIRECTORY"));
+ self.app_file = std::env::var_os(ekey!("RESTART_XUL_APP_FILE"));
+
+ // Only support `MOZ_APP_LAUNCHER` on linux and macos.
+ if cfg!(not(target_os = "windows")) {
+ self.restart_command = std::env::var_os("MOZ_APP_LAUNCHER");
+ }
+
+ if self.restart_command.is_none() {
+ self.restart_command = Some(
+ self.sibling_program_path(mozbuild::config::MOZ_APP_NAME)
+ .into(),
+ )
+ }
+
+ // We no longer use don't use `MOZ_CRASHREPORTER_RESTART_ARG_0`, see bug 1872920.
+ self.restart_args = (1..)
+ .into_iter()
+ .map_while(|arg_num| std::env::var_os(format!("{}_{}", ekey!("RESTART_ARG"), arg_num)))
+ // Sometimes these are empty, in which case they should be ignored.
+ .filter(|s| !s.is_empty())
+ .collect();
+
+ self.report_url = std::env::var_os(ekey!("URL"));
+
+ let mut args = std::env::args_os()
+ // skip program name
+ .skip(1);
+ self.dump_file = args.next().map(|p| p.into());
+ while let Some(arg) = args.next() {
+ log::warn!("ignoring extraneous argument: {}", arg.to_string_lossy());
+ }
+
+ self.strings = Some(lang::load().context("failed to load localized strings")?);
+
+ Ok(())
+ }
+
+ /// Get the localized string for the given index.
+ pub fn string(&self, index: &str) -> String {
+ self.build_string(index).get()
+ }
+
+ /// Build the localized string for the given index.
+ pub fn build_string<'a>(&'a self, index: &'a str) -> ConfigStringBuilder<'a> {
+ ConfigStringBuilder(
+ self.strings
+ .as_ref()
+ .expect("strings not set")
+ .builder(index),
+ )
+ }
+
+ /// Whether the configured language has right-to-left text flow.
+ pub fn is_rtl(&self) -> bool {
+ self.strings
+ .as_ref()
+ .map(|s| s.is_rtl())
+ .unwrap_or_default()
+ }
+
+ /// Load the extra file, updating configuration.
+ pub fn load_extra_file(&mut self) -> anyhow::Result<serde_json::Value> {
+ let extra_file = self.extra_file().unwrap();
+
+ // Load the extra file (which minidump-analyzer just updated).
+ let extra: serde_json::Value =
+ serde_json::from_reader(std::fs::File::open(&extra_file).with_context(|| {
+ self.build_string("crashreporter-error-opening-file")
+ .arg("path", extra_file.display().to_string())
+ .get()
+ })?)
+ .with_context(|| {
+ self.build_string("crashreporter-error-loading-file")
+ .arg("path", extra_file.display().to_string())
+ .get()
+ })?;
+
+ // Set report url if not already set.
+ if self.report_url.is_none() {
+ if let Some(url) = extra["ServerURL"].as_str() {
+ self.report_url = Some(url.into());
+ }
+ }
+
+ // Set the data dir if not already set.
+ if self.data_dir.is_none() {
+ let vendor = extra[VENDOR_KEY].as_str().unwrap_or(DEFAULT_VENDOR);
+ let product = extra[PRODUCT_KEY].as_str().unwrap_or(DEFAULT_PRODUCT);
+ self.data_dir = Some(self.get_data_dir(vendor, product)?);
+ }
+
+ // Clear the restart command if WER handled the crash. This prevents restarting the
+ // program. See bug 1872920.
+ if extra.get("WindowsErrorReporting").is_some() {
+ self.restart_command = None;
+ }
+
+ Ok(extra)
+ }
+
+ /// Get the path to the extra file.
+ ///
+ /// Returns None if no dump_file is set.
+ pub fn extra_file(&self) -> Option<PathBuf> {
+ self.dump_file.clone().map(extra_file_for_dump_file)
+ }
+
+ /// Get the path to the memory file.
+ ///
+ /// Returns None if no dump_file is set or if the memory file does not exist.
+ pub fn memory_file(&self) -> Option<PathBuf> {
+ self.dump_file.clone().and_then(|p| {
+ let p = memory_file_for_dump_file(p);
+ p.exists().then_some(p)
+ })
+ }
+
+ /// The path to the data directory.
+ ///
+ /// Panics if no data directory is set.
+ pub fn data_dir(&self) -> &Path {
+ self.data_dir.as_deref().unwrap()
+ }
+
+ /// The path to the dump file.
+ ///
+ /// Panics if no dump file is set.
+ pub fn dump_file(&self) -> &Path {
+ self.dump_file.as_deref().unwrap()
+ }
+
+ /// The id of the local dump file (the base filename without extension).
+ ///
+ /// Panics if no dump file is set.
+ pub fn local_dump_id(&self) -> Cow<str> {
+ self.dump_file().file_stem().unwrap().to_string_lossy()
+ }
+
+ /// Move crash data to the pending folder.
+ pub fn move_crash_data_to_pending(&mut self) -> anyhow::Result<()> {
+ let pending_crashes_dir = self.data_dir().join("pending");
+ std::fs::create_dir_all(&pending_crashes_dir).with_context(|| {
+ self.build_string("crashreporter-error-creating-dir")
+ .arg("path", pending_crashes_dir.display().to_string())
+ .get()
+ })?;
+
+ let move_file = |from: &Path| -> anyhow::Result<PathBuf> {
+ let to = pending_crashes_dir.join(from.file_name().unwrap());
+ std::fs::rename(from, &to).with_context(|| {
+ self.build_string("crashreporter-error-moving-path")
+ .arg("from", from.display().to_string())
+ .arg("to", to.display().to_string())
+ .get()
+ })?;
+ Ok(to)
+ };
+
+ let new_dump_file = move_file(self.dump_file())?;
+ move_file(self.extra_file().unwrap().as_ref())?;
+ // Failing to move the memory file is recoverable.
+ if let Some(memory_file) = self.memory_file() {
+ if let Err(e) = move_file(memory_file.as_ref()) {
+ log::warn!("failed to move memory file: {e}");
+ if let Err(e) = std::fs::remove_file(&memory_file) {
+ log::warn!("failed to remove {}: {e}", memory_file.display());
+ }
+ }
+ }
+
+ self.dump_file = Some(new_dump_file);
+
+ Ok(())
+ }
+
+ /// Form the path which signals EOL for a particular version.
+ pub fn version_eol_file(&self, version: &str) -> PathBuf {
+ self.data_dir().join(format!("EndOfLife{version}"))
+ }
+
+ /// Return the path used to store submitted crash ids.
+ pub fn submitted_crash_dir(&self) -> PathBuf {
+ self.data_dir().join("submitted")
+ }
+
+ /// Delete files related to the crash report.
+ pub fn delete_files(&self) {
+ if !self.delete_dump {
+ return;
+ }
+
+ for file in [&self.dump_file, &self.extra_file(), &self.memory_file()]
+ .into_iter()
+ .flatten()
+ {
+ if let Err(e) = std::fs::remove_file(file) {
+ log::warn!("failed to remove {}: {e}", file.display());
+ }
+ }
+ }
+
+ /// Prune old minidump files adjacent to the dump file.
+ pub fn prune_files(&self) -> anyhow::Result<()> {
+ log::info!("pruning minidump files to the {MINIDUMP_PRUNE_SAVE_COUNT} most recent");
+ let Some(file) = &self.dump_file else {
+ anyhow::bail!("no dump file")
+ };
+ let Some(dir) = file.parent() else {
+ anyhow::bail!("no parent directory for dump file")
+ };
+ log::debug!("pruning {} directory", dir.display());
+ let read_dir = dir.read_dir().with_context(|| {
+ format!(
+ "failed to read dump file parent directory {}",
+ dir.display()
+ )
+ })?;
+
+ let mut minidump_files = Vec::new();
+ for entry in read_dir {
+ match entry {
+ Err(e) => log::error!(
+ "error while iterating over {} directory entry: {e}",
+ dir.display()
+ ),
+ Ok(e) if e.path().extension() == Some("dmp".as_ref()) => {
+ // Return if the metadata can't be read, since not being able to get metadata
+ // for any file could make the selection of minidumps to delete incorrect.
+ let meta = e.metadata().with_context(|| {
+ format!("failed to read metadata for {}", e.path().display())
+ })?;
+ if meta.is_file() {
+ let modified_time =
+ meta.modified().expect(
+ "file modification time should be available on all crashreporter platforms",
+ );
+ minidump_files.push((modified_time, e.path()));
+ }
+ }
+ _ => (),
+ }
+ }
+
+ // Sort by modification time first, then path (just to have a defined behavior in the case
+ // of identical times). The reverse leaves the files in order from newest to oldest.
+ minidump_files.sort_unstable_by(|a, b| a.cmp(b).reverse());
+
+ // Delete files, skipping the most recent MINIDUMP_PRUNE_SAVE_COUNT.
+ for dump_file in minidump_files
+ .into_iter()
+ .skip(MINIDUMP_PRUNE_SAVE_COUNT)
+ .map(|v| v.1)
+ {
+ log::debug!("pruning {} and related files", dump_file.display());
+ if let Err(e) = std::fs::remove_file(&dump_file) {
+ log::warn!("failed to delete {}: {e}", dump_file.display());
+ }
+
+ // Ignore errors for the extra file and the memory file: they may not exist.
+ let _ = std::fs::remove_file(extra_file_for_dump_file(dump_file.clone()));
+ let _ = std::fs::remove_file(memory_file_for_dump_file(dump_file));
+ }
+ Ok(())
+ }
+
+ /// Get the path of a program that is a sibling of the crashreporter.
+ ///
+ /// On MacOS, this assumes that the crashreporter is its own application bundle within the main
+ /// program bundle. On other platforms this assumes siblings reside in the same directory as
+ /// the crashreporter.
+ ///
+ /// The returned path isn't guaranteed to exist.
+ // This method could be standalone rather than living in `Config`; it's here because it makes
+ // sense that if it were to rely on anything, it would be the `Config` (and that may change in
+ // the future).
+ pub fn sibling_program_path<N: AsRef<OsStr>>(&self, program: N) -> PathBuf {
+ // Expect shouldn't ever panic here because we need more than one argument to run
+ // the program in the first place (we've already previously iterated args).
+ //
+ // We use argv[0] rather than `std::env::current_exe` because `current_exe` doesn't define
+ // how symlinks are treated, and we want to support running directly from the local build
+ // directory (which uses symlinks on linux and macos).
+ let self_path = PathBuf::from(std::env::args_os().next().expect("failed to get argv[0]"));
+ let exe_extension = self_path.extension().unwrap_or_default();
+
+ let mut program_path = self_path.clone();
+ // Pop the executable off to get the parent directory.
+ program_path.pop();
+ program_path.push(program.as_ref());
+ program_path.set_extension(exe_extension);
+
+ if !program_path.exists() && cfg!(all(not(mock), target_os = "macos")) {
+ // On macOS the crash reporter client is shipped as an application bundle contained
+ // within Firefox's main application bundle. So when it's invoked its current working
+ // directory looks like:
+ // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/
+ // The other applications we ship with Firefox are stored in the main bundle
+ // (Firefox.app/Contents/MacOS/) so we we need to go back three directories
+ // to reach them.
+
+ // 4 pops: 1 for the path that was just pushed, and 3 more for
+ // `crashreporter.app/Contents/MacOS`.
+ for _ in 0..4 {
+ program_path.pop();
+ }
+ program_path.push(program.as_ref());
+ program_path.set_extension(exe_extension);
+ }
+
+ program_path
+ }
+
+ cfg_if::cfg_if! {
+ if #[cfg(mock)] {
+ fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
+ let mut path = PathBuf::from("data_dir");
+ path.push(vendor);
+ path.push(product);
+ path.push("Crash Reports");
+ Ok(path)
+ }
+ } else if #[cfg(target_os = "linux")] {
+ fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
+ // home_dir is deprecated due to incorrect behavior on windows, but we only use it on linux
+ #[allow(deprecated)]
+ let mut data_path =
+ std::env::home_dir().with_context(|| self.string("crashreporter-error-no-home-dir"))?;
+ data_path.push(format!(".{}", vendor.to_lowercase()));
+ data_path.push(product.to_lowercase());
+ data_path.push("Crash Reports");
+ Ok(data_path)
+ }
+ } else if #[cfg(target_os = "macos")] {
+ fn get_data_dir(&self, _vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
+ use objc::{
+ rc::autoreleasepool,
+ runtime::{Object, BOOL, YES},
+ *,
+ };
+ #[link(name = "Foundation", kind = "framework")]
+ extern "system" {
+ fn NSSearchPathForDirectoriesInDomains(
+ directory: usize,
+ domain_mask: usize,
+ expand_tilde: BOOL,
+ ) -> *mut Object /* NSArray<NSString*>* */;
+ }
+ #[allow(non_upper_case_globals)]
+ const NSApplicationSupportDirectory: usize = 14;
+ #[allow(non_upper_case_globals)]
+ const NSUserDomainMask: usize = 1;
+
+ let mut data_path = autoreleasepool(|| {
+ let paths /* NSArray<NSString*>* */ = unsafe {
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)
+ };
+ if paths.is_null() {
+ anyhow::bail!("NSSearchPathForDirectoriesInDomains returned nil");
+ }
+ let path: *mut Object /* NSString* */ = unsafe { msg_send![paths, firstObject] };
+ if path.is_null() {
+ anyhow::bail!("NSSearchPathForDirectoriesInDomains returned no paths");
+ }
+
+ let str_pointer: *const i8 = unsafe { msg_send![path, UTF8String] };
+ // # Safety
+ // The pointer is a readable C string with a null terminator.
+ let Ok(s) = unsafe { std::ffi::CStr::from_ptr(str_pointer) }.to_str() else {
+ anyhow::bail!("NSString wasn't valid UTF8");
+ };
+ Ok(PathBuf::from(s))
+ })?;
+ data_path.push(product);
+ std::fs::create_dir_all(&data_path).with_context(|| {
+ self.build_string("crashreporter-error-creating-dir")
+ .arg("path", data_path.display().to_string())
+ .get()
+ })?;
+ data_path.push("Crash Reports");
+ Ok(data_path)
+ }
+ } else if #[cfg(target_os = "windows")] {
+ fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
+ use crate::std::os::windows::ffi::OsStringExt;
+ use windows_sys::{
+ core::PWSTR,
+ Win32::{
+ Globalization::lstrlenW,
+ System::Com::CoTaskMemFree,
+ UI::Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath},
+ },
+ };
+
+ let mut path: PWSTR = std::ptr::null_mut();
+ let result = unsafe { SHGetKnownFolderPath(&FOLDERID_RoamingAppData, 0, 0, &mut path) };
+ if result != 0 {
+ unsafe { CoTaskMemFree(path as _) };
+ anyhow::bail!("failed to get known path for roaming appdata");
+ }
+
+ let length = unsafe { lstrlenW(path) };
+ let slice = unsafe { std::slice::from_raw_parts(path, length as usize) };
+ let osstr = OsString::from_wide(slice);
+ unsafe { CoTaskMemFree(path as _) };
+ let mut path = PathBuf::from(osstr);
+ path.push(vendor);
+ path.push(product);
+ path.push("Crash Reports");
+ Ok(path)
+ }
+ }
+ }
+}
+
+fn env_bool<K: AsRef<OsStr>>(name: K) -> bool {
+ std::env::var(name).map(|s| !s.is_empty()).unwrap_or(false)
+}
+
+fn env_path<K: AsRef<OsStr>>(name: K) -> Option<PathBuf> {
+ std::env::var_os(name).map(PathBuf::from)
+}
+
+fn extra_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf {
+ dump_file.set_extension("extra");
+ dump_file
+}
+
+fn memory_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf {
+ dump_file.set_extension("memory.json.gz");
+ dump_file
+}
diff --git a/toolkit/crashreporter/client/app/src/data.rs b/toolkit/crashreporter/client/app/src/data.rs
new file mode 100644
index 0000000000..474da8966a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/data.rs
@@ -0,0 +1,400 @@
+/* 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/. */
+
+//! Data binding types are used to implement dynamic behaviors in UIs. [`Event`] is the primitive
+//! type underlying most others. [Properties](Property) are what should usually be used in UI
+//! models, since they have `From` impls allowing different binding behaviors to be set.
+
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// An event which can have multiple subscribers.
+///
+/// The type parameter is the payload of the event.
+pub struct Event<T> {
+ subscribers: Rc<RefCell<Vec<Box<dyn Fn(&T)>>>>,
+}
+
+impl<T> Clone for Event<T> {
+ fn clone(&self) -> Self {
+ Event {
+ subscribers: self.subscribers.clone(),
+ }
+ }
+}
+
+impl<T> Default for Event<T> {
+ fn default() -> Self {
+ Event {
+ subscribers: Default::default(),
+ }
+ }
+}
+
+impl<T> std::fmt::Debug for Event<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(
+ f,
+ "{} {{ {} subscribers }}",
+ std::any::type_name::<Self>(),
+ self.subscribers.borrow().len()
+ )
+ }
+}
+
+impl<T> Event<T> {
+ /// Add a callback for when the event is fired.
+ pub fn subscribe<F>(&self, f: F)
+ where
+ F: Fn(&T) + 'static,
+ {
+ self.subscribers.borrow_mut().push(Box::new(f));
+ }
+
+ /// Fire the event with the given payload.
+ pub fn fire(&self, payload: &T) {
+ for f in self.subscribers.borrow().iter() {
+ f(payload);
+ }
+ }
+}
+
+/// A synchronized runtime value.
+///
+/// Consumers can subscribe to change events on the value. Change events are fired when
+/// `borrow_mut()` references are dropped.
+#[derive(Default)]
+pub struct Synchronized<T> {
+ inner: Rc<SynchronizedInner<T>>,
+}
+
+impl<T> Clone for Synchronized<T> {
+ fn clone(&self) -> Self {
+ Synchronized {
+ inner: self.inner.clone(),
+ }
+ }
+}
+
+impl<T: std::fmt::Debug> std::fmt::Debug for Synchronized<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct(std::any::type_name::<Self>())
+ .field("current", &*self.inner.current.borrow())
+ .field("change", &self.inner.change)
+ .finish()
+ }
+}
+
+#[derive(Default)]
+struct SynchronizedInner<T> {
+ current: RefCell<T>,
+ change: Event<T>,
+}
+
+impl<T> Synchronized<T> {
+ /// Create a new value with the given inner data.
+ pub fn new(initial: T) -> Self {
+ Synchronized {
+ inner: Rc::new(SynchronizedInner {
+ current: RefCell::new(initial),
+ change: Default::default(),
+ }),
+ }
+ }
+
+ /// Borrow a value's data.
+ pub fn borrow(&self) -> std::cell::Ref<T> {
+ self.inner.current.borrow()
+ }
+
+ /// Mutably borrow a value's data.
+ ///
+ /// When the mutable reference is dropped, a change event is fired.
+ pub fn borrow_mut(&self) -> ValueRefMut<'_, T> {
+ ValueRefMut {
+ value: std::mem::ManuallyDrop::new(self.inner.current.borrow_mut()),
+ inner: &self.inner,
+ }
+ }
+
+ /// Subscribe to change events in the value.
+ pub fn on_change<F: Fn(&T) + 'static>(&self, f: F) {
+ self.inner.change.subscribe(f);
+ }
+
+ /// Update another synchronized value when this one changes.
+ pub fn update_on_change<U: 'static, F: Fn(&T) -> U + 'static>(
+ &self,
+ other: &Synchronized<U>,
+ f: F,
+ ) {
+ let other = other.clone();
+ self.on_change(move |val| {
+ *other.borrow_mut() = f(val);
+ });
+ }
+
+ /// Create a new synchronized value which will update when this one changes.
+ pub fn mapped<U: 'static, F: Fn(&T) -> U + 'static>(&self, f: F) -> Synchronized<U> {
+ let s = Synchronized::new(f(&*self.borrow()));
+ self.update_on_change(&s, f);
+ s
+ }
+
+ pub fn join<A: 'static, B: 'static, F: Fn(&A, &B) -> T + Clone + 'static>(
+ a: &Synchronized<A>,
+ b: &Synchronized<B>,
+ f: F,
+ ) -> Self
+ where
+ T: 'static,
+ {
+ let s = Synchronized::new(f(&*a.borrow(), &*b.borrow()));
+ let update = cc! { (a,b,s) move || {
+ *s.borrow_mut() = f(&*a.borrow(), &*b.borrow());
+ }};
+ a.on_change(cc! { (update) move |_| update()});
+ b.on_change(move |_| update());
+ s
+ }
+}
+
+/// A runtime value that can be fetched on-demand (read-only).
+///
+/// Consumers call [`read`] or [`get`] to retrieve the value, while producers call [`register`] to
+/// set the function which is called to retrieve the value. This is of most use for things like
+/// editable text strings, where it would be unnecessarily expensive to e.g. update a
+/// `Synchronized` property as the text string is changed (debouncing could be used, but if change
+/// notification isn't needed then it's still unnecessary).
+pub struct OnDemand<T> {
+ get: Rc<RefCell<Option<Box<dyn Fn(&mut T) + 'static>>>>,
+}
+
+impl<T> Default for OnDemand<T> {
+ fn default() -> Self {
+ OnDemand {
+ get: Default::default(),
+ }
+ }
+}
+
+impl<T> Clone for OnDemand<T> {
+ fn clone(&self) -> Self {
+ OnDemand {
+ get: self.get.clone(),
+ }
+ }
+}
+
+impl<T> std::fmt::Debug for OnDemand<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(
+ f,
+ "{} {{ {} }}",
+ std::any::type_name::<Self>(),
+ if self.get.borrow().is_none() {
+ "not registered"
+ } else {
+ "registered"
+ }
+ )
+ }
+}
+
+impl<T> OnDemand<T> {
+ /// Reads the current value.
+ pub fn read(&self, value: &mut T) {
+ match &*self.get.borrow() {
+ None => {
+ // The test UI doesn't always register OnDemand getters (only on a per-test basis),
+ // so don't panic otherwise the tests will fail unnecessarily.
+ #[cfg(not(test))]
+ panic!("OnDemand not registered by renderer")
+ }
+ Some(f) => f(value),
+ }
+ }
+
+ /// Get a copy of the current value.
+ pub fn get(&self) -> T
+ where
+ T: Default,
+ {
+ let mut r = T::default();
+ self.read(&mut r);
+ r
+ }
+
+ /// Register the function to use when getting the value.
+ pub fn register(&self, f: impl Fn(&mut T) + 'static) {
+ *self.get.borrow_mut() = Some(Box::new(f));
+ }
+}
+
+/// A UI element property.
+///
+/// Properties support static and dynamic value bindings.
+/// * `T` can be converted to static bindings.
+/// * `Synchronized<T>` can be converted to dynamic bindings which will be updated
+/// bidirectionally.
+/// * `OnDemand<T>` can be converted to dynamic bindings which can be queried on an as-needed
+/// basis.
+#[derive(Clone, Debug)]
+pub enum Property<T> {
+ Static(T),
+ Binding(Synchronized<T>),
+ ReadOnly(OnDemand<T>),
+}
+
+#[cfg(test)]
+impl<T: Clone + Default + 'static> Property<T> {
+ pub fn set(&self, value: T) {
+ match self {
+ Property::Static(_) => panic!("cannot set static property"),
+ Property::Binding(s) => *s.borrow_mut() = value,
+ Property::ReadOnly(o) => o.register(move |v| *v = value.clone()),
+ }
+ }
+
+ pub fn get(&self) -> T {
+ match self {
+ Property::Static(v) => v.clone(),
+ Property::Binding(s) => s.borrow().clone(),
+ Property::ReadOnly(o) => o.get(),
+ }
+ }
+}
+
+impl<T: Default> Default for Property<T> {
+ fn default() -> Self {
+ Property::Static(Default::default())
+ }
+}
+
+impl<T> From<T> for Property<T> {
+ fn from(value: T) -> Self {
+ Property::Static(value)
+ }
+}
+
+impl<T> From<&Synchronized<T>> for Property<T> {
+ fn from(value: &Synchronized<T>) -> Self {
+ Property::Binding(value.clone())
+ }
+}
+
+impl<T> From<&OnDemand<T>> for Property<T> {
+ fn from(value: &OnDemand<T>) -> Self {
+ Property::ReadOnly(value.clone())
+ }
+}
+
+/// A mutable Value reference.
+///
+/// When dropped, the Value's change event will fire (_after_ demoting the RefMut to a Ref).
+pub struct ValueRefMut<'a, T> {
+ value: std::mem::ManuallyDrop<std::cell::RefMut<'a, T>>,
+ inner: &'a SynchronizedInner<T>,
+}
+
+impl<T> std::ops::Deref for ValueRefMut<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &*self.value
+ }
+}
+
+impl<T> std::ops::DerefMut for ValueRefMut<'_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut *self.value
+ }
+}
+
+impl<T> Drop for ValueRefMut<'_, T> {
+ fn drop(&mut self) {
+ unsafe { std::mem::ManuallyDrop::drop(&mut self.value) };
+ self.inner.change.fire(&*self.inner.current.borrow());
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
+
+ #[derive(Default, Clone)]
+ struct Trace {
+ count: Rc<AtomicUsize>,
+ }
+
+ impl Trace {
+ fn inc(&self) {
+ self.count.fetch_add(1, Relaxed);
+ }
+
+ fn set(&self, v: usize) {
+ self.count.store(v, Relaxed);
+ }
+
+ fn count(&self) -> usize {
+ self.count.load(Relaxed)
+ }
+ }
+
+ #[test]
+ fn event() {
+ let t1 = Trace::default();
+ let t2 = Trace::default();
+ let evt = Event::default();
+ evt.subscribe(cc! { (t1) move |x| {
+ assert!(x == &42);
+ t1.inc()
+ }});
+ evt.fire(&42);
+ assert_eq!(t1.count(), 1);
+ evt.subscribe(cc! { (t2) move |_| t2.inc() });
+ evt.fire(&42);
+ assert_eq!(t1.count(), 2);
+ assert_eq!(t2.count(), 1);
+ }
+
+ #[test]
+ fn synchronized() {
+ let t1 = Trace::default();
+ let s = Synchronized::<usize>::default();
+ assert_eq!(*s.borrow(), 0);
+
+ s.on_change(cc! { (t1) move |v| t1.set(*v) });
+ {
+ let mut s_ref = s.borrow_mut();
+ *s_ref = 41;
+ // Changes should only occur when the ref is dropped
+ assert_eq!(t1.count(), 0);
+ *s_ref = 42;
+ }
+ assert_eq!(t1.count(), 42);
+ assert_eq!(*s.borrow(), 42);
+ }
+
+ #[test]
+ fn ondemand() {
+ let t1 = Trace::default();
+ let d = OnDemand::<usize>::default();
+ d.register(|v| *v = 42);
+ {
+ let mut v = 0;
+ d.read(&mut v);
+ assert_eq!(v, 42);
+ }
+ d.register(|v| *v = 10);
+ assert_eq!(d.get(), 10);
+
+ t1.inc();
+ d.register(cc! { (t1) move |v| *v = t1.count() });
+ assert_eq!(d.get(), 1);
+ t1.set(42);
+ assert_eq!(d.get(), 42);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/lang/language_info.rs b/toolkit/crashreporter/client/app/src/lang/language_info.rs
new file mode 100644
index 0000000000..b05953e2b3
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/lang/language_info.rs
@@ -0,0 +1,74 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+use super::LangStrings;
+use anyhow::Context;
+use fluent::{bundle::FluentBundle, FluentResource};
+use unic_langid::LanguageIdentifier;
+
+const FALLBACK_FTL_FILE: &str = include_str!(mozbuild::srcdir_path!(
+ "/toolkit/locales/en-US/crashreporter/crashreporter.ftl"
+));
+const FALLBACK_BRANDING_FILE: &str = include_str!(mozbuild::srcdir_path!(
+ "/browser/branding/official/locales/en-US/brand.ftl"
+));
+
+/// Localization language information.
+#[derive(Debug, Clone)]
+pub struct LanguageInfo {
+ pub identifier: String,
+ pub ftl_definitions: String,
+ pub ftl_branding: String,
+}
+
+impl Default for LanguageInfo {
+ fn default() -> Self {
+ Self::fallback()
+ }
+}
+
+impl LanguageInfo {
+ /// Get the fallback bundled language information (en-US).
+ pub fn fallback() -> Self {
+ LanguageInfo {
+ identifier: "en-US".to_owned(),
+ ftl_definitions: FALLBACK_FTL_FILE.to_owned(),
+ ftl_branding: FALLBACK_BRANDING_FILE.to_owned(),
+ }
+ }
+
+ /// Load strings from the language info.
+ pub fn load_strings(self) -> anyhow::Result<LangStrings> {
+ let Self {
+ identifier: lang,
+ ftl_definitions: definitions,
+ ftl_branding: branding,
+ } = self;
+
+ let langid = lang
+ .parse::<LanguageIdentifier>()
+ .with_context(|| format!("failed to parse language identifier ({lang})"))?;
+ let rtl = langid.character_direction() == unic_langid::CharacterDirection::RTL;
+ let mut bundle = FluentBundle::new_concurrent(vec![langid]);
+
+ fn add_ftl<M>(
+ bundle: &mut FluentBundle<FluentResource, M>,
+ ftl: String,
+ ) -> anyhow::Result<()> {
+ let resource = FluentResource::try_new(ftl)
+ .ok()
+ .context("failed to create fluent resource")?;
+ bundle
+ .add_resource(resource)
+ .ok()
+ .context("failed to add fluent resource to bundle")?;
+ Ok(())
+ }
+
+ add_ftl(&mut bundle, branding).context("failed to add branding")?;
+ add_ftl(&mut bundle, definitions).context("failed to add localization")?;
+
+ Ok(LangStrings::new(bundle, rtl))
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/lang/mod.rs b/toolkit/crashreporter/client/app/src/lang/mod.rs
new file mode 100644
index 0000000000..9b7495f92a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/lang/mod.rs
@@ -0,0 +1,89 @@
+/* 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/. */
+
+mod language_info;
+mod omnijar;
+
+use fluent::{bundle::FluentBundle, FluentArgs, FluentResource};
+use intl_memoizer::concurrent::IntlLangMemoizer;
+#[cfg(test)]
+pub use language_info::LanguageInfo;
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+
+/// Get the localized string bundle.
+pub fn load() -> anyhow::Result<LangStrings> {
+ // TODO support langpacks, bug 1873210
+ omnijar::read().unwrap_or_else(|e| {
+ log::warn!("failed to read localization data from the omnijar ({e}), falling back to bundled content");
+ Default::default()
+ }).load_strings()
+}
+
+/// A bundle of localized strings.
+pub struct LangStrings {
+ bundle: FluentBundle<FluentResource, IntlLangMemoizer>,
+ rtl: bool,
+}
+
+/// Arguments to build a localized string.
+pub type LangStringsArgs<'a> = BTreeMap<&'a str, Cow<'a, str>>;
+
+impl LangStrings {
+ pub fn new(bundle: FluentBundle<FluentResource, IntlLangMemoizer>, rtl: bool) -> Self {
+ LangStrings { bundle, rtl }
+ }
+
+ /// Return whether the localized language has right-to-left text flow.
+ pub fn is_rtl(&self) -> bool {
+ self.rtl
+ }
+
+ pub fn get(&self, index: &str, args: LangStringsArgs) -> anyhow::Result<String> {
+ let mut fluent_args = FluentArgs::with_capacity(args.len());
+ for (k, v) in args {
+ fluent_args.set(k, v);
+ }
+
+ let Some(pattern) = self.bundle.get_message(index).and_then(|m| m.value()) else {
+ anyhow::bail!("failed to get fluent message for {index}");
+ };
+ let mut errs = Vec::new();
+ let ret = self
+ .bundle
+ .format_pattern(pattern, Some(&fluent_args), &mut errs);
+ if !errs.is_empty() {
+ anyhow::bail!("errors while formatting pattern: {errs:?}");
+ }
+ Ok(ret.into_owned())
+ }
+
+ pub fn builder<'a>(&'a self, index: &'a str) -> LangStringBuilder<'a> {
+ LangStringBuilder {
+ strings: self,
+ index,
+ args: Default::default(),
+ }
+ }
+}
+
+/// A localized string builder.
+pub struct LangStringBuilder<'a> {
+ strings: &'a LangStrings,
+ index: &'a str,
+ args: LangStringsArgs<'a>,
+}
+
+impl<'a> LangStringBuilder<'a> {
+ /// Set an argument for the string.
+ pub fn arg<V: Into<Cow<'a, str>>>(mut self, key: &'a str, value: V) -> Self {
+ self.args.insert(key, value.into());
+ self
+ }
+
+ /// Get the localized string.
+ pub fn get(self) -> anyhow::Result<String> {
+ self.strings.get(self.index, self.args)
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/lang/omnijar.rs b/toolkit/crashreporter/client/app/src/lang/omnijar.rs
new file mode 100644
index 0000000000..2d2c34dd8d
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/lang/omnijar.rs
@@ -0,0 +1,103 @@
+/* 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/. */
+
+use super::language_info::LanguageInfo;
+use crate::std::{
+ env::current_exe,
+ fs::File,
+ io::{BufRead, BufReader, Read},
+ path::Path,
+};
+use anyhow::Context;
+use zip::read::ZipArchive;
+
+/// Read the appropriate localization fluent definitions from the omnijar files.
+///
+/// Returns (locale name, fluent definitions).
+pub fn read() -> anyhow::Result<LanguageInfo> {
+ let mut path = current_exe().context("failed to get current executable")?;
+ path.pop();
+ path.push("omni.ja");
+
+ let mut zip = read_omnijar_file(&path)?;
+ let locales = {
+ let buf = BufReader::new(
+ zip.by_name("res/multilocale.txt")
+ .context("failed to read multilocale file in zip archive")?,
+ );
+ let line = buf
+ .lines()
+ .next()
+ .ok_or(anyhow::anyhow!("multilocale file was empty"))?
+ .context("failed to read first line of multilocale file")?;
+ line.split(",")
+ .map(|s| s.trim().to_owned())
+ .collect::<Vec<_>>()
+ };
+
+ let (locale, ftl_definitions) = 'defs: {
+ for locale in &locales {
+ match read_strings(locale, &mut zip) {
+ Ok(v) => break 'defs (locale.to_string(), v),
+ Err(e) => log::warn!("{e:#}"),
+ }
+ }
+ anyhow::bail!("failed to find any usable localized strings in the omnijar")
+ };
+
+ // The brand ftl is in the browser omnijar.
+ path.pop();
+ path.push("browser");
+ path.push("omni.ja");
+
+ let ftl_branding = 'branding: {
+ for locale in &locales {
+ match read_branding(&locale, &mut zip) {
+ Ok(v) => break 'branding v,
+ Err(e) => log::warn!("failed to read branding from omnijar: {e:#}"),
+ }
+ }
+ log::info!("using fallback branding info");
+ LanguageInfo::default().ftl_branding
+ };
+
+ Ok(LanguageInfo {
+ identifier: locale,
+ ftl_definitions,
+ ftl_branding,
+ })
+}
+
+/// Read the localized strings from the given zip archive (omnijar).
+fn read_strings(locale: &str, archive: &mut ZipArchive<File>) -> anyhow::Result<String> {
+ let mut file = archive
+ .by_name(&format!(
+ "localization/{locale}/crashreporter/crashreporter.ftl"
+ ))
+ .with_context(|| format!("failed to locate localization file for {locale}"))?;
+
+ let mut ftl_definitions = String::new();
+ file.read_to_string(&mut ftl_definitions)
+ .with_context(|| format!("failed to read localization file for {locale}"))?;
+
+ Ok(ftl_definitions)
+}
+
+/// Read the branding information from the given zip archive (omnijar).
+fn read_branding(locale: &str, archive: &mut ZipArchive<File>) -> anyhow::Result<String> {
+ let mut file = archive
+ .by_name(&format!("localization/{locale}/branding/brand.ftl"))
+ .with_context(|| format!("failed to locate branding localization file for {locale}"))?;
+ let mut s = String::new();
+ file.read_to_string(&mut s)
+ .with_context(|| format!("failed to read branding localization file for {locale}"))?;
+ Ok(s)
+}
+
+fn read_omnijar_file(path: &Path) -> anyhow::Result<ZipArchive<File>> {
+ ZipArchive::new(
+ File::open(&path).with_context(|| format!("failed to open {}", path.display()))?,
+ )
+ .with_context(|| format!("failed to read zip archive in {}", path.display()))
+}
diff --git a/toolkit/crashreporter/client/app/src/logging.rs b/toolkit/crashreporter/client/app/src/logging.rs
new file mode 100644
index 0000000000..c3f85312f8
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/logging.rs
@@ -0,0 +1,86 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! Application logging facilities.
+
+use crate::std::{
+ self,
+ path::Path,
+ sync::{Arc, Mutex},
+};
+
+/// Initialize logging and return a log target which can be used to change the destination of log
+/// statements.
+#[cfg_attr(mock, allow(unused))]
+pub fn init() -> LogTarget {
+ let log_target_inner = LogTargetInner::default();
+
+ env_logger::builder()
+ .parse_env(
+ env_logger::Env::new()
+ .filter("MOZ_CRASHEREPORTER")
+ .write_style("MOZ_CRASHREPORTER_STYLE"),
+ )
+ .target(env_logger::fmt::Target::Pipe(Box::new(
+ log_target_inner.clone(),
+ )))
+ .init();
+
+ LogTarget {
+ inner: log_target_inner,
+ }
+}
+
+/// Controls the target of logging.
+#[derive(Clone)]
+pub struct LogTarget {
+ inner: LogTargetInner,
+}
+
+impl LogTarget {
+ /// Set the file to which log statements will be written.
+ pub fn set_file(&self, path: &Path) {
+ match std::fs::File::create(path) {
+ Ok(file) => {
+ if let Ok(mut guard) = self.inner.target.lock() {
+ *guard = Box::new(file);
+ }
+ }
+ Err(e) => log::error!("failed to retarget log to {}: {e}", path.display()),
+ }
+ }
+}
+
+/// A private inner class implements Write, allows creation, etc. Externally the `LogTarget` only
+/// supports changing the target and nothing else.
+#[derive(Clone)]
+struct LogTargetInner {
+ target: Arc<Mutex<Box<dyn std::io::Write + Send + 'static>>>,
+}
+
+impl Default for LogTargetInner {
+ fn default() -> Self {
+ LogTargetInner {
+ target: Arc::new(Mutex::new(Box::new(std::io::stderr()))),
+ }
+ }
+}
+
+impl std::io::Write for LogTargetInner {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ let Ok(mut guard) = self.target.lock() else {
+ // Pretend we wrote successfully.
+ return Ok(buf.len());
+ };
+ guard.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ let Ok(mut guard) = self.target.lock() else {
+ // Pretend we flushed successfully.
+ return Ok(());
+ };
+ guard.flush()
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/logic.rs b/toolkit/crashreporter/client/app/src/logic.rs
new file mode 100644
index 0000000000..4ad1baa9c6
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/logic.rs
@@ -0,0 +1,660 @@
+/* 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()).unwrap_or_else(|e| {
+ log::warn!("failed to send crash ping: {e}");
+ None
+ });
+ 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 a crash ping to telemetry.
+ ///
+ /// Returns the crash ping uuid.
+ fn send_crash_ping(&self, minidump_hash: Option<&str>) -> anyhow::Result<Option<Uuid>> {
+ if self.config.ping_dir.is_none() {
+ log::warn!("not sending crash ping because no ping directory configured");
+ return Ok(None);
+ }
+
+ //TODO support glean crash pings (or change pingsender to do so)
+
+ let dump_id = self.config.local_dump_id();
+ let ping = net::legacy_telemetry::Ping::crash(&self.extra, dump_id.as_ref(), minidump_hash)
+ .context("failed to create telemetry crash ping")?;
+
+ let submission_url = ping
+ .submission_url(&self.extra)
+ .context("failed to generate ping submission URL")?;
+
+ let target_file = self
+ .config
+ .ping_dir
+ .as_ref()
+ .unwrap()
+ .join(format!("{}.json", ping.id()));
+
+ let file = std::fs::File::create(&target_file).with_context(|| {
+ format!(
+ "failed to open ping file {} for writing",
+ target_file.display()
+ )
+ })?;
+
+ serde_json::to_writer(file, &ping).context("failed to serialize telemetry crash ping")?;
+
+ let pingsender_path = self.config.sibling_program_path("pingsender");
+
+ crate::process::background_command(&pingsender_path)
+ .arg(submission_url)
+ .arg(target_file)
+ .spawn()
+ .with_context(|| {
+ format!(
+ "failed to launch pingsender process at {}",
+ pingsender_path.display()
+ )
+ })?;
+
+ // TODO asynchronously get pingsender result and log it?
+
+ Ok(Some(ping.id().clone()))
+ }
+
+ /// 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("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;
+ };
+
+ 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")
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/main.rs b/toolkit/crashreporter/client/app/src/main.rs
new file mode 100644
index 0000000000..07e1b04cb8
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/main.rs
@@ -0,0 +1,229 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! The crash reporter application.
+//!
+//! # Architecture
+//! The application uses a simple declarative [UI model](ui::model) to define the UI. This model
+//! contains [data bindings](data) which provide the dynamic behaviors of the UI. Separate UI
+//! implementations for linux (gtk), macos (cocoa), and windows (win32) exist, as well as a test UI
+//! which is virtual (no actual interface is presented) but allows runtime introspection.
+//!
+//! # Mocking
+//! This application contains mock interfaces for all the `std` functions it uses which interact
+//! with the host system. You can see their implementation in [`crate::std`]. To enable mocking,
+//! use the `mock` feature or build with `MOZ_CRASHREPORTER_MOCK` set (which, in `build.rs`, is
+//! translated to a `cfg` option). *Note* that this cfg _must_ be enabled when running tests.
+//! Unfortunately it is not possible to detect whether tests are being built in `build.rs, which
+//! is why a feature needed to be made in the first place (it is enabled automatically when running
+//! `mach rusttests`).
+//!
+//! Currently the input program configuration which is mocked when running the application is fixed
+//! (see the [`main`] implementation in this file). If needed in the future, it would be nice to
+//! extend this to allow runtime tweaking.
+//!
+//! # Development
+//! Because of the mocking support previously mentioned, in generally any `std` imports should
+//! actually use `crate::std`. If mocked functions/types are missing, they should be added with
+//! appropriate mocking hooks.
+
+// Use the WINDOWS windows subsystem. This prevents a console window from opening with the
+// application.
+#![cfg_attr(windows, windows_subsystem = "windows")]
+
+use crate::std::sync::Arc;
+use anyhow::Context;
+use config::Config;
+
+/// cc is short for Clone Capture, a shorthand way to clone a bunch of values before an expression
+/// (particularly useful for closures).
+///
+/// It is defined here to allow it to be used in all submodules (textual scope lookup).
+macro_rules! cc {
+ ( ($($c:ident),*) $e:expr ) => {
+ {
+ $(let $c = $c.clone();)*
+ $e
+ }
+ }
+}
+
+mod async_task;
+mod config;
+mod data;
+mod lang;
+mod logging;
+mod logic;
+mod net;
+mod process;
+mod settings;
+mod std;
+mod thread_bound;
+mod ui;
+
+#[cfg(test)]
+mod test;
+
+#[cfg(not(mock))]
+fn main() {
+ let log_target = logging::init();
+
+ let mut config = Config::new();
+ let config_result = config.read_from_environment();
+ config.log_target = Some(log_target);
+
+ let mut config = Arc::new(config);
+
+ let result = config_result.and_then(|()| {
+ let attempted_send = try_run(&mut config)?;
+ if !attempted_send {
+ // Exited without attempting to send the crash report; delete files.
+ config.delete_files();
+ }
+ Ok(())
+ });
+
+ if let Err(message) = result {
+ // TODO maybe errors should also delete files?
+ log::error!("exiting with error: {message}");
+ if !config.auto_submit {
+ // Only show a dialog if auto_submit is disabled.
+ ui::error_dialog(&config, message);
+ }
+ std::process::exit(1);
+ }
+}
+
+#[cfg(mock)]
+fn main() {
+ // TODO it'd be nice to be able to set these values at runtime in some way when running the
+ // mock application.
+
+ use crate::std::{
+ fs::{MockFS, MockFiles},
+ mock,
+ process::Command,
+ };
+ const MOCK_MINIDUMP_EXTRA: &str = r#"{
+ "Vendor": "FooCorp",
+ "ProductName": "Bar",
+ "ReleaseChannel": "release",
+ "BuildID": "1234",
+ "StackTraces": {
+ "status": "OK"
+ },
+ "Version": "100.0",
+ "ServerURL": "https://reports.example",
+ "TelemetryServerURL": "https://telemetry.example",
+ "TelemetryClientId": "telemetry_client",
+ "TelemetrySessionId": "telemetry_session",
+ "URL": "https://url.example"
+ }"#;
+
+ // Actual content doesn't matter, aside from the hash that is generated.
+ const MOCK_MINIDUMP_FILE: &[u8] = &[1, 2, 3, 4];
+ const MOCK_CURRENT_TIME: &str = "2004-11-09T12:34:56Z";
+ const MOCK_PING_UUID: uuid::Uuid = uuid::Uuid::nil();
+ const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000";
+
+ // Create a default set of files which allow successful operation.
+ let mock_files = MockFiles::new();
+ mock_files
+ .add_file("minidump.dmp", MOCK_MINIDUMP_FILE)
+ .add_file("minidump.extra", MOCK_MINIDUMP_EXTRA);
+
+ // Create a default mock environment which allows successful operation.
+ let mut mock = mock::builder();
+ mock.set(
+ Command::mock("work_dir/minidump-analyzer"),
+ Box::new(|_| Ok(crate::std::process::success_output())),
+ )
+ .set(
+ Command::mock("work_dir/pingsender"),
+ Box::new(|_| Ok(crate::std::process::success_output())),
+ )
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| {
+ let mut output = crate::std::process::success_output();
+ output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
+ // Network latency.
+ std::thread::sleep(std::time::Duration::from_secs(2));
+ Ok(output)
+ }),
+ )
+ .set(MockFS, mock_files.clone())
+ .set(
+ crate::std::env::MockCurrentExe,
+ "work_dir/crashreporter".into(),
+ )
+ .set(
+ crate::std::time::MockCurrentTime,
+ time::OffsetDateTime::parse(
+ MOCK_CURRENT_TIME,
+ &time::format_description::well_known::Rfc3339,
+ )
+ .unwrap()
+ .into(),
+ )
+ .set(mock::MockHook::new("ping_uuid"), MOCK_PING_UUID);
+
+ let result = mock.run(|| {
+ let mut cfg = Config::new();
+ cfg.data_dir = Some("data_dir".into());
+ cfg.events_dir = Some("events_dir".into());
+ cfg.ping_dir = Some("ping_dir".into());
+ cfg.dump_file = Some("minidump.dmp".into());
+ cfg.restart_command = Some("mockfox".into());
+ cfg.strings = Some(lang::load().unwrap());
+ let mut cfg = Arc::new(cfg);
+ try_run(&mut cfg)
+ });
+
+ if let Err(e) = result {
+ log::error!("exiting with error: {e}");
+ std::process::exit(1);
+ }
+}
+
+fn try_run(config: &mut Arc<Config>) -> anyhow::Result<bool> {
+ if config.dump_file.is_none() {
+ if !config.auto_submit {
+ Err(anyhow::anyhow!(config.string("crashreporter-information")))
+ } else {
+ Ok(false)
+ }
+ } else {
+ // Run minidump-analyzer to gather stack traces.
+ {
+ let analyzer_path = config.sibling_program_path("minidump-analyzer");
+ let mut cmd = crate::process::background_command(&analyzer_path);
+ if config.dump_all_threads {
+ cmd.arg("--full");
+ }
+ cmd.arg(config.dump_file());
+ let output = cmd
+ .output()
+ .with_context(|| config.string("crashreporter-error-minidump-analyzer"))?;
+ if !output.status.success() {
+ log::warn!(
+ "minidump-analyzer failed to run ({});\n\nstderr: {}\n\nstdout: {}",
+ output.status,
+ String::from_utf8_lossy(&output.stderr),
+ String::from_utf8_lossy(&output.stdout),
+ );
+ }
+ }
+
+ let extra = {
+ // Perform a few things which may change the config, then treat is as immutable.
+ let config = Arc::get_mut(config).expect("unexpected config references");
+ let extra = config.load_extra_file()?;
+ config.move_crash_data_to_pending()?;
+ extra
+ };
+
+ logic::ReportCrash::new(config.clone(), extra)?.run()
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs b/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs
new file mode 100644
index 0000000000..680f1614b0
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs
@@ -0,0 +1,177 @@
+/* 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/. */
+
+//! Support for legacy telemetry ping creation. The ping support serialization which should be used
+//! when submitting.
+
+use anyhow::Context;
+use serde::Serialize;
+use std::collections::BTreeMap;
+use uuid::Uuid;
+
+const TELEMETRY_VERSION: u64 = 4;
+const PAYLOAD_VERSION: u64 = 1;
+
+// Generated by `build.rs`.
+// static PING_ANNOTATIONS: phf::Set<&'static str>;
+include!(concat!(env!("OUT_DIR"), "/ping_annotations.rs"));
+
+#[derive(Serialize)]
+#[serde(tag = "type", rename_all = "camelCase")]
+pub enum Ping<'a> {
+ Crash {
+ id: Uuid,
+ version: u64,
+ #[serde(with = "time::serde::rfc3339")]
+ creation_date: time::OffsetDateTime,
+ client_id: &'a str,
+ #[serde(skip_serializing_if = "serde_json::Value::is_null")]
+ environment: serde_json::Value,
+ payload: Payload<'a>,
+ application: Application<'a>,
+ },
+}
+
+time::serde::format_description!(date_format, Date, "[year]-[month]-[day]");
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Payload<'a> {
+ session_id: &'a str,
+ version: u64,
+ #[serde(with = "date_format")]
+ crash_date: time::Date,
+ #[serde(with = "time::serde::rfc3339")]
+ crash_time: time::OffsetDateTime,
+ has_crash_environment: bool,
+ crash_id: &'a str,
+ minidump_sha256_hash: Option<&'a str>,
+ process_type: &'a str,
+ #[serde(skip_serializing_if = "serde_json::Value::is_null")]
+ stack_traces: serde_json::Value,
+ metadata: BTreeMap<&'a str, &'a str>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Application<'a> {
+ vendor: &'a str,
+ name: &'a str,
+ build_id: &'a str,
+ display_version: String,
+ platform_version: String,
+ version: &'a str,
+ channel: &'a str,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ architecture: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ xpcom_abi: Option<String>,
+}
+
+impl<'a> Ping<'a> {
+ pub fn crash(
+ extra: &'a serde_json::Value,
+ crash_id: &'a str,
+ minidump_sha256_hash: Option<&'a str>,
+ ) -> anyhow::Result<Self> {
+ let now: time::OffsetDateTime = crate::std::time::SystemTime::now().into();
+ let environment: serde_json::Value = extra["TelemetryEnvironment"]
+ .as_str()
+ .and_then(|estr| serde_json::from_str(estr).ok())
+ .unwrap_or_default();
+
+ // The subset of extra file entries (crash annotations) which are allowed in pings.
+ let metadata = extra
+ .as_object()
+ .map(|map| {
+ map.iter()
+ .filter_map(|(k, v)| {
+ PING_ANNOTATIONS
+ .contains(k)
+ .then(|| k.as_str())
+ .zip(v.as_str())
+ })
+ .collect()
+ })
+ .unwrap_or_default();
+
+ let display_version = environment
+ .pointer("/build/displayVersion")
+ .and_then(|s| s.as_str())
+ .unwrap_or_default()
+ .to_owned();
+ let platform_version = environment
+ .pointer("/build/platformVersion")
+ .and_then(|s| s.as_str())
+ .unwrap_or_default()
+ .to_owned();
+ let architecture = environment
+ .pointer("/build/architecture")
+ .and_then(|s| s.as_str())
+ .map(ToOwned::to_owned);
+ let xpcom_abi = environment
+ .pointer("/build/xpcomAbi")
+ .and_then(|s| s.as_str())
+ .map(ToOwned::to_owned);
+
+ Ok(Ping::Crash {
+ id: crate::std::mock::hook(Uuid::new_v4(), "ping_uuid"),
+ version: TELEMETRY_VERSION,
+ creation_date: now,
+ client_id: extra["TelemetryClientId"]
+ .as_str()
+ .context("missing TelemetryClientId")?,
+ environment,
+ payload: Payload {
+ session_id: extra["TelemetrySessionId"]
+ .as_str()
+ .context("missing TelemetrySessionId")?,
+ version: PAYLOAD_VERSION,
+ crash_date: now.date(),
+ crash_time: now,
+ has_crash_environment: true,
+ crash_id,
+ minidump_sha256_hash,
+ process_type: "main",
+ stack_traces: extra["StackTraces"].clone(),
+ metadata,
+ },
+ application: Application {
+ vendor: extra["Vendor"].as_str().unwrap_or_default(),
+ name: extra["ProductName"].as_str().unwrap_or_default(),
+ build_id: extra["BuildID"].as_str().unwrap_or_default(),
+ display_version,
+ platform_version,
+ version: extra["Version"].as_str().unwrap_or_default(),
+ channel: extra["ReleaseChannel"].as_str().unwrap_or_default(),
+ architecture,
+ xpcom_abi,
+ },
+ })
+ }
+
+ /// Generate the telemetry URL for submitting this ping.
+ pub fn submission_url(&self, extra: &serde_json::Value) -> anyhow::Result<String> {
+ let url = extra["TelemetryServerURL"]
+ .as_str()
+ .context("missing TelemetryServerURL")?;
+ let id = self.id();
+ let name = extra["ProductName"]
+ .as_str()
+ .context("missing ProductName")?;
+ let version = extra["Version"].as_str().context("missing Version")?;
+ let channel = extra["ReleaseChannel"]
+ .as_str()
+ .context("missing ReleaseChannel")?;
+ let buildid = extra["BuildID"].as_str().context("missing BuildID")?;
+ Ok(format!("{url}/submit/telemetry/{id}/crash/{name}/{version}/{channel}/{buildid}?v={TELEMETRY_VERSION}"))
+ }
+
+ /// Get the ping identifier.
+ pub fn id(&self) -> &Uuid {
+ match self {
+ Ping::Crash { id, .. } => id,
+ }
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/net/libcurl.rs b/toolkit/crashreporter/client/app/src/net/libcurl.rs
new file mode 100644
index 0000000000..0adfd7d4b4
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/net/libcurl.rs
@@ -0,0 +1,406 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! Partial libcurl bindings with some wrappers for safe cleanup.
+
+use crate::std::path::Path;
+use libloading::{Library, Symbol};
+use once_cell::sync::Lazy;
+use std::ffi::{c_char, c_long, c_uint, CStr, CString};
+
+// Constants lifted from `curl.h`
+const CURLE_OK: CurlCode = 0;
+const CURL_ERROR_SIZE: usize = 256;
+
+const CURLOPTTYPE_LONG: CurlOption = 0;
+const CURLOPTTYPE_OBJECTPOINT: CurlOption = 10000;
+const CURLOPTTYPE_FUNCTIONPOINT: CurlOption = 20000;
+const CURLOPTTYPE_STRINGPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT;
+const CURLOPTTYPE_CBPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT;
+
+const CURLOPT_WRITEDATA: CurlOption = CURLOPTTYPE_CBPOINT + 1;
+const CURLOPT_URL: CurlOption = CURLOPTTYPE_STRINGPOINT + 2;
+const CURLOPT_ERRORBUFFER: CurlOption = CURLOPTTYPE_OBJECTPOINT + 10;
+const CURLOPT_WRITEFUNCTION: CurlOption = CURLOPTTYPE_FUNCTIONPOINT + 11;
+const CURLOPT_USERAGENT: CurlOption = CURLOPTTYPE_STRINGPOINT + 18;
+const CURLOPT_MIMEPOST: CurlOption = CURLOPTTYPE_OBJECTPOINT + 269;
+const CURLOPT_MAXREDIRS: CurlOption = CURLOPTTYPE_LONG + 68;
+
+const CURLINFO_LONG: CurlInfo = 0x200000;
+const CURLINFO_RESPONSE_CODE: CurlInfo = CURLINFO_LONG + 2;
+
+const CURL_LIB_NAMES: &[&str] = if cfg!(target_os = "linux") {
+ &[
+ "libcurl.so",
+ "libcurl.so.4",
+ // Debian gives libcurl a different name when it is built against GnuTLS
+ "libcurl-gnutls.so",
+ "libcurl-gnutls.so.4",
+ // Older versions in case we find nothing better
+ "libcurl.so.3",
+ "libcurl-gnutls.so.3", // See above for Debian
+ ]
+} else if cfg!(target_os = "macos") {
+ &[
+ "/usr/lib/libcurl.dylib",
+ "/usr/lib/libcurl.4.dylib",
+ "/usr/lib/libcurl.3.dylib",
+ ]
+} else if cfg!(target_os = "windows") {
+ &["libcurl.dll", "curl.dll"]
+} else {
+ &[]
+};
+
+// Shim until min rust version 1.74 which allows std::io::Error::other
+fn error_other<E>(error: E) -> std::io::Error
+where
+ E: Into<Box<dyn std::error::Error + Send + Sync>>,
+{
+ std::io::Error::new(std::io::ErrorKind::Other, error)
+}
+
+#[repr(transparent)]
+#[derive(Clone, Copy)]
+struct CurlHandle(*mut ());
+type CurlCode = c_uint;
+type CurlOption = c_uint;
+type CurlInfo = c_uint;
+#[repr(transparent)]
+#[derive(Clone, Copy)]
+struct CurlMime(*mut ());
+#[repr(transparent)]
+#[derive(Clone, Copy)]
+struct CurlMimePart(*mut ());
+
+macro_rules! library_binding {
+ ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] fn $name:ident $args:tt $( -> $ret:ty )? ; $($rest:tt)* ) => {
+ library_binding! {
+ $localname
+ members[
+ $($members)*
+ $name: Symbol<'static, unsafe extern fn $args $(->$ret)?>,
+ ]
+ load[
+ $($load)*
+ $name: unsafe {
+ let symbol = $localname.get::<unsafe extern fn $args $(->$ret)?>(stringify!($name).as_bytes())
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ // All symbols refer to library, so `'static` lifetimes are safe (`library`
+ // will outlive them).
+ std::mem::transmute(symbol)
+ },
+ ]
+ $($rest)*
+ }
+ };
+ ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] ) => {
+ pub struct Curl {
+ $($members)*
+ _library: Library
+ }
+
+ impl Curl {
+ fn load() -> std::io::Result<Self> {
+ // Try each of the libraries, debug-logging load failures.
+ let library = CURL_LIB_NAMES.iter().find_map(|name| {
+ log::debug!("attempting to load {name}");
+ match unsafe { Library::new(name) } {
+ Ok(lib) => {
+ log::info!("loaded {name}");
+ Some(lib)
+ }
+ Err(e) => {
+ log::debug!("error when loading {name}: {e}");
+ None
+ }
+ }
+ });
+
+ let $localname = library.ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::NotFound, "failed to find curl library")
+ })?;
+
+ Ok(Curl { $($load)* _library: $localname })
+ }
+ }
+ };
+ ( $($rest:tt)* ) => {
+ library_binding! {
+ library members[] load[] $($rest)*
+ }
+ }
+}
+
+library_binding! {
+ fn curl_easy_init() -> CurlHandle;
+ fn curl_easy_setopt(CurlHandle, CurlOption, ...) -> CurlCode;
+ fn curl_easy_perform(CurlHandle) -> CurlCode;
+ fn curl_easy_getinfo(CurlHandle, CurlInfo, ...) -> CurlCode;
+ fn curl_easy_cleanup(CurlHandle);
+ fn curl_mime_init(CurlHandle) -> CurlMime;
+ fn curl_mime_addpart(CurlMime) -> CurlMimePart;
+ fn curl_mime_name(CurlMimePart, *const c_char) -> CurlCode;
+ fn curl_mime_filename(CurlMimePart, *const c_char) -> CurlCode;
+ fn curl_mime_type(CurlMimePart, *const c_char) -> CurlCode;
+ fn curl_mime_data(CurlMimePart, *const c_char, usize) -> CurlCode;
+ fn curl_mime_filedata(CurlMimePart, *const c_char) -> CurlCode;
+ fn curl_mime_free(CurlMime);
+}
+
+/// Load libcurl if possible.
+pub fn load() -> std::io::Result<&'static Curl> {
+ static CURL: Lazy<std::io::Result<Curl>> = Lazy::new(Curl::load);
+ CURL.as_ref().map_err(error_other)
+}
+
+#[derive(Debug)]
+pub struct Error {
+ code: CurlCode,
+ error: Option<String>,
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "curl error code {}", self.code)?;
+ if let Some(e) = &self.error {
+ write!(f, ": {e}")?;
+ }
+ Ok(())
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl From<Error> for std::io::Error {
+ fn from(e: Error) -> Self {
+ error_other(e)
+ }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+fn to_result(code: CurlCode) -> Result<()> {
+ if code == CURLE_OK {
+ Ok(())
+ } else {
+ Err(Error { code, error: None })
+ }
+}
+
+impl Curl {
+ pub fn easy(&self) -> std::io::Result<Easy> {
+ let handle = unsafe { (self.curl_easy_init)() };
+ if handle.0.is_null() {
+ Err(error_other("curl_easy_init failed"))
+ } else {
+ Ok(Easy {
+ lib: self,
+ handle,
+ mime: Default::default(),
+ })
+ }
+ }
+}
+
+struct ErrorBuffer([u8; CURL_ERROR_SIZE]);
+
+impl Default for ErrorBuffer {
+ fn default() -> Self {
+ ErrorBuffer([0; CURL_ERROR_SIZE])
+ }
+}
+
+pub struct Easy<'a> {
+ lib: &'a Curl,
+ handle: CurlHandle,
+ mime: Option<Mime<'a>>,
+}
+
+impl<'a> Easy<'a> {
+ pub fn set_url(&mut self, url: &str) -> Result<()> {
+ let url = CString::new(url.to_string()).unwrap();
+ to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_URL, url.as_ptr()) })
+ }
+
+ pub fn set_user_agent(&mut self, user_agent: &str) -> Result<()> {
+ let ua = CString::new(user_agent.to_string()).unwrap();
+ to_result(unsafe {
+ (self.lib.curl_easy_setopt)(self.handle, CURLOPT_USERAGENT, ua.as_ptr())
+ })
+ }
+
+ pub fn mime(&self) -> std::io::Result<Mime<'a>> {
+ let handle = unsafe { (self.lib.curl_mime_init)(self.handle) };
+ if handle.0.is_null() {
+ Err(error_other("curl_mime_init failed"))
+ } else {
+ Ok(Mime {
+ lib: self.lib,
+ handle,
+ })
+ }
+ }
+
+ pub fn set_mime_post(&mut self, mime: Mime<'a>) -> Result<()> {
+ let result = to_result(unsafe {
+ (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MIMEPOST, mime.handle)
+ });
+ if result.is_ok() {
+ self.mime = Some(mime);
+ }
+ result
+ }
+
+ pub fn set_max_redirs(&mut self, redirs: c_long) -> Result<()> {
+ to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MAXREDIRS, redirs) })
+ }
+
+ /// Returns the response data on success.
+ pub fn perform(&self) -> Result<Vec<u8>> {
+ // Set error buffer, but degrade service if it doesn't work.
+ let mut error_buffer = ErrorBuffer::default();
+ let error_buffer_set = unsafe {
+ (self.lib.curl_easy_setopt)(
+ self.handle,
+ CURLOPT_ERRORBUFFER,
+ error_buffer.0.as_mut_ptr() as *mut c_char,
+ )
+ } == CURLE_OK;
+
+ // Set the write function to fill a Vec. If there is a panic, this might leave stale
+ // pointers in the curl options, but they won't be used without another perform, at which
+ // point they'll be overwritten.
+ let mut data: Vec<u8> = Vec::new();
+ extern "C" fn write_callback(
+ data: *const u8,
+ size: usize,
+ nmemb: usize,
+ dest: &mut Vec<u8>,
+ ) -> usize {
+ let total = size * nmemb;
+ dest.extend(unsafe { std::slice::from_raw_parts(data, total) });
+ total
+ }
+ unsafe {
+ to_result((self.lib.curl_easy_setopt)(
+ self.handle,
+ CURLOPT_WRITEFUNCTION,
+ write_callback as extern "C" fn(*const u8, usize, usize, &mut Vec<u8>) -> usize,
+ ))?;
+ to_result((self.lib.curl_easy_setopt)(
+ self.handle,
+ CURLOPT_WRITEDATA,
+ &mut data as *mut _,
+ ))?;
+ };
+
+ let mut result = to_result(unsafe { (self.lib.curl_easy_perform)(self.handle) });
+
+ // Clean up a bit by unsetting the write function and write data, though they won't be used
+ // anywhere else. Ignore return values.
+ unsafe {
+ (self.lib.curl_easy_setopt)(
+ self.handle,
+ CURLOPT_WRITEFUNCTION,
+ std::ptr::null_mut::<()>(),
+ );
+ (self.lib.curl_easy_setopt)(self.handle, CURLOPT_WRITEDATA, std::ptr::null_mut::<()>());
+ }
+
+ if error_buffer_set {
+ unsafe {
+ (self.lib.curl_easy_setopt)(
+ self.handle,
+ CURLOPT_ERRORBUFFER,
+ std::ptr::null_mut::<()>(),
+ )
+ };
+ if let Err(e) = &mut result {
+ if let Ok(cstr) = CStr::from_bytes_until_nul(error_buffer.0.as_slice()) {
+ e.error = Some(cstr.to_string_lossy().into_owned());
+ }
+ }
+ }
+
+ result.map(move |()| data)
+ }
+
+ pub fn get_response_code(&self) -> Result<u64> {
+ let mut code = c_long::default();
+ to_result(unsafe {
+ (self.lib.curl_easy_getinfo)(
+ self.handle,
+ CURLINFO_RESPONSE_CODE,
+ &mut code as *mut c_long,
+ )
+ })?;
+ Ok(code.try_into().expect("negative http response code"))
+ }
+}
+
+impl Drop for Easy<'_> {
+ fn drop(&mut self) {
+ self.mime.take();
+ unsafe { (self.lib.curl_easy_cleanup)(self.handle) };
+ }
+}
+
+pub struct Mime<'a> {
+ lib: &'a Curl,
+ handle: CurlMime,
+}
+
+impl<'a> Mime<'a> {
+ pub fn add_part(&mut self) -> std::io::Result<MimePart<'a>> {
+ let handle = unsafe { (self.lib.curl_mime_addpart)(self.handle) };
+ if handle.0.is_null() {
+ Err(error_other("curl_mime_addpart failed"))
+ } else {
+ Ok(MimePart {
+ lib: self.lib,
+ handle,
+ })
+ }
+ }
+}
+
+impl Drop for Mime<'_> {
+ fn drop(&mut self) {
+ unsafe { (self.lib.curl_mime_free)(self.handle) };
+ }
+}
+
+pub struct MimePart<'a> {
+ lib: &'a Curl,
+ handle: CurlMimePart,
+}
+
+impl MimePart<'_> {
+ pub fn set_name(&mut self, name: &str) -> Result<()> {
+ let name = CString::new(name.to_string()).unwrap();
+ to_result(unsafe { (self.lib.curl_mime_name)(self.handle, name.as_ptr()) })
+ }
+
+ pub fn set_filename(&mut self, filename: &str) -> Result<()> {
+ let filename = CString::new(filename.to_string()).unwrap();
+ to_result(unsafe { (self.lib.curl_mime_filename)(self.handle, filename.as_ptr()) })
+ }
+
+ pub fn set_type(&mut self, mime_type: &str) -> Result<()> {
+ let mime_type = CString::new(mime_type.to_string()).unwrap();
+ to_result(unsafe { (self.lib.curl_mime_type)(self.handle, mime_type.as_ptr()) })
+ }
+
+ pub fn set_filedata(&mut self, file: &Path) -> Result<()> {
+ let file = CString::new(file.display().to_string()).unwrap();
+ to_result(unsafe { (self.lib.curl_mime_filedata)(self.handle, file.as_ptr()) })
+ }
+
+ pub fn set_data(&mut self, data: &[u8]) -> Result<()> {
+ to_result(unsafe {
+ (self.lib.curl_mime_data)(self.handle, data.as_ptr() as *const c_char, data.len())
+ })
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/net/mod.rs b/toolkit/crashreporter/client/app/src/net/mod.rs
new file mode 100644
index 0000000000..d9951d7fc3
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/net/mod.rs
@@ -0,0 +1,12 @@
+/* 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/. */
+
+pub mod legacy_telemetry;
+mod libcurl;
+pub mod report;
+
+#[cfg(test)]
+pub fn can_load_libcurl() -> bool {
+ libcurl::load().is_ok()
+}
diff --git a/toolkit/crashreporter/client/app/src/net/report.rs b/toolkit/crashreporter/client/app/src/net/report.rs
new file mode 100644
index 0000000000..46be952547
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/net/report.rs
@@ -0,0 +1,276 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! Support for crash report creation and upload.
+//!
+//! Upload currently uses the system libcurl or curl binary rather than a rust network stack (as
+//! curl is more mature, albeit the code to interact with it must be a bit more careful).
+
+use crate::std::{ffi::OsStr, path::Path, process::Child};
+use anyhow::Context;
+
+#[cfg(mock)]
+use crate::std::mock::{mock_key, MockKey};
+
+#[cfg(mock)]
+mock_key! {
+ pub struct MockLibCurl => Box<dyn Fn(&CrashReport) -> std::io::Result<std::io::Result<String>> + Send + Sync>
+}
+
+pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
+
+/// A crash report to upload.
+///
+/// Post a multipart form payload to the report URL.
+///
+/// The form data contains:
+/// | name | filename | content | mime |
+/// ====================================
+/// | `extra` | `extra.json` | extra json object | `application/json`|
+/// | `upload_file_minidump` | dump file name | dump file contents | derived (probably application/binary) |
+/// if present:
+/// | `memory_report` | memory file name | memory file contents | derived (probably gzipped json) |
+pub struct CrashReport<'a> {
+ pub extra: &'a serde_json::Value,
+ pub dump_file: &'a Path,
+ pub memory_file: Option<&'a Path>,
+ pub url: &'a OsStr,
+}
+
+impl CrashReport<'_> {
+ /// Send the crash report.
+ pub fn send(&self) -> std::io::Result<CrashReportSender> {
+ // Windows 10+ and macOS 10.15+ contain `curl` 7.64.1+ as a system-provided binary, so
+ // `send_with_curl_binary` should not fail.
+ //
+ // Linux distros generally do not contain `curl`, but `libcurl` is very likely to be
+ // incidentally installed (if not outright part of the distro base packages). Based on a
+ // cursory look at the debian repositories as an examplar, the curl binary is much less
+ // likely to be incidentally installed.
+ //
+ // For uniformity, we always will try the curl binary first, then try libcurl if that
+ // fails.
+
+ let extra_json_data = serde_json::to_string(self.extra)?;
+
+ self.send_with_curl_binary(extra_json_data.clone())
+ .or_else(|e| {
+ log::info!("failed to invoke curl ({e}), trying libcurl");
+ self.send_with_libcurl(extra_json_data.clone())
+ })
+ }
+
+ /// Send the crash report using the `curl` binary.
+ fn send_with_curl_binary(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> {
+ let mut cmd = crate::process::background_command("curl");
+
+ cmd.args(["--user-agent", USER_AGENT]);
+
+ cmd.arg("--form");
+ // `@-` causes the data to be read from stdin, which is desirable to not have to worry
+ // about process argument string length limitations (though they are generally pretty high
+ // limits).
+ cmd.arg("extra=@-;filename=extra.json;type=application/json");
+
+ cmd.arg("--form");
+ cmd.arg(format!(
+ "upload_file_minidump=@{}",
+ CurlQuote(&self.dump_file.display().to_string())
+ ));
+
+ if let Some(path) = self.memory_file {
+ cmd.arg("--form");
+ cmd.arg(format!(
+ "memory_report=@{}",
+ CurlQuote(&path.display().to_string())
+ ));
+ }
+
+ cmd.arg(self.url);
+
+ cmd.stdin(std::process::Stdio::piped());
+ cmd.stdout(std::process::Stdio::piped());
+ cmd.stderr(std::process::Stdio::piped());
+
+ cmd.spawn().map(move |child| CrashReportSender::CurlChild {
+ child,
+ extra_json_data,
+ })
+ }
+
+ /// Send the crash report using the `curl` library.
+ fn send_with_libcurl(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> {
+ #[cfg(mock)]
+ if !crate::std::mock::try_hook(false, "use_system_libcurl") {
+ return self.send_with_mock_libcurl(extra_json_data);
+ }
+
+ let curl = super::libcurl::load()?;
+ let mut easy = curl.easy()?;
+
+ easy.set_url(&self.url.to_string_lossy())?;
+ easy.set_user_agent(USER_AGENT)?;
+ easy.set_max_redirs(30)?;
+
+ let mut mime = easy.mime()?;
+ {
+ let mut part = mime.add_part()?;
+ part.set_name("extra")?;
+ part.set_filename("extra.json")?;
+ part.set_type("application/json")?;
+ part.set_data(extra_json_data.as_bytes())?;
+ }
+ {
+ let mut part = mime.add_part()?;
+ part.set_name("upload_file_minidump")?;
+ part.set_filename(&self.dump_file.display().to_string())?;
+ part.set_filedata(self.dump_file)?;
+ }
+ if let Some(path) = self.memory_file {
+ let mut part = mime.add_part()?;
+ part.set_name("memory_report")?;
+ part.set_filename(&path.display().to_string())?;
+ part.set_filedata(path)?;
+ }
+ easy.set_mime_post(mime)?;
+
+ Ok(CrashReportSender::LibCurl { easy })
+ }
+
+ #[cfg(mock)]
+ fn send_with_mock_libcurl(
+ &self,
+ _extra_json_data: String,
+ ) -> std::io::Result<CrashReportSender> {
+ MockLibCurl
+ .get(|f| f(&self))
+ .map(|response| CrashReportSender::MockLibCurl { response })
+ }
+}
+
+pub enum CrashReportSender {
+ CurlChild {
+ child: Child,
+ extra_json_data: String,
+ },
+ LibCurl {
+ easy: super::libcurl::Easy<'static>,
+ },
+ #[cfg(mock)]
+ MockLibCurl {
+ response: std::io::Result<String>,
+ },
+}
+
+impl CrashReportSender {
+ pub fn finish(self) -> anyhow::Result<Response> {
+ let response = match self {
+ Self::CurlChild {
+ mut child,
+ extra_json_data,
+ } => {
+ {
+ let mut stdin = child
+ .stdin
+ .take()
+ .context("failed to get curl process stdin")?;
+ std::io::copy(&mut std::io::Cursor::new(extra_json_data), &mut stdin)
+ .context("failed to write extra file data to stdin of curl process")?;
+ // stdin is dropped at the end of this scope so that the stream gets an EOF,
+ // otherwise curl will wait for more input.
+ }
+ let output = child
+ .wait_with_output()
+ .context("failed to wait on curl process")?;
+ anyhow::ensure!(
+ output.status.success(),
+ "process failed (exit status {}) with stderr: {}",
+ output.status,
+ String::from_utf8_lossy(&output.stderr)
+ );
+ String::from_utf8_lossy(&output.stdout).into_owned()
+ }
+ Self::LibCurl { easy } => {
+ let response = easy.perform()?;
+ let response_code = easy.get_response_code()?;
+
+ let response = String::from_utf8_lossy(&response).into_owned();
+ dbg!(&response, &response_code);
+
+ anyhow::ensure!(
+ response_code == 200,
+ "unexpected response code ({response_code}): {response}"
+ );
+
+ response
+ }
+ #[cfg(mock)]
+ Self::MockLibCurl { response } => response?.into(),
+ };
+
+ log::debug!("received response from sending report: {:?}", &*response);
+ Ok(Response::parse(response))
+ }
+}
+
+/// A parsed response from submitting a crash report.
+#[derive(Default, Debug)]
+pub struct Response {
+ pub crash_id: Option<String>,
+ pub stop_sending_reports_for: Option<String>,
+ pub view_url: Option<String>,
+ pub discarded: bool,
+}
+
+impl Response {
+ /// Parse a server response.
+ ///
+ /// The response should be newline-separated `<key>=<value>` pairs.
+ fn parse<S: AsRef<str>>(response: S) -> Self {
+ let mut ret = Self::default();
+ // Fields may be omitted, and parsing is best-effort but will not produce any errors (just
+ // a default Response struct).
+ for line in response.as_ref().lines() {
+ if let Some((key, value)) = line.split_once('=') {
+ match key {
+ "StopSendingReportsFor" => {
+ ret.stop_sending_reports_for = Some(value.to_owned())
+ }
+ "Discarded" => ret.discarded = true,
+ "CrashID" => ret.crash_id = Some(value.to_owned()),
+ "ViewURL" => ret.view_url = Some(value.to_owned()),
+ _ => (),
+ }
+ }
+ }
+ ret
+ }
+}
+
+/// Quote a string per https://curl.se/docs/manpage.html#-F.
+/// That is, add quote characters and escape " and \ with backslashes.
+struct CurlQuote<'a>(&'a str);
+impl std::fmt::Display for CurlQuote<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use std::fmt::Write;
+
+ f.write_char('"')?;
+ const ESCAPE_CHARS: [char; 2] = ['"', '\\'];
+ for substr in self.0.split_inclusive(ESCAPE_CHARS) {
+ // The last string returned by `split_inclusive` may or may not contain the
+ // search character, unfortunately.
+ if substr.ends_with(ESCAPE_CHARS) {
+ // Safe to use a byte offset rather than a character offset because the
+ // ESCAPE_CHARS are each 1 byte in utf8.
+ let (s, escape) = substr.split_at(substr.len() - 1);
+ f.write_str(s)?;
+ f.write_char('\\')?;
+ f.write_str(escape)?;
+ } else {
+ f.write_str(substr)?;
+ }
+ }
+ f.write_char('"')
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/process.rs b/toolkit/crashreporter/client/app/src/process.rs
new file mode 100644
index 0000000000..126e5b533b
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/process.rs
@@ -0,0 +1,23 @@
+/* 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/. */
+
+//! Process utility functions.
+
+use crate::std::{ffi::OsStr, process::Command};
+
+/// Return a command configured to run in the background.
+///
+/// This means that no associated console will be opened, when applicable.
+pub fn background_command<S: AsRef<OsStr>>(program: S) -> Command {
+ #[allow(unused_mut)]
+ let mut cmd = Command::new(program);
+ #[cfg(windows)]
+ {
+ #[cfg_attr(mock, allow(unused))]
+ use std::os::windows::process::CommandExt;
+ use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
+ cmd.creation_flags(CREATE_NO_WINDOW);
+ }
+ cmd
+}
diff --git a/toolkit/crashreporter/client/app/src/settings.rs b/toolkit/crashreporter/client/app/src/settings.rs
new file mode 100644
index 0000000000..7ea0e4fe26
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/settings.rs
@@ -0,0 +1,39 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+//! Persistent settings of the application.
+
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+pub struct Settings {
+ /// Whether a crash report should be sent.
+ pub submit_report: bool,
+ /// Whether the URL that was open should be included in a sent report.
+ pub include_url: bool,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Settings {
+ submit_report: true,
+ include_url: false,
+ }
+ }
+}
+
+impl Settings {
+ /// Write the settings to the given writer.
+ pub fn to_writer<W: std::io::Write>(&self, writer: W) -> anyhow::Result<()> {
+ Ok(serde_json::to_writer_pretty(writer, self)?)
+ }
+
+ /// Read the settings from the given reader.
+ pub fn from_reader<R: std::io::Read>(reader: R) -> anyhow::Result<Self> {
+ Ok(serde_json::from_reader(reader)?)
+ }
+
+ #[cfg(test)]
+ pub fn to_string(&self) -> String {
+ serde_json::to_string_pretty(self).unwrap()
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/env.rs b/toolkit/crashreporter/client/app/src/std/env.rs
new file mode 100644
index 0000000000..edc22ded8d
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/env.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+use super::mock::{mock_key, MockKey};
+pub use std::env::VarError;
+use std::ffi::{OsStr, OsString};
+
+mock_key! {
+ pub struct MockCurrentExe => std::path::PathBuf
+}
+
+pub struct ArgsOs {
+ argv0: Option<OsString>,
+}
+
+impl Iterator for ArgsOs {
+ type Item = OsString;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ Some(
+ self.argv0
+ .take()
+ .expect("only argv[0] is available when mocked"),
+ )
+ }
+}
+
+pub fn var<K: AsRef<OsStr>>(_key: K) -> Result<String, VarError> {
+ unimplemented!("no var access in tests")
+}
+
+pub fn var_os<K: AsRef<OsStr>>(_key: K) -> Option<OsString> {
+ unimplemented!("no var access in tests")
+}
+
+pub fn args_os() -> ArgsOs {
+ MockCurrentExe.get(|r| ArgsOs {
+ argv0: Some(r.clone().into()),
+ })
+}
+
+pub fn current_exe() -> std::io::Result<super::path::PathBuf> {
+ Ok(MockCurrentExe.get(|r| r.clone().into()))
+}
diff --git a/toolkit/crashreporter/client/app/src/std/fs.rs b/toolkit/crashreporter/client/app/src/std/fs.rs
new file mode 100644
index 0000000000..8ba2c572d5
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/fs.rs
@@ -0,0 +1,559 @@
+/* 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/. */
+
+use crate::std::mock::{mock_key, MockKey};
+use std::collections::HashMap;
+use std::ffi::OsString;
+use std::io::{ErrorKind, Read, Result, Seek, SeekFrom, Write};
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+use std::time::SystemTime;
+
+/// Mock filesystem file content.
+#[derive(Debug, Default, Clone)]
+pub struct MockFileContent(Arc<Mutex<Vec<u8>>>);
+
+impl MockFileContent {
+ pub fn empty() -> Self {
+ Self::default()
+ }
+
+ pub fn new(data: String) -> Self {
+ Self::new_bytes(data.into())
+ }
+
+ pub fn new_bytes(data: Vec<u8>) -> Self {
+ MockFileContent(Arc::new(Mutex::new(data)))
+ }
+}
+
+impl From<()> for MockFileContent {
+ fn from(_: ()) -> Self {
+ Self::empty()
+ }
+}
+
+impl From<String> for MockFileContent {
+ fn from(s: String) -> Self {
+ Self::new(s)
+ }
+}
+
+impl From<&str> for MockFileContent {
+ fn from(s: &str) -> Self {
+ Self::new(s.to_owned())
+ }
+}
+
+impl From<Vec<u8>> for MockFileContent {
+ fn from(bytes: Vec<u8>) -> Self {
+ Self::new_bytes(bytes)
+ }
+}
+
+impl From<&[u8]> for MockFileContent {
+ fn from(bytes: &[u8]) -> Self {
+ Self::new_bytes(bytes.to_owned())
+ }
+}
+
+/// Mocked filesystem directory entries.
+pub type MockDirEntries = HashMap<OsString, MockFSItem>;
+
+/// The content of a mock filesystem item.
+pub enum MockFSContent {
+ /// File content.
+ File(Result<MockFileContent>),
+ /// A directory with the given entries.
+ Dir(MockDirEntries),
+}
+
+impl std::fmt::Debug for MockFSContent {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::File(_) => f.debug_tuple("File").finish(),
+ Self::Dir(e) => f.debug_tuple("Dir").field(e).finish(),
+ }
+ }
+}
+
+/// A mock filesystem item.
+#[derive(Debug)]
+pub struct MockFSItem {
+ /// The content of the item (file/dir).
+ pub content: MockFSContent,
+ /// The modification time of the item.
+ pub modified: SystemTime,
+}
+
+impl From<MockFSContent> for MockFSItem {
+ fn from(content: MockFSContent) -> Self {
+ MockFSItem {
+ content,
+ modified: SystemTime::UNIX_EPOCH,
+ }
+ }
+}
+
+/// A mock filesystem.
+#[derive(Debug, Clone)]
+pub struct MockFiles {
+ root: Arc<Mutex<MockFSItem>>,
+}
+
+impl Default for MockFiles {
+ fn default() -> Self {
+ MockFiles {
+ root: Arc::new(Mutex::new(MockFSContent::Dir(Default::default()).into())),
+ }
+ }
+}
+
+impl MockFiles {
+ /// Create a new, empty filesystem.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Add a mocked file with the given content. The modification time will be the unix epoch.
+ ///
+ /// Pancis if the parent directory is not already mocked.
+ pub fn add_file<P: AsRef<Path>, C: Into<MockFileContent>>(&self, path: P, content: C) -> &Self {
+ self.add_file_result(path, Ok(content.into()), SystemTime::UNIX_EPOCH)
+ }
+
+ /// Add a mocked directory.
+ pub fn add_dir<P: AsRef<Path>>(&self, path: P) -> &Self {
+ self.path(path, true, |_| ()).unwrap();
+ self
+ }
+
+ /// Add a mocked file that returns the given result and has the given modification time.
+ ///
+ /// Pancis if the parent directory is not already mocked.
+ pub fn add_file_result<P: AsRef<Path>>(
+ &self,
+ path: P,
+ result: Result<MockFileContent>,
+ modified: SystemTime,
+ ) -> &Self {
+ let name = path.as_ref().file_name().expect("invalid path");
+ self.parent_dir(path.as_ref(), move |dir| {
+ if dir.contains_key(name) {
+ Err(ErrorKind::AlreadyExists.into())
+ } else {
+ dir.insert(
+ name.to_owned(),
+ MockFSItem {
+ content: MockFSContent::File(result),
+ modified,
+ },
+ );
+ Ok(())
+ }
+ })
+ .and_then(|r| r)
+ .unwrap();
+ self
+ }
+
+ /// If create_dirs is true, all missing path components (_including the final component_) are
+ /// created as directories. In this case `Err` is only returned if a file conflicts with
+ /// a directory component.
+ pub fn path<P: AsRef<Path>, F, R>(&self, path: P, create_dirs: bool, f: F) -> Result<R>
+ where
+ F: FnOnce(&mut MockFSItem) -> R,
+ {
+ let mut guard = self.root.lock().unwrap();
+ let mut cur_entry = &mut *guard;
+ for component in path.as_ref().components() {
+ use std::path::Component::*;
+ match component {
+ CurDir | RootDir | Prefix(_) => continue,
+ ParentDir => panic!("unsupported path: {}", path.as_ref().display()),
+ Normal(name) => {
+ let cur_dir = match &mut cur_entry.content {
+ MockFSContent::File(_) => return Err(ErrorKind::NotFound.into()),
+ MockFSContent::Dir(d) => d,
+ };
+ cur_entry = if create_dirs {
+ cur_dir
+ .entry(name.to_owned())
+ .or_insert_with(|| MockFSContent::Dir(Default::default()).into())
+ } else {
+ cur_dir.get_mut(name).ok_or(ErrorKind::NotFound)?
+ };
+ }
+ }
+ }
+ Ok(f(cur_entry))
+ }
+
+ /// Get the mocked parent directory of the given path and call a callback on the mocked
+ /// directory's entries.
+ pub fn parent_dir<P: AsRef<Path>, F, R>(&self, path: P, f: F) -> Result<R>
+ where
+ F: FnOnce(&mut MockDirEntries) -> R,
+ {
+ self.path(
+ path.as_ref().parent().unwrap_or(&Path::new("")),
+ false,
+ move |item| match &mut item.content {
+ MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
+ MockFSContent::Dir(d) => Ok(f(d)),
+ },
+ )
+ .and_then(|r| r)
+ }
+
+ /// Return a file assertion helper for the mocked filesystem.
+ pub fn assert_files(&self) -> AssertFiles {
+ let mut files = HashMap::new();
+ let root = self.root.lock().unwrap();
+
+ fn dir(files: &mut HashMap<PathBuf, MockFileContent>, path: &Path, item: &MockFSItem) {
+ match &item.content {
+ MockFSContent::File(Ok(c)) => {
+ files.insert(path.to_owned(), c.clone());
+ }
+ MockFSContent::Dir(d) => {
+ for (component, item) in d {
+ dir(files, &path.join(component), item);
+ }
+ }
+ _ => (),
+ }
+ }
+ dir(&mut files, Path::new(""), &*root);
+ AssertFiles { files }
+ }
+}
+
+/// A utility for asserting the state of the mocked filesystem.
+///
+/// All files must be accounted for; when dropped, a panic will occur if some files remain which
+/// weren't checked.
+#[derive(Debug)]
+pub struct AssertFiles {
+ files: HashMap<PathBuf, MockFileContent>,
+}
+
+// On windows we ignore drive prefixes. This is only relevant for real paths, which are only
+// present for edge case situations in tests (where AssertFiles is used).
+fn remove_prefix(p: &Path) -> &Path {
+ let mut iter = p.components();
+ if let Some(std::path::Component::Prefix(_)) = iter.next() {
+ iter.next(); // Prefix is followed by RootDir
+ iter.as_path()
+ } else {
+ p
+ }
+}
+
+impl AssertFiles {
+ /// Assert that the given path contains the given content (as a utf8 string).
+ pub fn check<P: AsRef<Path>, S: AsRef<str>>(&mut self, path: P, content: S) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ let Some(mfc) = self.files.remove(p) else {
+ panic!("missing file: {}", p.display());
+ };
+ let guard = mfc.0.lock().unwrap();
+ assert_eq!(
+ std::str::from_utf8(&*guard).unwrap(),
+ content.as_ref(),
+ "file content mismatch: {}",
+ p.display()
+ );
+ self
+ }
+
+ /// Assert that the given path contains the given byte content.
+ pub fn check_bytes<P: AsRef<Path>, B: AsRef<[u8]>>(
+ &mut self,
+ path: P,
+ content: B,
+ ) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ let Some(mfc) = self.files.remove(p) else {
+ panic!("missing file: {}", p.display());
+ };
+ let guard = mfc.0.lock().unwrap();
+ assert_eq!(
+ &*guard,
+ content.as_ref(),
+ "file content mismatch: {}",
+ p.display()
+ );
+ self
+ }
+
+ /// Ignore the given file (whether it exists or not).
+ pub fn ignore<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
+ self.files.remove(remove_prefix(path.as_ref()));
+ self
+ }
+
+ /// Assert that the given path exists without checking its content.
+ pub fn check_exists<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ if self.files.remove(p).is_none() {
+ panic!("missing file: {}", p.display());
+ }
+ self
+ }
+
+ /// Finish checking files.
+ ///
+ /// This panics if all files were not checked.
+ ///
+ /// This is also called when the value is dropped.
+ pub fn finish(&mut self) {
+ let files = std::mem::take(&mut self.files);
+ if !files.is_empty() {
+ panic!("additional files not expected: {:?}", files.keys());
+ }
+ }
+}
+
+impl Drop for AssertFiles {
+ fn drop(&mut self) {
+ if !std::thread::panicking() {
+ self.finish();
+ }
+ }
+}
+
+mock_key! {
+ pub struct MockFS => MockFiles
+}
+
+pub struct File {
+ content: MockFileContent,
+ pos: usize,
+}
+
+impl File {
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
+ MockFS.get(move |files| {
+ files
+ .path(path, false, |item| match &item.content {
+ MockFSContent::File(result) => result
+ .as_ref()
+ .map(|b| File {
+ content: b.clone(),
+ pos: 0,
+ })
+ .map_err(|e| e.kind().into()),
+ MockFSContent::Dir(_) => Err(ErrorKind::NotFound.into()),
+ })
+ .and_then(|r| r)
+ })
+ }
+
+ pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
+ let path = path.as_ref();
+ MockFS.get(|files| {
+ let name = path.file_name().expect("invalid path");
+ files.parent_dir(path, move |d| {
+ if !d.contains_key(name) {
+ d.insert(
+ name.to_owned(),
+ MockFSItem {
+ content: MockFSContent::File(Ok(Default::default())),
+ modified: super::time::SystemTime::now().0,
+ },
+ );
+ }
+ })
+ })?;
+ Self::open(path)
+ }
+}
+
+impl Read for File {
+ fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+ let guard = self.content.0.lock().unwrap();
+ if self.pos >= guard.len() {
+ return Ok(0);
+ }
+ let to_read = std::cmp::min(buf.len(), guard.len() - self.pos);
+ buf[..to_read].copy_from_slice(&guard[self.pos..self.pos + to_read]);
+ self.pos += to_read;
+ Ok(to_read)
+ }
+}
+
+impl Seek for File {
+ fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
+ let len = self.content.0.lock().unwrap().len();
+ match pos {
+ SeekFrom::Start(n) => self.pos = n as usize,
+ SeekFrom::End(n) => {
+ if n < 0 {
+ let offset = -n as usize;
+ if offset > len {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "out of bounds",
+ ));
+ }
+ self.pos = len - offset;
+ } else {
+ self.pos = len + n as usize
+ }
+ }
+ SeekFrom::Current(n) => {
+ if n < 0 {
+ let offset = -n as usize;
+ if offset > self.pos {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "out of bounds",
+ ));
+ }
+ self.pos -= offset;
+ } else {
+ self.pos += n as usize;
+ }
+ }
+ }
+ Ok(self.pos as u64)
+ }
+}
+
+impl Write for File {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ let mut guard = self.content.0.lock().unwrap();
+ let end = self.pos + buf.len();
+ if end > guard.len() {
+ guard.resize(end, 0);
+ }
+ (&mut guard[self.pos..end]).copy_from_slice(buf);
+ self.pos = end;
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ Ok(())
+ }
+}
+
+pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
+ MockFS.get(move |files| files.path(path, true, |_| ()))
+}
+
+pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
+ MockFS.get(move |files| {
+ let from_name = from.as_ref().file_name().expect("invalid path");
+ let item = files
+ .parent_dir(from.as_ref(), move |d| {
+ d.remove(from_name).ok_or(ErrorKind::NotFound.into())
+ })
+ .and_then(|r| r)?;
+
+ let to_name = to.as_ref().file_name().expect("invalid path");
+ files
+ .parent_dir(to.as_ref(), move |d| {
+ // Just error if `to` exists, which doesn't quite follow `std::fs::rename` behavior.
+ if d.contains_key(to_name) {
+ Err(ErrorKind::AlreadyExists.into())
+ } else {
+ d.insert(to_name.to_owned(), item);
+ Ok(())
+ }
+ })
+ .and_then(|r| r)
+ })
+}
+
+pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
+ MockFS.get(move |files| {
+ let name = path.as_ref().file_name().expect("invalid path");
+ files
+ .parent_dir(path.as_ref(), |d| {
+ if let Some(MockFSItem {
+ content: MockFSContent::Dir(_),
+ ..
+ }) = d.get(name)
+ {
+ Err(ErrorKind::NotFound.into())
+ } else {
+ d.remove(name).ok_or(ErrorKind::NotFound.into()).map(|_| ())
+ }
+ })
+ .and_then(|r| r)
+ })
+}
+
+pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
+ File::create(path.as_ref())?.write_all(contents.as_ref())
+}
+
+pub struct ReadDir {
+ base: PathBuf,
+ children: Vec<OsString>,
+}
+
+impl ReadDir {
+ pub fn new(path: &Path) -> Result<Self> {
+ MockFS.get(move |files| {
+ files
+ .path(path, false, |item| match &item.content {
+ MockFSContent::Dir(d) => Ok(ReadDir {
+ base: path.to_owned(),
+ children: d.keys().cloned().collect(),
+ }),
+ MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
+ })
+ .and_then(|r| r)
+ })
+ }
+}
+
+impl Iterator for ReadDir {
+ type Item = Result<DirEntry>;
+ fn next(&mut self) -> Option<Self::Item> {
+ let child = self.children.pop()?;
+ Some(Ok(DirEntry(self.base.join(child))))
+ }
+}
+
+pub struct DirEntry(PathBuf);
+
+impl DirEntry {
+ pub fn path(&self) -> super::path::PathBuf {
+ super::path::PathBuf(self.0.clone())
+ }
+
+ pub fn metadata(&self) -> Result<Metadata> {
+ MockFS.get(|files| {
+ files.path(&self.0, false, |item| {
+ let is_dir = matches!(&item.content, MockFSContent::Dir(_));
+ Metadata {
+ is_dir,
+ modified: item.modified,
+ }
+ })
+ })
+ }
+}
+
+pub struct Metadata {
+ is_dir: bool,
+ modified: SystemTime,
+}
+
+impl Metadata {
+ pub fn is_file(&self) -> bool {
+ !self.is_dir
+ }
+
+ pub fn is_dir(&self) -> bool {
+ self.is_dir
+ }
+
+ pub fn modified(&self) -> Result<super::time::SystemTime> {
+ Ok(super::time::SystemTime(self.modified))
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/mock.rs b/toolkit/crashreporter/client/app/src/std/mock.rs
new file mode 100644
index 0000000000..ed942a09bd
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mock.rs
@@ -0,0 +1,254 @@
+/* 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/. */
+
+//! Mocking utilities.
+//!
+//! Mock data is set on a per-thread basis. [`crate::std::thread`] handles this automatically for
+//! scoped threads, and warns about creating threads otherwise (which won't be able to
+//! automatically share mocked data, but it can be easily done with [`SharedMockData`] when
+//! appropriate).
+//!
+//! Mock data is stored using type erasure, with a [`MockKey`] indexing arbitrary values. Use
+//! [`mock_key!`] to define keys and the values to which they map. This approach was taken as a
+//! matter of covenience for programmers, and the resulting creation and consumption APIs are
+//! succinct yet extensible.
+//!
+//! Consumers should define keys (and expose them for mockers), and at runtime create a mock key
+//! instance and call [`MockKey::get`] or [`MockKey::try_get`] to retrieve mocked values to use.
+//!
+//! Mockers should call [`builder`] to create a builder, [`set`](Builder::set) key/value mappings,
+//! and call [`run`](Builder::run) to execute code with the mock data set.
+
+use std::any::{Any, TypeId};
+use std::collections::{hash_map::DefaultHasher, HashMap};
+use std::hash::{Hash, Hasher};
+use std::sync::atomic::{AtomicPtr, Ordering::Relaxed};
+
+type MockDataMap = HashMap<Box<dyn MockKeyStored>, Box<dyn Any + Send + Sync>>;
+
+thread_local! {
+ static MOCK_DATA: AtomicPtr<MockDataMap> = Default::default();
+}
+
+/// A trait intended to be used as a trait object interface for mock keys.
+pub trait MockKeyStored: Any + std::fmt::Debug + Sync {
+ fn eq(&self, other: &dyn MockKeyStored) -> bool;
+ fn hash(&self, state: &mut DefaultHasher);
+}
+
+impl PartialEq for dyn MockKeyStored {
+ fn eq(&self, other: &Self) -> bool {
+ MockKeyStored::eq(self, other)
+ }
+}
+
+impl Eq for dyn MockKeyStored {}
+
+impl Hash for dyn MockKeyStored {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.type_id().hash(state);
+ let mut hasher = DefaultHasher::new();
+ MockKeyStored::hash(self, &mut hasher);
+ state.write_u64(hasher.finish());
+ }
+}
+
+impl dyn MockKeyStored {
+ pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
+ if self.type_id() == TypeId::of::<T>() {
+ Some(unsafe { &*(self as *const _ as *const T) })
+ } else {
+ None
+ }
+ }
+}
+
+/// A type which can be used as a mock key.
+pub trait MockKey: MockKeyStored + Sized {
+ /// The value to which the key maps.
+ type Value: Any + Send + Sync;
+
+ /// Get the value set for this key, returning `None` if no data is set.
+ fn try_get<F, R>(&self, f: F) -> Option<R>
+ where
+ F: FnOnce(&Self::Value) -> R,
+ {
+ MOCK_DATA.with(move |ptr| {
+ let ptr = ptr.load(Relaxed);
+ if ptr.is_null() {
+ panic!("no mock data set");
+ }
+ unsafe { &*ptr }
+ .get(self as &dyn MockKeyStored)
+ .and_then(move |b| b.downcast_ref())
+ .map(f)
+ })
+ }
+
+ /// Get the value set for this key.
+ ///
+ /// Panics if no mock data is set for the key.
+ fn get<F, R>(&self, f: F) -> R
+ where
+ F: FnOnce(&Self::Value) -> R,
+ {
+ match self.try_get(f) {
+ Some(v) => v,
+ None => panic!("mock data for {self:?} not set"),
+ }
+ }
+}
+
+/// Mock data which can be shared amongst threads.
+pub struct SharedMockData(AtomicPtr<MockDataMap>);
+
+impl Clone for SharedMockData {
+ fn clone(&self) -> Self {
+ SharedMockData(AtomicPtr::new(self.0.load(Relaxed)))
+ }
+}
+
+impl SharedMockData {
+ /// Create a `SharedMockData` which stores the mock data from the current thread.
+ pub fn new() -> Self {
+ MOCK_DATA.with(|ptr| SharedMockData(AtomicPtr::new(ptr.load(Relaxed))))
+ }
+
+ /// Set the mock data on the current thread.
+ ///
+ /// # Safety
+ /// Callers must ensure that the mock data outlives the lifetime of the thread.
+ pub unsafe fn set(self) {
+ MOCK_DATA.with(|ptr| ptr.store(self.0.into_inner(), Relaxed));
+ }
+}
+
+/// Create a mock builder, which allows adding mock data and running functions under that mock
+/// environment.
+pub fn builder() -> Builder {
+ Builder::new()
+}
+
+/// A mock data builder.
+#[derive(Default)]
+pub struct Builder {
+ data: MockDataMap,
+}
+
+impl Builder {
+ /// Create a new, empty builder.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Set a mock data key/value mapping.
+ pub fn set<K: MockKey>(&mut self, key: K, value: K::Value) -> &mut Self {
+ self.data.insert(Box::new(key), Box::new(value));
+ self
+ }
+
+ /// Run the given function with mock data set.
+ pub fn run<F, R>(&mut self, f: F) -> R
+ where
+ F: FnOnce() -> R,
+ {
+ MOCK_DATA.with(|ptr| ptr.store(&mut self.data, Relaxed));
+ let ret = f();
+ MOCK_DATA.with(|ptr| ptr.store(std::ptr::null_mut(), Relaxed));
+ ret
+ }
+}
+
+/// A general-purpose [`MockKey`] keyed by an identifier string and the stored type.
+///
+/// Use [`hook`] or [`try_hook`] in code accessing the values.
+pub struct MockHook<T> {
+ name: &'static str,
+ _p: std::marker::PhantomData<fn() -> T>,
+}
+
+impl<T> std::fmt::Debug for MockHook<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct(&format!("MockHook<{}>", std::any::type_name::<T>()))
+ .field("name", &self.name)
+ .finish()
+ }
+}
+
+impl<T: 'static> MockKeyStored for MockHook<T> {
+ fn eq(&self, other: &dyn MockKeyStored) -> bool {
+ std::any::TypeId::of::<Self>() == other.type_id()
+ && self.name == other.downcast_ref::<Self>().unwrap().name
+ }
+ fn hash(&self, state: &mut DefaultHasher) {
+ self.name.hash(state)
+ }
+}
+
+impl<T: Any + Send + Sync + 'static> MockKey for MockHook<T> {
+ type Value = T;
+}
+
+impl<T> MockHook<T> {
+ /// Create a new mock hook key with the given name.
+ pub fn new(name: &'static str) -> Self {
+ MockHook {
+ name,
+ _p: Default::default(),
+ }
+ }
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be
+/// used instead. Panics if the hook isn't set.
+pub fn hook<T: Any + Send + Sync + Clone>(_normally: T, name: &'static str) -> T {
+ MockHook::new(name).get(|v: &T| v.clone())
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set,
+/// the given value will be used instead.
+pub fn try_hook<T: Any + Send + Sync + Clone>(fallback: T, name: &'static str) -> T {
+ MockHook::new(name)
+ .try_get(|v: &T| v.clone())
+ .unwrap_or(fallback)
+}
+
+/// Create a mock key with an associated value type.
+///
+/// Supports the following syntaxes:
+/// * Unit struct: `<visibility> struct NAME => VALUE_TYPE`
+/// * Tuple struct: `<visibility> struct NAME(ITEMS) => VALUE_TYPE`
+/// * Normal struct: `<visibility> struct NAME { FIELDS } => VALUE_TYPE`
+macro_rules! mock_key {
+ ( $vis:vis struct $name:ident => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name;] $name $value }
+ };
+ ( $vis:vis struct $name:ident ($($tuple:tt)*) => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name($($tuple)*);] $name $value }
+ };
+ ( $vis:vis struct $name:ident {$($full:tt)*} => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name{$($full)*}] $name $value }
+ };
+ ( @structdef [$($def:tt)+] $name:ident $value:ty ) => {
+ #[derive(Debug, PartialEq, Eq, Hash)]
+ $($def)+
+
+ impl crate::std::mock::MockKeyStored for $name {
+ fn eq(&self, other: &dyn crate::std::mock::MockKeyStored) -> bool {
+ std::any::TypeId::of::<Self>() == other.type_id()
+ && PartialEq::eq(self, other.downcast_ref::<Self>().unwrap())
+ }
+
+ fn hash(&self, state: &mut std::collections::hash_map::DefaultHasher) {
+ std::hash::Hash::hash(self, state)
+ }
+ }
+
+ impl crate::std::mock::MockKey for $name {
+ type Value = $value;
+ }
+ }
+}
+
+pub(crate) use mock_key;
diff --git a/toolkit/crashreporter/client/app/src/std/mock_stub.rs b/toolkit/crashreporter/client/app/src/std/mock_stub.rs
new file mode 100644
index 0000000000..a02ac3dea1
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mock_stub.rs
@@ -0,0 +1,20 @@
+/* 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/. */
+#![allow(dead_code)]
+
+//! Stubs used when mocking isn't enabled.
+
+/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be
+/// used instead. Panics if the hook isn't set.
+#[inline(always)]
+pub fn hook<T: std::any::Any + Send + Sync + Clone>(normally: T, _name: &'static str) -> T {
+ normally
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set,
+/// the given value will be used instead.
+#[inline(always)]
+pub fn try_hook<T: std::any::Any + Send + Sync + Clone>(fallback: T, _name: &'static str) -> T {
+ fallback
+}
diff --git a/toolkit/crashreporter/client/app/src/std/mod.rs b/toolkit/crashreporter/client/app/src/std/mod.rs
new file mode 100644
index 0000000000..467d6b0c14
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mod.rs
@@ -0,0 +1,33 @@
+/* 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/. */
+
+//! Standard library wrapper (for mocking in tests).
+//!
+//! In general this should always be used rather than `std` directly, and _especially_ when using
+//! `std` functions and types which interact with the runtime host environment.
+//!
+//! Note that, in some cases, this wrapper extends the `std` library. Notably, the [`mock`] module
+//! adds mocking functions.
+
+#![cfg_attr(mock, allow(unused))]
+
+pub use std::*;
+
+#[cfg_attr(not(mock), path = "mock_stub.rs")]
+pub mod mock;
+
+#[cfg(mock)]
+pub mod env;
+#[cfg(mock)]
+pub mod fs;
+#[cfg(mock)]
+pub mod net;
+#[cfg(mock)]
+pub mod path;
+#[cfg(mock)]
+pub mod process;
+#[cfg(mock)]
+pub mod thread;
+#[cfg(mock)]
+pub mod time;
diff --git a/toolkit/crashreporter/client/app/src/std/net.rs b/toolkit/crashreporter/client/app/src/std/net.rs
new file mode 100644
index 0000000000..dd51a44756
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/net.rs
@@ -0,0 +1,5 @@
+/* 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/. */
+
+//! Stub to avoid net use, if any.
diff --git a/toolkit/crashreporter/client/app/src/std/path.rs b/toolkit/crashreporter/client/app/src/std/path.rs
new file mode 100644
index 0000000000..c565615514
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/path.rs
@@ -0,0 +1,157 @@
+/* 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/. */
+
+//! We unfortunately have to mock `Path` because of `exists`, `try_exists`, and `metadata`.
+
+pub use std::path::*;
+
+use super::mock::MockKey;
+use std::ffi::OsStr;
+
+macro_rules! delegate {
+ ( fn $name:ident (&self $(, $arg:ident : $argty:ty )* ) -> $ret:ty ) => {
+ pub fn $name (&self, $($arg : $ty)*) -> $ret {
+ self.0.$name($($arg),*)
+ }
+ }
+}
+
+#[repr(transparent)]
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Path(std::path::Path);
+
+impl AsRef<std::path::Path> for Path {
+ fn as_ref(&self) -> &std::path::Path {
+ &self.0
+ }
+}
+
+impl AsRef<OsStr> for Path {
+ fn as_ref(&self) -> &OsStr {
+ self.0.as_ref()
+ }
+}
+
+impl AsRef<Path> for &str {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl AsRef<Path> for String {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl AsRef<Path> for &OsStr {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl Path {
+ fn from_path(path: &std::path::Path) -> &Self {
+ // # Safety
+ // Transparent wrapper is safe to transmute.
+ unsafe { std::mem::transmute(path) }
+ }
+
+ pub fn exists(&self) -> bool {
+ super::fs::MockFS
+ .try_get(|files| {
+ files
+ .path(self, false, |item| match &item.content {
+ super::fs::MockFSContent::File(r) => r.is_ok(),
+ _ => true,
+ })
+ .unwrap_or(false)
+ })
+ .unwrap_or(false)
+ }
+
+ pub fn read_dir(&self) -> super::io::Result<super::fs::ReadDir> {
+ super::fs::ReadDir::new(&self.0)
+ }
+
+ delegate!(fn display(&self) -> Display);
+ delegate!(fn file_stem(&self) -> Option<&OsStr>);
+ delegate!(fn file_name(&self) -> Option<&OsStr>);
+ delegate!(fn extension(&self) -> Option<&OsStr>);
+
+ pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
+ PathBuf(self.0.join(&path.as_ref().0))
+ }
+
+ pub fn parent(&self) -> Option<&Path> {
+ self.0.parent().map(Path::from_path)
+ }
+}
+
+#[repr(transparent)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct PathBuf(pub(super) std::path::PathBuf);
+
+impl PathBuf {
+ pub fn set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool {
+ self.0.set_extension(extension)
+ }
+
+ pub fn push<P: AsRef<Path>>(&mut self, path: P) {
+ self.0.push(path.as_ref())
+ }
+
+ pub fn pop(&mut self) -> bool {
+ self.0.pop()
+ }
+}
+
+impl std::ops::Deref for PathBuf {
+ type Target = Path;
+ fn deref(&self) -> &Self::Target {
+ Path::from_path(self.0.as_ref())
+ }
+}
+
+impl AsRef<Path> for PathBuf {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.0.as_ref())
+ }
+}
+
+impl AsRef<std::path::Path> for PathBuf {
+ fn as_ref(&self) -> &std::path::Path {
+ self.0.as_ref()
+ }
+}
+
+impl AsRef<OsStr> for PathBuf {
+ fn as_ref(&self) -> &OsStr {
+ self.0.as_ref()
+ }
+}
+
+impl From<std::ffi::OsString> for PathBuf {
+ fn from(os_str: std::ffi::OsString) -> Self {
+ PathBuf(os_str.into())
+ }
+}
+
+impl From<std::path::PathBuf> for PathBuf {
+ fn from(pathbuf: std::path::PathBuf) -> Self {
+ PathBuf(pathbuf)
+ }
+}
+
+impl From<PathBuf> for std::ffi::OsString {
+ fn from(pathbuf: PathBuf) -> Self {
+ pathbuf.0.into()
+ }
+}
+
+impl From<&str> for PathBuf {
+ fn from(s: &str) -> Self {
+ PathBuf(s.into())
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/process.rs b/toolkit/crashreporter/client/app/src/std/process.rs
new file mode 100644
index 0000000000..49dd028447
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/process.rs
@@ -0,0 +1,201 @@
+/* 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/. */
+
+pub use std::process::*;
+
+use crate::std::mock::{mock_key, MockKey};
+
+use std::ffi::{OsStr, OsString};
+use std::io::Result;
+use std::sync::{Arc, Mutex};
+
+mock_key! {
+ // Uses PathBuf rather than OsString to avoid path separator differences.
+ pub struct MockCommand(::std::path::PathBuf) => Box<dyn Fn(&Command) -> Result<Output> + Send + Sync>
+}
+
+#[derive(Debug)]
+pub struct Command {
+ pub program: OsString,
+ pub args: Vec<OsString>,
+ pub env: std::collections::HashMap<OsString, OsString>,
+ pub stdin: Vec<u8>,
+ // XXX The spawn stuff is hacky, but for now there's only one case where we really need to
+ // interact with `spawn` so we live with it for testing.
+ pub spawning: bool,
+ pub spawned_child: Mutex<Option<::std::process::Child>>,
+}
+
+impl Command {
+ pub fn mock<S: AsRef<OsStr>>(program: S) -> MockCommand {
+ MockCommand(program.as_ref().into())
+ }
+
+ pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
+ Command {
+ program: program.as_ref().into(),
+ args: vec![],
+ env: Default::default(),
+ stdin: Default::default(),
+ spawning: false,
+ spawned_child: Mutex::new(None),
+ }
+ }
+
+ pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
+ self.args.push(arg.as_ref().into());
+ self
+ }
+
+ pub fn args<I, S>(&mut self, args: I) -> &mut Self
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ for arg in args.into_iter() {
+ self.arg(arg);
+ }
+ self
+ }
+
+ pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
+ where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.env.insert(key.as_ref().into(), val.as_ref().into());
+ self
+ }
+
+ pub fn stdin<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn stdout<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn stderr<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn output(&mut self) -> std::io::Result<Output> {
+ MockCommand(self.program.as_os_str().into()).get(|f| f(self))
+ }
+
+ pub fn spawn(&mut self) -> std::io::Result<Child> {
+ self.spawning = true;
+ self.output()?;
+ self.spawning = false;
+ let stdin = Arc::new(Mutex::new(vec![]));
+ Ok(Child {
+ stdin: Some(ChildStdin {
+ data: stdin.clone(),
+ }),
+ cmd: self.clone_for_child(),
+ stdin_data: Some(stdin),
+ })
+ }
+
+ #[cfg(windows)]
+ pub fn creation_flags(&mut self, _flags: u32) -> &mut Self {
+ self
+ }
+
+ pub fn output_from_real_command(&self) -> std::io::Result<Output> {
+ let mut spawned_child = self.spawned_child.lock().unwrap();
+ if spawned_child.is_none() {
+ *spawned_child = Some(
+ ::std::process::Command::new(self.program.clone())
+ .args(self.args.clone())
+ .envs(self.env.clone())
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?,
+ );
+ }
+
+ if self.spawning {
+ return Ok(success_output());
+ }
+
+ let mut child = spawned_child.take().unwrap();
+ {
+ let mut input = child.stdin.take().unwrap();
+ std::io::copy(&mut std::io::Cursor::new(&self.stdin), &mut input)?;
+ }
+ child.wait_with_output()
+ }
+
+ fn clone_for_child(&self) -> Self {
+ Command {
+ program: self.program.clone(),
+ args: self.args.clone(),
+ env: self.env.clone(),
+ stdin: self.stdin.clone(),
+ spawning: false,
+ spawned_child: Mutex::new(self.spawned_child.lock().unwrap().take()),
+ }
+ }
+}
+
+pub struct Child {
+ pub stdin: Option<ChildStdin>,
+ cmd: Command,
+ stdin_data: Option<Arc<Mutex<Vec<u8>>>>,
+}
+
+impl Child {
+ pub fn wait_with_output(mut self) -> std::io::Result<Output> {
+ self.ref_wait_with_output().unwrap()
+ }
+
+ fn ref_wait_with_output(&mut self) -> Option<std::io::Result<Output>> {
+ drop(self.stdin.take());
+ if let Some(stdin) = self.stdin_data.take() {
+ self.cmd.stdin = Arc::try_unwrap(stdin)
+ .expect("stdin not dropped, wait_with_output may block")
+ .into_inner()
+ .unwrap();
+ Some(MockCommand(self.cmd.program.as_os_str().into()).get(|f| f(&self.cmd)))
+ } else {
+ None
+ }
+ }
+}
+
+pub struct ChildStdin {
+ data: Arc<Mutex<Vec<u8>>>,
+}
+
+impl std::io::Write for ChildStdin {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ self.data.lock().unwrap().write(buf)
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ Ok(())
+ }
+}
+
+#[cfg(unix)]
+pub fn success_exit_status() -> ExitStatus {
+ use std::os::unix::process::ExitStatusExt;
+ ExitStatus::from_raw(0)
+}
+
+#[cfg(windows)]
+pub fn success_exit_status() -> ExitStatus {
+ use std::os::windows::process::ExitStatusExt;
+ ExitStatus::from_raw(0)
+}
+
+pub fn success_output() -> Output {
+ Output {
+ status: success_exit_status(),
+ stdout: vec![],
+ stderr: vec![],
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/thread.rs b/toolkit/crashreporter/client/app/src/std/thread.rs
new file mode 100644
index 0000000000..d2dc74702a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/thread.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+pub use std::thread::*;
+
+// Mock `spawn` just to issue a warning that mocking within the thread won't work without manual
+// intervention.
+pub fn spawn<F, T>(f: F) -> JoinHandle<T>
+where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+{
+ eprintln!("warning: mocking won't work in `std::thread::spawn`ed threads by default. Use `std::mock::SharedMockData` if mocking is needed and it's safe to do so.");
+ std::thread::spawn(f)
+}
+
+pub struct Scope<'scope, 'env: 'scope> {
+ mock_data: super::mock::SharedMockData,
+ scope: &'scope std::thread::Scope<'scope, 'env>,
+}
+
+impl<'scope, 'env> Scope<'scope, 'env> {
+ pub fn spawn<F, T>(&self, f: F) -> ScopedJoinHandle<'scope, T>
+ where
+ F: FnOnce() -> T + Send + 'scope,
+ T: Send + 'scope,
+ {
+ let mock_data = self.mock_data.clone();
+ self.scope.spawn(move || {
+ // # Safety
+ // `thread::scope` guarantees that the mock data will outlive the thread.
+ unsafe { mock_data.set() };
+ f()
+ })
+ }
+}
+
+pub fn scope<'env, F, T>(f: F) -> T
+where
+ F: for<'scope> FnOnce(Scope<'scope, 'env>) -> T,
+{
+ let mock_data = super::mock::SharedMockData::new();
+ std::thread::scope(|scope| f(Scope { mock_data, scope }))
+}
diff --git a/toolkit/crashreporter/client/app/src/std/time.rs b/toolkit/crashreporter/client/app/src/std/time.rs
new file mode 100644
index 0000000000..5c351a7bcf
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/time.rs
@@ -0,0 +1,32 @@
+/* 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/. */
+
+use super::mock::{mock_key, MockKey};
+pub use std::time::{Duration, SystemTimeError};
+
+mock_key! {
+ pub struct MockCurrentTime => std::time::SystemTime
+}
+
+#[repr(transparent)]
+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
+pub struct SystemTime(pub(super) std::time::SystemTime);
+
+impl From<SystemTime> for ::time::OffsetDateTime {
+ fn from(t: SystemTime) -> Self {
+ t.0.into()
+ }
+}
+
+impl SystemTime {
+ pub const UNIX_EPOCH: SystemTime = SystemTime(std::time::SystemTime::UNIX_EPOCH);
+
+ pub fn now() -> Self {
+ MockCurrentTime.get(|t| SystemTime(*t))
+ }
+
+ pub fn duration_since(&self, earlier: Self) -> Result<Duration, SystemTimeError> {
+ self.0.duration_since(earlier.0)
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/test.rs b/toolkit/crashreporter/client/app/src/test.rs
new file mode 100644
index 0000000000..42d2334bca
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/test.rs
@@ -0,0 +1,1289 @@
+/* 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/. */
+
+//! Tests here mostly interact with the [test UI](crate::ui::test). As such, most tests read a bit
+//! more like integration tests than unit tests, testing the behavior of the application as a
+//! whole.
+
+use super::*;
+use crate::config::{test::MINIDUMP_PRUNE_SAVE_COUNT, Config};
+use crate::settings::Settings;
+use crate::std::{
+ ffi::OsString,
+ fs::{MockFS, MockFiles},
+ io::ErrorKind,
+ mock,
+ process::Command,
+ sync::{
+ atomic::{AtomicUsize, Ordering::Relaxed},
+ Arc,
+ },
+};
+use crate::ui::{self, test::model, ui_impl::Interact};
+
+/// A simple thread-safe counter which can be used in tests to mark that certain code paths were
+/// hit.
+#[derive(Clone, Default)]
+struct Counter(Arc<AtomicUsize>);
+
+impl Counter {
+ /// Create a new zero counter.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Increment the counter.
+ pub fn inc(&self) {
+ self.0.fetch_add(1, Relaxed);
+ }
+
+ /// Get the current count.
+ pub fn count(&self) -> usize {
+ self.0.load(Relaxed)
+ }
+
+ /// Assert that the current count is 1.
+ pub fn assert_one(&self) {
+ assert_eq!(self.count(), 1);
+ }
+}
+
+/// Fluent wraps arguments with the unicode BiDi characters.
+struct FluentArg<T>(T);
+
+impl<T: std::fmt::Display> std::fmt::Display for FluentArg<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ use crate::std::fmt::Write;
+ f.write_char('\u{2068}')?;
+ self.0.fmt(f)?;
+ f.write_char('\u{2069}')
+ }
+}
+
+/// Run a gui and interaction on separate threads.
+fn gui_interact<G, I, R>(gui: G, interact: I) -> R
+where
+ G: FnOnce() -> R,
+ I: FnOnce(Interact) + Send + 'static,
+{
+ let i = Interact::hook();
+ let handle = {
+ let i = i.clone();
+ ::std::thread::spawn(move || {
+ i.wait_for_ready();
+ interact(i);
+ })
+ };
+ let ret = gui();
+ // In case the gui failed before launching.
+ i.cancel();
+ handle.join().unwrap();
+ ret
+}
+
+const MOCK_MINIDUMP_EXTRA: &str = r#"{
+ "Vendor": "FooCorp",
+ "ProductName": "Bar",
+ "ReleaseChannel": "release",
+ "BuildID": "1234",
+ "StackTraces": {
+ "status": "OK"
+ },
+ "Version": "100.0",
+ "ServerURL": "https://reports.example.com",
+ "TelemetryServerURL": "https://telemetry.example.com",
+ "TelemetryClientId": "telemetry_client",
+ "TelemetrySessionId": "telemetry_session",
+ "SomeNestedJson": { "foo": "bar" },
+ "URL": "https://url.example.com"
+ }"#;
+
+// Actual content doesn't matter, aside from the hash that is generated.
+const MOCK_MINIDUMP_FILE: &[u8] = &[1, 2, 3, 4];
+const MOCK_MINIDUMP_SHA256: &str =
+ "9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a";
+macro_rules! current_date {
+ () => {
+ "2004-11-09"
+ };
+}
+const MOCK_CURRENT_DATE: &str = current_date!();
+const MOCK_CURRENT_TIME: &str = concat!(current_date!(), "T12:34:56Z");
+const MOCK_PING_UUID: uuid::Uuid = uuid::Uuid::nil();
+const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000";
+
+fn current_datetime() -> time::OffsetDateTime {
+ time::OffsetDateTime::parse(
+ MOCK_CURRENT_TIME,
+ &time::format_description::well_known::Rfc3339,
+ )
+ .unwrap()
+}
+
+fn current_unix_time() -> i64 {
+ current_datetime().unix_timestamp()
+}
+
+fn current_system_time() -> ::std::time::SystemTime {
+ current_datetime().into()
+}
+
+/// A basic configuration which populates some necessary/useful fields.
+fn test_config() -> Config {
+ let mut cfg = Config::default();
+ cfg.data_dir = Some("data_dir".into());
+ cfg.events_dir = Some("events_dir".into());
+ cfg.ping_dir = Some("ping_dir".into());
+ cfg.dump_file = Some("minidump.dmp".into());
+ cfg.strings = lang::LanguageInfo::default().load_strings().ok();
+ cfg
+}
+
+/// A test fixture to make configuration, mocking, and assertions easier.
+struct GuiTest {
+ /// The configuration used in the test. Initialized to [`test_config`].
+ pub config: Config,
+ /// The mock builder used in the test, initialized with a basic set of mocked values to ensure
+ /// most things will work out of the box.
+ pub mock: mock::Builder,
+ /// The mocked filesystem, which can be used for mock setup and assertions after completion.
+ pub files: MockFiles,
+}
+
+impl GuiTest {
+ /// Create a new GuiTest with enough configured for the application to run
+ pub fn new() -> Self {
+ // Create a default set of files which allow successful operation.
+ let mock_files = MockFiles::new();
+ mock_files
+ .add_file_result(
+ "minidump.dmp",
+ Ok(MOCK_MINIDUMP_FILE.into()),
+ current_system_time(),
+ )
+ .add_file_result(
+ "minidump.extra",
+ Ok(MOCK_MINIDUMP_EXTRA.into()),
+ current_system_time(),
+ );
+
+ // Create a default mock environment which allows successful operation.
+ let mut mock = mock::builder();
+ mock.set(
+ Command::mock("work_dir/minidump-analyzer"),
+ Box::new(|_| Ok(crate::std::process::success_output())),
+ )
+ .set(
+ Command::mock("work_dir/pingsender"),
+ Box::new(|_| Ok(crate::std::process::success_output())),
+ )
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| {
+ let mut output = crate::std::process::success_output();
+ output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
+ Ok(output)
+ }),
+ )
+ .set(MockFS, mock_files.clone())
+ .set(
+ crate::std::env::MockCurrentExe,
+ "work_dir/crashreporter".into(),
+ )
+ .set(crate::std::time::MockCurrentTime, current_system_time())
+ .set(mock::MockHook::new("ping_uuid"), MOCK_PING_UUID);
+
+ GuiTest {
+ config: test_config(),
+ mock,
+ files: mock_files,
+ }
+ }
+
+ /// Run the test as configured, using the given function to interact with the GUI.
+ ///
+ /// Returns the final result of the application logic.
+ pub fn try_run<F: FnOnce(Interact) + Send + 'static>(
+ &mut self,
+ interact: F,
+ ) -> anyhow::Result<bool> {
+ let GuiTest {
+ ref mut config,
+ ref mut mock,
+ ..
+ } = self;
+ let mut config = Arc::new(std::mem::take(config));
+
+ // Run the mock environment.
+ mock.run(move || gui_interact(move || try_run(&mut config), interact))
+ }
+
+ /// Run the test as configured, using the given function to interact with the GUI.
+ ///
+ /// Panics if the application logic returns an error (which would normally be displayed to the
+ /// user).
+ pub fn run<F: FnOnce(Interact) + Send + 'static>(&mut self, interact: F) {
+ if let Err(e) = self.try_run(interact) {
+ panic!(
+ "gui failure:{}",
+ e.chain().map(|e| format!("\n {e}")).collect::<String>()
+ );
+ }
+ }
+
+ /// Get the file assertion helper.
+ pub fn assert_files(&self) -> AssertFiles {
+ AssertFiles {
+ data_dir: "data_dir".into(),
+ events_dir: "events_dir".into(),
+ inner: self.files.assert_files(),
+ }
+ }
+}
+
+/// A wrapper around the mock [`AssertFiles`](crate::std::fs::AssertFiles).
+///
+/// This implements higher-level assertions common across tests, but also supports the lower-level
+/// assertions (though those return the [`AssertFiles`](crate::std::fs::AssertFiles) reference so
+/// higher-level assertions must be chained first).
+struct AssertFiles {
+ data_dir: String,
+ events_dir: String,
+ inner: std::fs::AssertFiles,
+}
+
+impl AssertFiles {
+ fn data(&self, rest: &str) -> String {
+ format!("{}/{rest}", &self.data_dir)
+ }
+
+ fn events(&self, rest: &str) -> String {
+ format!("{}/{rest}", &self.events_dir)
+ }
+
+ /// Set the data dir if not the default.
+ pub fn set_data_dir<S: ToString>(&mut self, data_dir: S) -> &mut Self {
+ let data_dir = data_dir.to_string();
+ // Data dir should be relative to root.
+ self.data_dir = data_dir.trim_start_matches('/').to_string();
+ self
+ }
+
+ /// Ignore the generated log file.
+ pub fn ignore_log(&mut self) -> &mut Self {
+ self.inner.ignore(self.data("submit.log"));
+ self
+ }
+
+ /// Assert that the crash report was submitted according to the filesystem.
+ pub fn submitted(&mut self) -> &mut Self {
+ self.inner.check(
+ self.data(&format!("submitted/{MOCK_REMOTE_CRASH_ID}.txt")),
+ format!("Crash ID: {}\n", FluentArg(MOCK_REMOTE_CRASH_ID)),
+ );
+ self
+ }
+
+ /// Assert that the given settings where saved.
+ pub fn saved_settings(&mut self, settings: Settings) -> &mut Self {
+ self.inner.check(
+ self.data("crashreporter_settings.json"),
+ settings.to_string(),
+ );
+ self
+ }
+
+ /// Assert that a crash is pending according to the filesystem.
+ pub fn pending(&mut self) -> &mut Self {
+ let dmp = self.data("pending/minidump.dmp");
+ self.inner
+ .check(self.data("pending/minidump.extra"), MOCK_MINIDUMP_EXTRA)
+ .check_bytes(dmp, MOCK_MINIDUMP_FILE);
+ self
+ }
+
+ /// Assert that a crash ping was sent according to the filesystem.
+ pub fn ping(&mut self) -> &mut Self {
+ self.inner.check(
+ format!("ping_dir/{MOCK_PING_UUID}.json"),
+ serde_json::json! {{
+ "type": "crash",
+ "id": MOCK_PING_UUID,
+ "version": 4,
+ "creation_date": MOCK_CURRENT_TIME,
+ "client_id": "telemetry_client",
+ "payload": {
+ "sessionId": "telemetry_session",
+ "version": 1,
+ "crashDate": MOCK_CURRENT_DATE,
+ "crashTime": MOCK_CURRENT_TIME,
+ "hasCrashEnvironment": true,
+ "crashId": "minidump",
+ "minidumpSha256Hash": MOCK_MINIDUMP_SHA256,
+ "processType": "main",
+ "stackTraces": {
+ "status": "OK"
+ },
+ "metadata": {
+ "BuildID": "1234",
+ "ProductName": "Bar",
+ "ReleaseChannel": "release",
+ }
+ },
+ "application": {
+ "vendor": "FooCorp",
+ "name": "Bar",
+ "buildId": "1234",
+ "displayVersion": "",
+ "platformVersion": "",
+ "version": "100.0",
+ "channel": "release"
+ }
+ }}
+ .to_string(),
+ );
+ self
+ }
+
+ /// Assert that a crash submission event was written with the given submission status.
+ pub fn submission_event(&mut self, success: bool) -> &mut Self {
+ self.inner.check(
+ self.events("minidump-submission"),
+ format!(
+ "crash.submission.1\n\
+ {}\n\
+ minidump\n\
+ {success}\n\
+ {}",
+ current_unix_time(),
+ if success { MOCK_REMOTE_CRASH_ID } else { "" }
+ ),
+ );
+ self
+ }
+}
+
+impl std::ops::Deref for AssertFiles {
+ type Target = std::fs::AssertFiles;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl std::ops::DerefMut for AssertFiles {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+
+#[test]
+fn error_dialog() {
+ gui_interact(
+ || {
+ let cfg = Config::default();
+ ui::error_dialog(&cfg, "an error occurred")
+ },
+ |interact| {
+ interact.element("close", |_style, b: &model::Button| b.click.fire(&()));
+ },
+ );
+}
+
+#[test]
+fn no_dump_file() {
+ let mut cfg = Arc::new(Config::default());
+ {
+ let cfg = Arc::get_mut(&mut cfg).unwrap();
+ cfg.strings = lang::LanguageInfo::default().load_strings().ok();
+ }
+ assert!(try_run(&mut cfg).is_err());
+ Arc::get_mut(&mut cfg).unwrap().auto_submit = true;
+ assert!(try_run(&mut cfg).is_ok());
+}
+
+#[test]
+fn minidump_analyzer_error() {
+ mock::builder()
+ .set(
+ Command::mock("work_dir/minidump-analyzer"),
+ Box::new(|_| Err(ErrorKind::NotFound.into())),
+ )
+ .set(
+ crate::std::env::MockCurrentExe,
+ "work_dir/crashreporter".into(),
+ )
+ .run(|| {
+ let cfg = test_config();
+ assert!(try_run(&mut Arc::new(cfg)).is_err());
+ });
+}
+
+#[test]
+fn no_extra_file() {
+ mock::builder()
+ .set(
+ Command::mock("work_dir/minidump-analyzer"),
+ Box::new(|_| Ok(crate::std::process::success_output())),
+ )
+ .set(
+ crate::std::env::MockCurrentExe,
+ "work_dir/crashreporter".into(),
+ )
+ .set(MockFS, {
+ let files = MockFiles::new();
+ files.add_file_result(
+ "minidump.extra",
+ Err(ErrorKind::NotFound.into()),
+ ::std::time::SystemTime::UNIX_EPOCH,
+ );
+ files
+ })
+ .run(|| {
+ let cfg = test_config();
+ assert!(try_run(&mut Arc::new(cfg)).is_err());
+ });
+}
+
+#[test]
+fn auto_submit() {
+ let mut test = GuiTest::new();
+ test.config.auto_submit = true;
+ // auto_submit should not do any GUI things, including creating the crashreporter_settings.json
+ // file.
+ test.mock.run(|| {
+ assert!(try_run(&mut Arc::new(std::mem::take(&mut test.config))).is_ok());
+ });
+ test.assert_files().ignore_log().submitted().pending();
+}
+
+#[test]
+fn restart() {
+ let mut test = GuiTest::new();
+ test.config.restart_command = Some("my_process".into());
+ test.config.restart_args = vec!["a".into(), "b".into()];
+ let ran_process = Counter::new();
+ let mock_ran_process = ran_process.clone();
+ test.mock.set(
+ Command::mock("my_process"),
+ Box::new(move |cmd| {
+ assert_eq!(cmd.args, &["a", "b"]);
+ mock_ran_process.inc();
+ Ok(crate::std::process::success_output())
+ }),
+ );
+ test.run(|interact| {
+ interact.element("restart", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending();
+ ran_process.assert_one();
+}
+
+#[test]
+fn no_restart_with_windows_error_reporting() {
+ let mut test = GuiTest::new();
+ test.config.restart_command = Some("my_process".into());
+ test.config.restart_args = vec!["a".into(), "b".into()];
+ // Add the "WindowsErrorReporting" key to the extra file
+ const MINIDUMP_EXTRA_CONTENTS: &str = r#"{
+ "Vendor": "FooCorp",
+ "ProductName": "Bar",
+ "ReleaseChannel": "release",
+ "BuildID": "1234",
+ "StackTraces": {
+ "status": "OK"
+ },
+ "Version": "100.0",
+ "ServerURL": "https://reports.example.com",
+ "TelemetryServerURL": "https://telemetry.example.com",
+ "TelemetryClientId": "telemetry_client",
+ "TelemetrySessionId": "telemetry_session",
+ "SomeNestedJson": { "foo": "bar" },
+ "URL": "https://url.example.com",
+ "WindowsErrorReporting": 1
+ }"#;
+ test.files = {
+ let mock_files = MockFiles::new();
+ mock_files
+ .add_file_result(
+ "minidump.dmp",
+ Ok(MOCK_MINIDUMP_FILE.into()),
+ current_system_time(),
+ )
+ .add_file_result(
+ "minidump.extra",
+ Ok(MINIDUMP_EXTRA_CONTENTS.into()),
+ current_system_time(),
+ );
+ test.mock.set(MockFS, mock_files.clone());
+ mock_files
+ };
+ let ran_process = Counter::new();
+ let mock_ran_process = ran_process.clone();
+ test.mock.set(
+ Command::mock("my_process"),
+ Box::new(move |cmd| {
+ assert_eq!(cmd.args, &["a", "b"]);
+ mock_ran_process.inc();
+ Ok(crate::std::process::success_output())
+ }),
+ );
+ test.run(|interact| {
+ interact.element("restart", |style, b: &model::Button| {
+ // Check that the button is hidden, and invoke the click anyway to ensure the process
+ // isn't restarted (the window will still be closed).
+ assert_eq!(style.visible.get(), false);
+ b.click.fire(&())
+ });
+ });
+ let mut assert_files = test.assert_files();
+ assert_files
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted();
+ {
+ let dmp = assert_files.data("pending/minidump.dmp");
+ let extra = assert_files.data("pending/minidump.extra");
+ assert_files
+ .check(extra, MINIDUMP_EXTRA_CONTENTS)
+ .check_bytes(dmp, MOCK_MINIDUMP_FILE);
+ }
+
+ assert_eq!(ran_process.count(), 0);
+}
+
+#[test]
+fn quit() {
+ let mut test = GuiTest::new();
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending();
+}
+
+#[test]
+fn delete_dump() {
+ let mut test = GuiTest::new();
+ test.config.delete_dump = true;
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted();
+}
+
+#[test]
+fn no_submit() {
+ let mut test = GuiTest::new();
+ test.files.add_dir("data_dir").add_file(
+ "data_dir/crashreporter_settings.json",
+ Settings {
+ submit_report: true,
+ include_url: true,
+ }
+ .to_string(),
+ );
+ test.run(|interact| {
+ interact.element("send", |_style, c: &model::Checkbox| {
+ assert!(c.checked.get())
+ });
+ interact.element("include-url", |_style, c: &model::Checkbox| {
+ assert!(c.checked.get())
+ });
+ interact.element("send", |_style, c: &model::Checkbox| c.checked.set(false));
+ interact.element("include-url", |_style, c: &model::Checkbox| {
+ c.checked.set(false)
+ });
+
+ // When submission is unchecked, the following elements should be disabled.
+ interact.element("details", |style, _: &model::Button| {
+ assert!(!style.enabled.get());
+ });
+ interact.element("comment", |style, _: &model::TextBox| {
+ assert!(!style.enabled.get());
+ });
+ interact.element("include-url", |style, _: &model::Checkbox| {
+ assert!(!style.enabled.get());
+ });
+
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings {
+ submit_report: false,
+ include_url: false,
+ })
+ .pending();
+}
+
+#[test]
+fn ping_and_event_files() {
+ let mut test = GuiTest::new();
+ test.files
+ .add_dir("ping_dir")
+ .add_dir("events_dir")
+ .add_file(
+ "events_dir/minidump",
+ "1\n\
+ 12:34:56\n\
+ e0423878-8d59-4452-b82e-cad9c846836e\n\
+ {\"foo\":\"bar\"}",
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending()
+ .submission_event(true)
+ .ping()
+ .check(
+ "events_dir/minidump",
+ format!(
+ "1\n\
+ 12:34:56\n\
+ e0423878-8d59-4452-b82e-cad9c846836e\n\
+ {}",
+ serde_json::json! {{
+ "foo": "bar",
+ "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
+ "CrashPingUUID": MOCK_PING_UUID,
+ "StackTraces": { "status": "OK" }
+ }}
+ ),
+ );
+}
+
+#[test]
+fn pingsender_failure() {
+ let mut test = GuiTest::new();
+ test.mock.set(
+ Command::mock("work_dir/pingsender"),
+ Box::new(|_| Err(ErrorKind::NotFound.into())),
+ );
+ test.files
+ .add_dir("ping_dir")
+ .add_dir("events_dir")
+ .add_file(
+ "events_dir/minidump",
+ "1\n\
+ 12:34:56\n\
+ e0423878-8d59-4452-b82e-cad9c846836e\n\
+ {\"foo\":\"bar\"}",
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending()
+ .submission_event(true)
+ .ping()
+ .check(
+ "events_dir/minidump",
+ format!(
+ "1\n\
+ 12:34:56\n\
+ e0423878-8d59-4452-b82e-cad9c846836e\n\
+ {}",
+ serde_json::json! {{
+ "foo": "bar",
+ "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
+ // No crash ping UUID since pingsender fails
+ "StackTraces": { "status": "OK" }
+ }}
+ ),
+ );
+}
+
+#[test]
+fn eol_version() {
+ let mut test = GuiTest::new();
+ test.files
+ .add_dir("data_dir")
+ .add_file("data_dir/EndOfLife100.0", "");
+ // Should fail before opening the gui
+ let result = test.try_run(|_| ());
+ assert_eq!(
+ result.expect_err("should fail on EOL version").to_string(),
+ "Version end of life: crash reports are no longer accepted."
+ );
+ test.assert_files()
+ .ignore_log()
+ .pending()
+ .ignore("data_dir/EndOfLife100.0");
+}
+
+#[test]
+fn details_window() {
+ let mut test = GuiTest::new();
+ test.run(|interact| {
+ let details_visible = || {
+ interact.window("crash-details-window", |style, _w: &model::Window| {
+ style.visible.get()
+ })
+ };
+ assert_eq!(details_visible(), false);
+ interact.element("details", |_style, b: &model::Button| b.click.fire(&()));
+ assert_eq!(details_visible(), true);
+ let details_text = loop {
+ let v = interact.element("details-text", |_style, t: &model::TextBox| t.content.get());
+ if v == "Loading…" {
+ // Wait for the details to be populated.
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ continue;
+ } else {
+ break v;
+ }
+ };
+ interact.element("close-details", |_style, b: &model::Button| b.click.fire(&()));
+ assert_eq!(details_visible(), false);
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ assert_eq!(details_text,
+ "BuildID: 1234\n\
+ ProductName: Bar\n\
+ ReleaseChannel: release\n\
+ SomeNestedJson: {\"foo\":\"bar\"}\n\
+ SubmittedFrom: Client\n\
+ TelemetryClientId: telemetry_client\n\
+ TelemetryServerURL: https://telemetry.example.com\n\
+ TelemetrySessionId: telemetry_session\n\
+ Throttleable: 1\n\
+ Vendor: FooCorp\n\
+ Version: 100.0\n\
+ This report also contains technical information about the state of the application when it crashed.\n"
+ );
+ });
+}
+
+#[test]
+fn data_dir_default() {
+ let mut test = GuiTest::new();
+ test.config.data_dir = None;
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ test.assert_files()
+ .set_data_dir("data_dir/FooCorp/Bar/Crash Reports")
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending();
+}
+
+#[test]
+fn include_url() {
+ for setting in [false, true] {
+ let mut test = GuiTest::new();
+ test.files.add_dir("data_dir").add_file(
+ "data_dir/crashreporter_settings.json",
+ Settings {
+ submit_report: true,
+ include_url: setting,
+ }
+ .to_string(),
+ );
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(
+ net::report::MockLibCurl,
+ Box::new(move |report| {
+ assert_eq!(
+ report.extra.get("URL").and_then(|v| v.as_str()),
+ setting.then_some("https://url.example.com")
+ );
+ Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ }
+}
+
+#[test]
+fn comment() {
+ const COMMENT: &str = "My program crashed";
+
+ for set_comment in [false, true] {
+ let invoked = Counter::new();
+ let mock_invoked = invoked.clone();
+ let mut test = GuiTest::new();
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(
+ net::report::MockLibCurl,
+ Box::new(move |report| {
+ mock_invoked.inc();
+ assert_eq!(
+ report.extra.get("Comments").and_then(|v| v.as_str()),
+ set_comment.then_some(COMMENT)
+ );
+ Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
+ }),
+ );
+ test.run(move |interact| {
+ if set_comment {
+ interact.element("comment", |_style, c: &model::TextBox| {
+ c.content.set(COMMENT.into())
+ });
+ }
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ invoked.assert_one();
+ }
+}
+
+#[test]
+fn curl_binary() {
+ let mut test = GuiTest::new();
+ test.files.add_file("minidump.memory.json.gz", "");
+ let ran_process = Counter::new();
+ let mock_ran_process = ran_process.clone();
+ test.mock.set(
+ Command::mock("curl"),
+ Box::new(move |cmd| {
+ if cmd.spawning {
+ return Ok(crate::std::process::success_output());
+ }
+
+ // Curl strings need backslashes escaped.
+ let curl_escaped_separator = if std::path::MAIN_SEPARATOR == '\\' {
+ "\\\\"
+ } else {
+ std::path::MAIN_SEPARATOR_STR
+ };
+
+ let expected_args: Vec<OsString> = [
+ "--user-agent",
+ net::report::USER_AGENT,
+ "--form",
+ "extra=@-;filename=extra.json;type=application/json",
+ "--form",
+ &format!(
+ "upload_file_minidump=@\"data_dir{0}pending{0}minidump.dmp\"",
+ curl_escaped_separator
+ ),
+ "--form",
+ &format!(
+ "memory_report=@\"data_dir{0}pending{0}minidump.memory.json.gz\"",
+ curl_escaped_separator
+ ),
+ "https://reports.example.com",
+ ]
+ .into_iter()
+ .map(Into::into)
+ .collect();
+ assert_eq!(cmd.args, expected_args);
+ let mut output = crate::std::process::success_output();
+ output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
+ mock_ran_process.inc();
+ Ok(output)
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ ran_process.assert_one();
+}
+
+#[test]
+fn curl_library() {
+ let invoked = Counter::new();
+ let mock_invoked = invoked.clone();
+ let mut test = GuiTest::new();
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(
+ net::report::MockLibCurl,
+ Box::new(move |_| {
+ mock_invoked.inc();
+ Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+ invoked.assert_one();
+}
+
+#[test]
+fn report_not_sent() {
+ let mut test = GuiTest::new();
+ test.files.add_dir("events_dir");
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(
+ net::report::MockLibCurl,
+ Box::new(move |_| Err(std::io::ErrorKind::NotFound.into())),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submission_event(false)
+ .pending();
+}
+
+#[test]
+fn report_response_failed() {
+ let mut test = GuiTest::new();
+ test.files.add_dir("events_dir");
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(
+ net::report::MockLibCurl,
+ Box::new(move |_| Ok(Err(std::io::ErrorKind::NotFound.into()))),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submission_event(false)
+ .pending();
+}
+
+#[test]
+fn response_indicates_discarded() {
+ let mut test = GuiTest::new();
+ // A response indicating discarded triggers a prune of the directory containing the minidump.
+ // Since there is one more minidump (the main one, minidump.dmp), pruning should keep all but
+ // the first 3, which will be the oldest.
+ const SHOULD_BE_PRUNED: usize = 3;
+
+ for i in 0..MINIDUMP_PRUNE_SAVE_COUNT + SHOULD_BE_PRUNED - 1 {
+ test.files.add_dir("data_dir/pending").add_file_result(
+ format!("data_dir/pending/minidump{i}.dmp"),
+ Ok("contents".into()),
+ ::std::time::SystemTime::UNIX_EPOCH + ::std::time::Duration::from_secs(1234 + i as u64),
+ );
+ if i % 2 == 0 {
+ test.files
+ .add_file(format!("data_dir/pending/minidump{i}.extra"), "{}");
+ }
+ if i % 5 == 0 {
+ test.files
+ .add_file(format!("data_dir/pending/minidump{i}.memory.json.gz"), "{}");
+ }
+ }
+ test.mock.set(
+ Command::mock("curl"),
+ Box::new(|_| {
+ let mut output = crate::std::process::success_output();
+ output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}\nDiscarded=1").into();
+ Ok(output)
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ let mut assert_files = test.assert_files();
+ assert_files
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .pending();
+ for i in SHOULD_BE_PRUNED..MINIDUMP_PRUNE_SAVE_COUNT + SHOULD_BE_PRUNED - 1 {
+ assert_files.check_exists(format!("data_dir/pending/minidump{i}.dmp"));
+ if i % 2 == 0 {
+ assert_files.check_exists(format!("data_dir/pending/minidump{i}.extra"));
+ }
+ if i % 5 == 0 {
+ assert_files.check_exists(format!("data_dir/pending/minidump{i}.memory.json.gz"));
+ }
+ }
+}
+
+#[test]
+fn response_view_url() {
+ let mut test = GuiTest::new();
+ test.mock.set(
+ Command::mock("curl"),
+ Box::new(|_| {
+ let mut output = crate::std::process::success_output();
+ output.stdout =
+ format!("CrashID={MOCK_REMOTE_CRASH_ID}\nViewURL=https://foo.bar.example").into();
+ Ok(output)
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .pending()
+ .check(
+ format!("data_dir/submitted/{MOCK_REMOTE_CRASH_ID}.txt"),
+ format!(
+ "\
+ Crash ID: {}\n\
+ You can view details of this crash at {}.\n",
+ FluentArg(MOCK_REMOTE_CRASH_ID),
+ FluentArg("https://foo.bar.example")
+ ),
+ );
+}
+
+#[test]
+fn response_stop_sending_reports() {
+ let mut test = GuiTest::new();
+ test.mock.set(
+ Command::mock("curl"),
+ Box::new(|_| {
+ let mut output = crate::std::process::success_output();
+ output.stdout =
+ format!("CrashID={MOCK_REMOTE_CRASH_ID}\nStopSendingReportsFor=100.0").into();
+ Ok(output)
+ }),
+ );
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted()
+ .pending()
+ .check_exists("data_dir/EndOfLife100.0");
+}
+
+/// A real temporary directory in the host filesystem.
+///
+/// The directory is guaranteed to be unique to the test suite process (in case of crash, it can be
+/// inspected).
+///
+/// When dropped, the directory is deleted.
+struct TempDir {
+ path: ::std::path::PathBuf,
+}
+
+impl TempDir {
+ /// Create a new directory with the given identifying name.
+ ///
+ /// The name should be unique to deconflict amongst concurrent tests.
+ pub fn new(name: &str) -> Self {
+ let path = ::std::env::temp_dir().join(format!(
+ "{}-test-{}-{name}",
+ env!("CARGO_PKG_NAME"),
+ std::process::id()
+ ));
+ ::std::fs::create_dir_all(&path).unwrap();
+ TempDir { path }
+ }
+
+ /// Get the temporary directory path.
+ pub fn path(&self) -> &::std::path::Path {
+ &self.path
+ }
+}
+
+impl Drop for TempDir {
+ fn drop(&mut self) {
+ // Best-effort removal, ignore errors.
+ let _ = ::std::fs::remove_dir_all(&self.path);
+ }
+}
+
+/// A mock crash report server.
+///
+/// When dropped, the server is shutdown.
+struct TestCrashReportServer {
+ addr: ::std::net::SocketAddr,
+ shutdown_and_thread: Option<(
+ tokio::sync::oneshot::Sender<()>,
+ std::thread::JoinHandle<()>,
+ )>,
+}
+
+impl TestCrashReportServer {
+ /// Create and start a mock crash report server on an ephemeral port, returning a handle to the
+ /// server.
+ pub fn run() -> Self {
+ let (shutdown, rx) = tokio::sync::oneshot::channel();
+
+ use warp::Filter;
+
+ let submit = warp::path("submit")
+ .and(warp::filters::method::post())
+ .and(warp::filters::header::header("content-type"))
+ .and(warp::filters::body::bytes())
+ .and_then(|content_type: String, body: bytes::Bytes| async move {
+ let Some(boundary) = content_type.strip_prefix("multipart/form-data; boundary=")
+ else {
+ return Err(warp::reject());
+ };
+
+ let body = String::from_utf8_lossy(&*body).to_owned();
+
+ for part in body.split(&format!("--{boundary}")).skip(1) {
+ if part == "--\r\n" {
+ break;
+ }
+
+ let (_headers, _data) = part.split_once("\r\n\r\n").unwrap_or(("", part));
+ // TODO validate parts
+ }
+ Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}"))
+ });
+
+ let (addr_channel_tx, addr_channel_rx) = std::sync::mpsc::sync_channel(0);
+
+ let thread = ::std::thread::spawn(move || {
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("failed to create tokio runtime");
+ let _guard = rt.enter();
+
+ let (addr, server) =
+ warp::serve(submit).bind_with_graceful_shutdown(([127, 0, 0, 1], 0), async move {
+ rx.await.ok();
+ });
+
+ addr_channel_tx.send(addr).unwrap();
+
+ rt.block_on(server)
+ });
+
+ let addr = addr_channel_rx.recv().unwrap();
+
+ TestCrashReportServer {
+ addr,
+ shutdown_and_thread: Some((shutdown, thread)),
+ }
+ }
+
+ /// Get the url to which to submit crash reports for this mocked server.
+ pub fn submit_url(&self) -> String {
+ format!("http://{}/submit", self.addr)
+ }
+}
+
+impl Drop for TestCrashReportServer {
+ fn drop(&mut self) {
+ let (shutdown, thread) = self.shutdown_and_thread.take().unwrap();
+ let _ = shutdown.send(());
+ thread.join().unwrap();
+ }
+}
+
+#[test]
+fn real_curl_binary() {
+ if ::std::process::Command::new("curl").output().is_err() {
+ eprintln!("no curl binary; skipping real_curl_binary test");
+ return;
+ }
+
+ let server = TestCrashReportServer::run();
+
+ let mut test = GuiTest::new();
+ test.mock.set(
+ Command::mock("curl"),
+ Box::new(|cmd| cmd.output_from_real_command()),
+ );
+ test.config.report_url = Some(server.submit_url().into());
+ test.config.delete_dump = true;
+
+ // We need the dump file to actually exist since the curl binary is passed the file path.
+ // The dump file needs to exist at the pending dir location.
+
+ let tempdir = TempDir::new("real_curl_binary");
+ let data_dir = tempdir.path().to_owned();
+ let pending_dir = data_dir.join("pending");
+ test.config.data_dir = Some(data_dir.clone().into());
+ ::std::fs::create_dir_all(&pending_dir).unwrap();
+ let dump_file = pending_dir.join("minidump.dmp");
+ ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap();
+
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .set_data_dir(data_dir.display())
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted();
+}
+
+#[test]
+fn real_curl_library() {
+ if !crate::net::can_load_libcurl() {
+ eprintln!("no libcurl; skipping real_libcurl test");
+ return;
+ }
+
+ let server = TestCrashReportServer::run();
+
+ let mut test = GuiTest::new();
+ test.mock
+ .set(
+ Command::mock("curl"),
+ Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
+ )
+ .set(mock::MockHook::new("use_system_libcurl"), true);
+ test.config.report_url = Some(server.submit_url().into());
+ test.config.delete_dump = true;
+
+ // We need the dump file to actually exist since libcurl is passed the file path.
+ // The dump file needs to exist at the pending dir location.
+
+ let tempdir = TempDir::new("real_libcurl");
+ let data_dir = tempdir.path().to_owned();
+ let pending_dir = data_dir.join("pending");
+ test.config.data_dir = Some(data_dir.clone().into());
+ ::std::fs::create_dir_all(&pending_dir).unwrap();
+ let dump_file = pending_dir.join("minidump.dmp");
+ ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap();
+
+ test.run(|interact| {
+ interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
+ });
+
+ test.assert_files()
+ .set_data_dir(data_dir.display())
+ .ignore_log()
+ .saved_settings(Settings::default())
+ .submitted();
+}
diff --git a/toolkit/crashreporter/client/app/src/thread_bound.rs b/toolkit/crashreporter/client/app/src/thread_bound.rs
new file mode 100644
index 0000000000..28095f42f4
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/thread_bound.rs
@@ -0,0 +1,41 @@
+/* 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/. */
+
+//! Encapsulate thread-bound values in a safe manner.
+//!
+//! This allows non-`Send`/`Sync` values to be transferred across thread boundaries, checking at
+//! runtime at access sites whether it is safe to use them.
+
+pub struct ThreadBound<T> {
+ data: T,
+ origin: std::thread::ThreadId,
+}
+
+impl<T: Default> Default for ThreadBound<T> {
+ fn default() -> Self {
+ ThreadBound::new(Default::default())
+ }
+}
+
+impl<T> ThreadBound<T> {
+ pub fn new(data: T) -> Self {
+ ThreadBound {
+ data,
+ origin: std::thread::current().id(),
+ }
+ }
+
+ pub fn borrow(&self) -> &T {
+ assert!(
+ std::thread::current().id() == self.origin,
+ "unsafe access to thread-bound value"
+ );
+ &self.data
+ }
+}
+
+// # Safety
+// Access to the inner value is only permitted on the originating thread.
+unsafe impl<T> Send for ThreadBound<T> {}
+unsafe impl<T> Sync for ThreadBound<T> {}
diff --git a/toolkit/crashreporter/client/app/src/ui/crashreporter.png b/toolkit/crashreporter/client/app/src/ui/crashreporter.png
new file mode 100644
index 0000000000..5e68bac17c
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/crashreporter.png
Binary files differ
diff --git a/toolkit/crashreporter/client/app/src/ui/gtk.rs b/toolkit/crashreporter/client/app/src/ui/gtk.rs
new file mode 100644
index 0000000000..a76f99b0bd
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/gtk.rs
@@ -0,0 +1,841 @@
+/* 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/. */
+
+use super::model::{self, Alignment, Application, Element};
+use crate::std::{
+ cell::RefCell,
+ ffi::{c_char, CString},
+ rc::Rc,
+ sync::atomic::{AtomicBool, Ordering::Relaxed},
+};
+use crate::{
+ data::{Event, Property, Synchronized},
+ std,
+};
+
+/// Create a `std::ffi::CStr` directly from a literal string.
+///
+/// The argument is an `expr` rather than `literal` so that other macros can be used (such as
+/// `stringify!`).
+macro_rules! cstr {
+ ( $str:expr ) => {
+ #[allow(unused_unsafe)]
+ unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($str, "\0").as_bytes()) }
+ .as_ptr()
+ };
+}
+
+/// A GTK+ UI implementation.
+#[derive(Default)]
+pub struct UI {
+ running: AtomicBool,
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ unsafe {
+ let stream = gtk::g_memory_input_stream_new_from_data(
+ super::icon::PNG_DATA.as_ptr() as _,
+ // unwrap() because the PNG_DATA length will be well within gssize limits (32-bit
+ // at the smallest).
+ super::icon::PNG_DATA.len().try_into().unwrap(),
+ None,
+ );
+ let icon_pixbuf =
+ gtk::gdk_pixbuf_new_from_stream(stream, std::ptr::null_mut(), std::ptr::null_mut());
+ gtk::g_object_unref(stream as _);
+
+ gtk::gtk_window_set_default_icon(icon_pixbuf);
+
+ let app_ptr = gtk::gtk_application_new(
+ std::ptr::null(),
+ gtk::GApplicationFlags_G_APPLICATION_FLAGS_NONE,
+ );
+ gtk::g_signal_connect_data(
+ app_ptr as *mut _,
+ cstr!("activate"),
+ Some(std::mem::transmute(
+ render_application
+ as for<'a> unsafe extern "C" fn(*mut gtk::GtkApplication, &'a Application),
+ )),
+ &app as *const Application as *mut Application as _,
+ None,
+ 0,
+ );
+ self.running.store(true, Relaxed);
+ gtk::g_application_run(app_ptr as *mut gtk::GApplication, 0, std::ptr::null_mut());
+ self.running.store(false, Relaxed);
+ gtk::g_object_unref(app_ptr as *mut _);
+ gtk::g_object_unref(icon_pixbuf as _);
+ }
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ if !self.running.load(Relaxed) {
+ log::debug!("ignoring `invoke` as main loop isn't running");
+ return;
+ }
+ type BoxedData = Option<model::InvokeFn>;
+
+ unsafe extern "C" fn call(ptr: gtk::gpointer) -> gtk::gboolean {
+ let f: &mut BoxedData = ToPointer::from_ptr(ptr as _);
+ f.take().unwrap()();
+ false.into()
+ }
+
+ unsafe extern "C" fn drop(ptr: gtk::gpointer) {
+ let _: Box<BoxedData> = ToPointer::from_ptr(ptr as _);
+ }
+
+ let data: Box<BoxedData> = Box::new(Some(f));
+
+ unsafe {
+ let main_context = gtk::g_main_context_default();
+ gtk::g_main_context_invoke_full(
+ main_context,
+ 0, // G_PRIORITY_DEFAULT
+ Some(call as unsafe extern "C" fn(gtk::gpointer) -> gtk::gboolean),
+ data.to_ptr() as _,
+ Some(drop as unsafe extern "C" fn(gtk::gpointer)),
+ );
+ }
+ }
+}
+
+/// Types that can be converted to and from a pointer.
+///
+/// These types must be sized to avoid fat pointers (i.e., the pointers must be FFI-compatible, the
+/// same size as usize).
+trait ToPointer: Sized {
+ fn to_ptr(self) -> *mut ();
+ /// # Safety
+ /// The caller must ensure that the pointer was created as the result of `to_ptr` on the same
+ /// or a compatible type, and that the data is still valid.
+ unsafe fn from_ptr(ptr: *mut ()) -> Self;
+}
+
+/// Types that can be attached to a GLib object to be dropped when the widget is dropped.
+trait DropWithObject: Sized {
+ fn drop_with_object(self, object: *mut gtk::GObject);
+ fn drop_with_widget(self, widget: *mut gtk::GtkWidget) {
+ self.drop_with_object(widget as *mut _);
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char);
+}
+
+impl<T: ToPointer> DropWithObject for T {
+ fn drop_with_object(self, object: *mut gtk::GObject) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(
+ ptr: gtk::gpointer,
+ _object: *mut gtk::GObject,
+ ) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe { gtk::g_object_weak_ref(object, Some(free_ptr::<T>), self.to_ptr() as *mut _) };
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(ptr: gtk::gpointer) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe {
+ gtk::g_object_set_data_full(object, key, self.to_ptr() as *mut _, Some(free_ptr::<T>))
+ };
+ }
+}
+
+impl ToPointer for CString {
+ fn to_ptr(self) -> *mut () {
+ self.into_raw() as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ CString::from_raw(ptr as *mut c_char)
+ }
+}
+
+impl<T> ToPointer for Rc<T> {
+ fn to_ptr(self) -> *mut () {
+ Rc::into_raw(self) as *mut T as *mut ()
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Rc::from_raw(ptr as *mut T as *const T)
+ }
+}
+
+impl<T> ToPointer for Box<T> {
+ fn to_ptr(self) -> *mut () {
+ Box::into_raw(self) as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Box::from_raw(ptr as _)
+ }
+}
+
+impl<T: Sized> ToPointer for &mut T {
+ fn to_ptr(self) -> *mut () {
+ self as *mut T as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ &mut *(ptr as *mut T)
+ }
+}
+
+/// Connect a GTK+ object signal to a function, providing an additional context value (by
+/// reference).
+macro_rules! connect_signal {
+ ( object $object:expr ; with $with:expr ;
+ signal $name:ident ($target:ident : &$type:ty $(, $argname:ident : $argtype:ty )* ) $( -> $ret:ty )? $body:block
+ ) => {{
+ unsafe extern "C" fn $name($($argname : $argtype ,)* $target: &$type) $( -> $ret )? $body
+ #[allow(unused_unsafe)]
+ unsafe {
+ gtk::g_signal_connect_data(
+ $object as *mut _,
+ cstr!(stringify!($name)),
+ Some(std::mem::transmute(
+ $name
+ as for<'a> unsafe extern "C" fn(
+ $($argtype,)*
+ &'a $type,
+ ) $( -> $ret )?,
+ )),
+ $with as *const $type as *mut $type as _,
+ None,
+ 0,
+ );
+ }
+ }};
+}
+
+/// Bind a read only (from the renderer perspective) property to a widget.
+///
+/// The `set` function is called initially and when the property value changes.
+macro_rules! property_read_only {
+ ( property $property:expr ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ v.on_change(move |$name| $setbody);
+ }
+ Property::ReadOnly(_) => (),
+ }
+ }};
+}
+
+/// Bind a read/write property to a widget signal.
+///
+/// This currently only allows signals which are of the form
+/// `void(*)(SomeGtkObject*, gpointer user_data)`.
+macro_rules! property_with_signal {
+ ( object $object:expr ; property $property:expr ; signal $signame:ident ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ fn get( $getobj:ident : $gettype:ty ) -> $result:ty $getbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ let changing = Rc::new(RefCell::new(false));
+ struct SignalData {
+ changing: Rc<RefCell<bool>>,
+ value: Synchronized<$result>,
+ }
+ let signal_data = Box::new(SignalData {
+ changing: changing.clone(),
+ value: v.clone(),
+ });
+ v.on_change(move |$name| {
+ if !*changing.borrow() {
+ *changing.borrow_mut() = true;
+ $setbody;
+ *changing.borrow_mut() = false;
+ }
+ });
+ connect_signal! {
+ object $object;
+ with signal_data.as_ref();
+ signal $signame(signal_data: &SignalData, $getobj: $gettype) {
+ let new_value = (|| $getbody)();
+ if !*signal_data.changing.borrow() {
+ *signal_data.changing.borrow_mut() = true;
+ *signal_data.value.borrow_mut() = new_value;
+ *signal_data.changing.borrow_mut() = false;
+ }
+ }
+ }
+ signal_data.drop_with_object($object as _);
+ }
+ Property::ReadOnly(v) => {
+ v.register(move |target: &mut $result| {
+ let $getobj: $gettype = $object as _;
+ *target = (|| $getbody)();
+ });
+ }
+ }
+ }};
+}
+
+unsafe extern "C" fn render_application(app_ptr: *mut gtk::GtkApplication, app: &Application) {
+ unsafe {
+ gtk::gtk_widget_set_default_direction(if app.rtl {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_RTL
+ } else {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_LTR
+ });
+ }
+ for window in &app.windows {
+ let window_ptr = render_window(&window.element_type);
+ let style = &window.style;
+
+ // Set size before applying style (since the style will set the visibility and show the
+ // window). Note that we take the size request as an initial size here.
+ //
+ // `gtk_window_set_default_size` doesn't work; it resizes to the size request of the inner
+ // labels (instead of wrapping them) since it doesn't know how small they should be (that's
+ // dictated by the window size!).
+ unsafe {
+ gtk::gtk_window_resize(
+ window_ptr as _,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+ }
+
+ apply_style(window_ptr, style);
+ unsafe {
+ gtk::gtk_application_add_window(app_ptr, window_ptr as *mut _);
+ }
+ }
+}
+
+fn render(element: &Element) -> Option<*mut gtk::GtkWidget> {
+ let widget = render_element_type(&element.element_type)?;
+ apply_style(widget, &element.style);
+ Some(widget)
+}
+
+fn apply_style(widget: *mut gtk::GtkWidget, style: &model::ElementStyle) {
+ unsafe {
+ gtk::gtk_widget_set_halign(widget, alignment(&style.horizontal_alignment));
+ if style.horizontal_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_hexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_valign(widget, alignment(&style.vertical_alignment));
+ if style.vertical_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_vexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_size_request(
+ widget,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+
+ gtk::gtk_widget_set_margin_start(widget, style.margin.start as i32);
+ gtk::gtk_widget_set_margin_end(widget, style.margin.end as i32);
+ gtk::gtk_widget_set_margin_top(widget, style.margin.top as i32);
+ gtk::gtk_widget_set_margin_bottom(widget, style.margin.bottom as i32);
+ }
+ property_read_only! {
+ property &style.visible;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_visible(widget, new_value.clone().into());
+ };
+ }
+ }
+ property_read_only! {
+ property &style.enabled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_sensitive(widget, new_value.clone().into());
+ }
+ }
+ }
+}
+
+fn alignment(align: &Alignment) -> gtk::GtkAlign {
+ use Alignment::*;
+ match align {
+ Start => gtk::GtkAlign_GTK_ALIGN_START,
+ Center => gtk::GtkAlign_GTK_ALIGN_CENTER,
+ End => gtk::GtkAlign_GTK_ALIGN_END,
+ Fill => gtk::GtkAlign_GTK_ALIGN_FILL,
+ }
+}
+
+struct PangoAttrList {
+ list: *mut gtk::PangoAttrList,
+}
+
+impl PangoAttrList {
+ pub fn new() -> Self {
+ PangoAttrList {
+ list: unsafe { gtk::pango_attr_list_new() },
+ }
+ }
+
+ pub fn bold(&mut self) -> &mut Self {
+ unsafe {
+ gtk::pango_attr_list_insert(
+ self.list,
+ gtk::pango_attr_weight_new(gtk::PangoWeight_PANGO_WEIGHT_BOLD),
+ )
+ };
+ self
+ }
+}
+
+impl std::ops::Deref for PangoAttrList {
+ type Target = *mut gtk::PangoAttrList;
+
+ fn deref(&self) -> &Self::Target {
+ &self.list
+ }
+}
+
+impl std::ops::DerefMut for PangoAttrList {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.list
+ }
+}
+
+impl Drop for PangoAttrList {
+ fn drop(&mut self) {
+ unsafe { gtk::pango_attr_list_unref(self.list) };
+ }
+}
+
+fn render_element_type(element_type: &model::ElementType) -> Option<*mut gtk::GtkWidget> {
+ use model::ElementType::*;
+ Some(match element_type {
+ Label(model::Label { text, bold }) => {
+ let label_ptr = unsafe { gtk::gtk_label_new(std::ptr::null()) };
+ match text {
+ Property::Static(text) => {
+ let text = CString::new(text.clone()).ok()?;
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, text.as_ptr()) };
+ text.drop_with_widget(label_ptr);
+ }
+ Property::Binding(b) => {
+ let label_text = Rc::new(RefCell::new(CString::new(b.borrow().clone()).ok()?));
+ unsafe {
+ gtk::gtk_label_set_text(label_ptr as _, label_text.borrow().as_ptr())
+ };
+ let lt = label_text.clone();
+ label_text.drop_with_widget(label_ptr);
+ b.on_change(move |t| {
+ let Some(cstr) = CString::new(t.clone()).ok() else {
+ return;
+ };
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, cstr.as_ptr()) };
+ *lt.borrow_mut() = cstr;
+ });
+ }
+ Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"),
+ }
+ unsafe { gtk::gtk_label_set_line_wrap(label_ptr as _, true.into()) };
+ // This is gtk_label_set_{xalign,yalign} in gtk 3.16+
+ unsafe { gtk::gtk_misc_set_alignment(label_ptr as _, 0.0, 0.5) };
+ if *bold {
+ unsafe {
+ gtk::gtk_label_set_attributes(label_ptr as _, **PangoAttrList::new().bold())
+ };
+ }
+ label_ptr
+ }
+ HBox(model::HBox {
+ items,
+ spacing,
+ affirmative_order,
+ }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_HORIZONTAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ let items_iter: Box<dyn Iterator<Item = &Element>> = if *affirmative_order {
+ Box::new(items.iter().rev())
+ } else {
+ Box::new(items.iter())
+ };
+ for item in items_iter {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case horizontal alignment to pack into the end if appropriate
+ if item.style.horizontal_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ VBox(model::VBox { items, spacing }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_VERTICAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ for item in items {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case vertical alignment to pack into the end if appropriate
+ if item.style.vertical_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ Button(model::Button { content, click }) => {
+ let button_ptr = unsafe { gtk::gtk_button_new() };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ // Always center widgets in buttons.
+ gtk::gtk_widget_set_valign(widget, alignment(&Alignment::Center));
+ gtk::gtk_widget_set_halign(widget, alignment(&Alignment::Center));
+ gtk::gtk_container_add(button_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ connect_signal! {
+ object button_ptr;
+ with click;
+ signal clicked(event: &Event<()>, _button: *mut gtk::GtkButton) {
+ event.fire(&());
+ }
+ }
+ button_ptr
+ }
+ Checkbox(model::Checkbox { checked, label }) => {
+ let cb_ptr = match label {
+ None => unsafe { gtk::gtk_check_button_new() },
+ Some(s) => {
+ let label = CString::new(s.clone()).ok()?;
+ let cb = unsafe { gtk::gtk_check_button_new_with_label(label.as_ptr()) };
+ label.drop_with_widget(cb);
+ cb
+ }
+ };
+ property_with_signal! {
+ object cb_ptr;
+ property checked;
+ signal toggled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_toggle_button_set_active(cb_ptr as *mut _, new_value.clone().into())
+ };
+ }
+ fn get(button: *mut gtk::GtkToggleButton) -> bool {
+ unsafe {
+ gtk::gtk_toggle_button_get_active(button) == 1
+ }
+ }
+ }
+ cb_ptr
+ }
+ TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let text_ptr = unsafe { gtk::gtk_text_view_new() };
+ unsafe {
+ const GTK_WRAP_WORD_CHAR: u32 = 3;
+ gtk::gtk_text_view_set_wrap_mode(text_ptr as *mut _, GTK_WRAP_WORD_CHAR);
+ gtk::gtk_text_view_set_editable(text_ptr as *mut _, editable.clone().into());
+ gtk::gtk_text_view_set_accepts_tab(text_ptr as *mut _, false.into());
+ }
+ let buffer = unsafe { gtk::gtk_text_view_get_buffer(text_ptr as *mut _) };
+
+ struct State {
+ placeholder: Option<Placeholder>,
+ }
+
+ struct Placeholder {
+ string: CString,
+ visible: RefCell<bool>,
+ }
+
+ impl Placeholder {
+ fn focus(&self, widget: *mut gtk::GtkWidget) {
+ if *self.visible.borrow() {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), 0);
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ std::ptr::null(),
+ );
+ }
+ *self.visible.borrow_mut() = false;
+ }
+ }
+
+ fn unfocus(&self, widget: *mut gtk::GtkWidget) {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+
+ let mut end_iter = gtk::GtkTextIter::default();
+ gtk::gtk_text_buffer_get_end_iter(buffer, &mut end_iter);
+ let is_empty = gtk::gtk_text_iter_get_offset(&end_iter) == 0;
+
+ if is_empty && !*self.visible.borrow() {
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), -1);
+ let context = gtk::gtk_widget_get_style_context(widget);
+ let mut color = gtk::GdkRGBA::default();
+ gtk::gtk_style_context_get_color(
+ context,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_INSENSITIVE,
+ &mut color,
+ );
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ &color,
+ );
+ *self.visible.borrow_mut() = true;
+ }
+ }
+ }
+ }
+
+ let mut state = Box::new(State { placeholder: None });
+
+ if let Some(placeholder) = placeholder {
+ state.placeholder = Some(Placeholder {
+ string: CString::new(placeholder.clone()).ok()?,
+ visible: RefCell::new(false),
+ });
+
+ let placeholder = state.placeholder.as_ref().unwrap();
+
+ placeholder.unfocus(text_ptr);
+
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_in_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.focus(widget);
+ false.into()
+ }
+ }
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_out_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.unfocus(widget);
+ false.into()
+ }
+ }
+ }
+
+ // Attach the state so that we can access it in the changed signal.
+ // This is kind of ugly; in the future it might be a nicer developer interface to simply
+ // always use a closure as the user data (which itself can capture arbitrary things). This
+ // would move the data management from GTK to rust.
+ state.set_data(buffer as *mut _, cstr!("textview-state"));
+
+ property_with_signal! {
+ object buffer;
+ property content;
+ signal changed;
+ fn set(new_value: &String) {
+ unsafe {
+ gtk::gtk_text_buffer_set_text(buffer, new_value.as_ptr() as *const c_char, new_value.len().try_into().unwrap());
+ }
+ }
+ fn get(buffer: *mut gtk::GtkTextBuffer) -> String {
+ let state = unsafe {
+ gtk::g_object_get_data(buffer as *mut _, cstr!("textview-state"))
+ };
+ if !state.is_null() {
+ let s: &State = unsafe { &*(state as *mut State as *const State) };
+ if let Some(placeholder) = &s.placeholder {
+ if *placeholder.visible.borrow() {
+ return "".to_owned();
+ }
+ }
+ }
+ let mut start_iter = gtk::GtkTextIter::default();
+ unsafe {
+ gtk::gtk_text_buffer_get_start_iter(buffer, &mut start_iter);
+ }
+
+ let mut s = String::new();
+ loop {
+ let c = unsafe { gtk::gtk_text_iter_get_char(&start_iter) };
+ if c == 0 {
+ break;
+ }
+ // Safety:
+ // gunichar is guaranteed to be a valid unicode codepoint (if nonzero).
+ s.push(unsafe { char::from_u32_unchecked(c) });
+ unsafe {
+ gtk::gtk_text_iter_forward_char(&mut start_iter);
+ }
+ }
+ s
+ }
+ }
+ text_ptr
+ }
+ Scroll(model::Scroll { content }) => {
+ let scroll_ptr =
+ unsafe { gtk::gtk_scrolled_window_new(std::ptr::null_mut(), std::ptr::null_mut()) };
+ unsafe {
+ gtk::gtk_scrolled_window_set_policy(
+ scroll_ptr as *mut _,
+ gtk::GtkPolicyType_GTK_POLICY_NEVER,
+ gtk::GtkPolicyType_GTK_POLICY_ALWAYS,
+ );
+ gtk::gtk_scrolled_window_set_shadow_type(
+ scroll_ptr as *mut _,
+ gtk::GtkShadowType_GTK_SHADOW_IN,
+ );
+ };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ gtk::gtk_container_add(scroll_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ scroll_ptr
+ }
+ Progress(model::Progress { amount }) => {
+ let progress_ptr = unsafe { gtk::gtk_progress_bar_new() };
+ property_read_only! {
+ property amount;
+ fn set(value: &Option<f32>) {
+ match &*value {
+ Some(v) => unsafe {
+ gtk::gtk_progress_bar_set_fraction(
+ progress_ptr as *mut _,
+ v.clamp(0f32,1f32) as f64,
+ );
+ }
+ None => unsafe {
+ gtk::gtk_progress_bar_pulse(progress_ptr as *mut _);
+
+ fn auto_pulse_progress_bar(progress: *mut gtk::GtkProgressBar) {
+ unsafe extern fn pulse(progress: *mut std::ffi::c_void) -> gtk::gboolean {
+ if gtk::gtk_widget_is_visible(progress as _) == 0 {
+ false.into()
+ } else {
+ gtk::gtk_progress_bar_pulse(progress as _);
+ true.into()
+ }
+ }
+ unsafe {
+ gtk::g_timeout_add(100, Some(pulse as unsafe extern fn(*mut std::ffi::c_void) -> gtk::gboolean), progress as _);
+ }
+
+ }
+
+ connect_signal! {
+ object progress_ptr;
+ with std::ptr::null_mut();
+ signal show(_user_data: &(), progress: *mut gtk::GtkWidget) {
+ auto_pulse_progress_bar(progress as *mut _);
+ }
+ }
+ auto_pulse_progress_bar(progress_ptr as *mut _);
+ }
+ }
+ }
+ }
+ progress_ptr
+ }
+ })
+}
+
+fn render_window(
+ model::Window {
+ title,
+ content,
+ children,
+ modal,
+ close,
+ }: &model::Window,
+) -> *mut gtk::GtkWidget {
+ unsafe {
+ let window_ptr = gtk::gtk_window_new(gtk::GtkWindowType_GTK_WINDOW_TOPLEVEL);
+ if !title.is_empty() {
+ if let Some(title) = CString::new(title.clone()).ok() {
+ gtk::gtk_window_set_title(window_ptr as *mut _, title.as_ptr());
+ title.drop_with_widget(window_ptr);
+ }
+ }
+ if let Some(content) = content {
+ if let Some(widget) = render(content) {
+ gtk::gtk_container_add(window_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ for child in children {
+ let widget = render_window(&child.element_type);
+ apply_style(widget, &child.style);
+ gtk::gtk_window_set_transient_for(widget as *mut _, window_ptr as *mut _);
+ // Delete should hide the window.
+ gtk::g_signal_connect_data(
+ widget as *mut _,
+ cstr!("delete-event"),
+ Some(std::mem::transmute(
+ gtk::gtk_widget_hide_on_delete
+ as unsafe extern "C" fn(*mut gtk::GtkWidget) -> i32,
+ )),
+ std::ptr::null_mut(),
+ None,
+ 0,
+ );
+ }
+ if *modal {
+ gtk::gtk_window_set_modal(window_ptr as *mut _, true.into());
+ }
+ if let Some(close) = close {
+ close.subscribe(move |&()| gtk::gtk_window_close(window_ptr as *mut _));
+ }
+
+ window_ptr
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/mod.rs b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs
new file mode 100644
index 0000000000..6520d16472
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs
@@ -0,0 +1,1122 @@
+/* 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/. */
+
+//! A UI using the macos cocoa API.
+//!
+//! This UI contains some edge cases that aren't implemented, for instance:
+//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button
+//! containing Label), etc.
+//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, Text
+//! doesn't handle Binding, etc).
+//!
+//! The rendering ignores `ElementStyle::margin` entirely, because
+//! * `NSView` doesn't support margins (so working them into constraints would be a bit annoying),
+//! and
+//! * `NSView.layoutMarginsGuide` results in a layout almost identical to what the margins (at the
+//! time of this writing) in the UI layouts are achieving.
+//!
+//! In a few cases, init or creation functions are called which _could_ return nil and are wrapped
+//! in their type wrapper (as those functions return `instancetype`/`id`). We consider this safe
+//! enough because it won't cause unsoundness (they are only passed to objc functions which can
+//! take nil arguments) and the failure case is very unlikely.
+
+#![allow(non_upper_case_globals)]
+
+use self::objc::*;
+use super::model::{self, Alignment, Application, Element, TypedElement};
+use crate::data::Property;
+use cocoa::{
+ INSApplication, INSBox, INSButton, INSColor, INSControl, INSFont, INSLayoutAnchor,
+ INSLayoutConstraint, INSLayoutDimension, INSLayoutGuide, INSMenu, INSMenuItem,
+ INSMutableParagraphStyle, INSObject, INSProcessInfo, INSProgressIndicator, INSRunLoop,
+ INSScrollView, INSStackView, INSText, INSTextContainer, INSTextField, INSTextView, INSView,
+ INSWindow, NSArray_NSArrayCreation, NSAttributedString_NSExtendedAttributedString,
+ NSDictionary_NSDictionaryCreation, NSRunLoop_NSRunLoopConveniences,
+ NSStackView_NSStackViewGravityAreas, NSString_NSStringExtensionMethods,
+ NSTextField_NSTextFieldConvenience, NSView_NSConstraintBasedLayoutInstallingConstraints,
+ NSView_NSConstraintBasedLayoutLayering, NSView_NSSafeAreas, PNSObject,
+};
+use once_cell::sync::Lazy;
+
+/// https://developer.apple.com/documentation/foundation/1497293-string_encodings/nsutf8stringencoding?language=objc
+const NSUTF8StringEncoding: cocoa::NSStringEncoding = 4;
+
+/// Constant from NSCell.h
+const NSControlStateValueOn: cocoa::NSControlStateValue = 1;
+
+/// Constant from NSLayoutConstraint.h
+const NSLayoutPriorityDefaultHigh: cocoa::NSLayoutPriority = 750.0;
+
+mod objc;
+
+/// A MacOS Cocoa UI implementation.
+#[derive(Default)]
+pub struct UI;
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ let nsapp = unsafe { cocoa::NSApplication::sharedApplication() };
+
+ Objc::<AppDelegate>::register();
+ Objc::<Button>::register();
+ Objc::<Checkbox>::register();
+ Objc::<TextView>::register();
+ Objc::<Window>::register();
+
+ rc::autoreleasepool(|| {
+ let delegate = AppDelegate::new(app).into_object();
+ // Set delegate
+ unsafe { nsapp.setDelegate_(delegate.instance as *mut _) };
+
+ // Set up the main menu
+ unsafe {
+ let appname = read_nsstring(cocoa::NSProcessInfo::processInfo().processName());
+ let mainmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ mainmenu.init();
+
+ {
+ // We don't need a title for the app menu item nor menu; it will always come from
+ // the process name regardless of what we set.
+ let appmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease();
+ appmenuitem.init();
+ mainmenu.addItem_(appmenuitem);
+
+ let appmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ appmenu.init();
+
+ {
+ let quit = StrongRef::new(cocoa::NSMenuItem::alloc());
+ quit.initWithTitle_action_keyEquivalent_(
+ nsstring(&format!("Quit {appname}")),
+ sel!(terminate:),
+ nsstring("q"),
+ );
+ appmenu.addItem_(quit.autorelease());
+ }
+ appmenuitem.setSubmenu_(appmenu.autorelease());
+ }
+ {
+ let editmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease();
+ editmenuitem.initWithTitle_action_keyEquivalent_(
+ nsstring("Edit"),
+ runtime::Sel::from_ptr(std::ptr::null()),
+ nsstring(""),
+ );
+ mainmenu.addItem_(editmenuitem);
+
+ let editmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ editmenu.initWithTitle_(nsstring("Edit"));
+
+ let add_item = |name, selector, shortcut| {
+ let item = StrongRef::new(cocoa::NSMenuItem::alloc());
+ item.initWithTitle_action_keyEquivalent_(
+ nsstring(name),
+ selector,
+ nsstring(shortcut),
+ );
+ editmenu.addItem_(item.autorelease());
+ };
+
+ add_item("Undo", sel!(undo:), "z");
+ add_item("Redo", sel!(redo:), "Z");
+ editmenu.addItem_(cocoa::NSMenuItem::separatorItem());
+ add_item("Cut", sel!(cut:), "x");
+ add_item("Copy", sel!(copy:), "c");
+ add_item("Paste", sel!(paste:), "v");
+ add_item("Delete", sel!(delete:), "");
+ add_item("Select All", sel!(selectAll:), "a");
+
+ editmenuitem.setSubmenu_(editmenu.autorelease());
+ }
+
+ nsapp.setMainMenu_(mainmenu.autorelease());
+ }
+
+ // Run the main application loop
+ unsafe { nsapp.run() };
+ });
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ // Blocks only take `Fn`, so we have to wrap the boxed function.
+ let f = std::cell::RefCell::new(Some(f));
+ enqueue(move || {
+ if let Some(f) = f.borrow_mut().take() {
+ f();
+ }
+ });
+ }
+}
+
+fn enqueue<F: Fn() + 'static>(f: F) {
+ let block = block::ConcreteBlock::new(f);
+ // The block must be an RcBlock so addOperationWithBlock can retain it.
+ // https://docs.rs/block/latest/block/#creating-blocks
+ let block = block.copy();
+
+ // We need to explicitly signal that the enqueued blocks can run in both the default mode (the
+ // main loop) and modal mode, otherwise when modal windows are opened things get stuck.
+ struct RunloopModes(cocoa::NSArray);
+
+ impl RunloopModes {
+ pub fn new() -> Self {
+ unsafe {
+ let objects: [cocoa::id; 2] = [
+ cocoa::NSDefaultRunLoopMode.0,
+ cocoa::NSModalPanelRunLoopMode.0,
+ ];
+ RunloopModes(
+ cocoa::NSArray(<cocoa::NSArray as NSArray_NSArrayCreation<
+ cocoa::NSRunLoopMode,
+ >>::arrayWithObjects_count_(
+ objects.as_slice().as_ptr() as *const *mut u64,
+ objects
+ .as_slice()
+ .len()
+ .try_into()
+ .expect("usize can't fit in u64"),
+ )),
+ )
+ }
+ }
+ }
+
+ // # Safety
+ // The array is static and cannot be changed.
+ unsafe impl Sync for RunloopModes {}
+ unsafe impl Send for RunloopModes {}
+
+ static RUNLOOP_MODES: Lazy<RunloopModes> = Lazy::new(RunloopModes::new);
+
+ unsafe {
+ cocoa::NSRunLoop::mainRunLoop().performInModes_block_(RUNLOOP_MODES.0, &*block);
+ }
+}
+
+#[repr(transparent)]
+struct Rect(pub cocoa::NSRect);
+
+unsafe impl Encode for Rect {
+ fn encode() -> Encoding {
+ unsafe { Encoding::from_str("{CGRect={CGPoint=dd}{CGSize=dd}}") }
+ }
+}
+
+/// Create an NSString by copying a str.
+fn nsstring(v: &str) -> cocoa::NSString {
+ unsafe {
+ StrongRef::new(cocoa::NSString(
+ cocoa::NSString::alloc().initWithBytes_length_encoding_(
+ v.as_ptr() as *const _,
+ v.len().try_into().expect("usize can't fit in u64"),
+ NSUTF8StringEncoding,
+ ),
+ ))
+ }
+ .autorelease()
+}
+
+/// Create a String by copying an NSString
+fn read_nsstring(s: cocoa::NSString) -> String {
+ let c_str = unsafe { std::ffi::CStr::from_ptr(s.UTF8String()) };
+ c_str.to_str().expect("NSString isn't UTF8").to_owned()
+}
+
+fn nsrect<X: Into<f64>, Y: Into<f64>, W: Into<f64>, H: Into<f64>>(
+ x: X,
+ y: Y,
+ w: W,
+ h: H,
+) -> cocoa::NSRect {
+ cocoa::NSRect {
+ origin: cocoa::NSPoint {
+ x: x.into(),
+ y: y.into(),
+ },
+ size: cocoa::NSSize {
+ width: w.into(),
+ height: h.into(),
+ },
+ }
+}
+
+struct AppDelegate {
+ app: Option<Application>,
+ windows: Vec<StrongRef<cocoa::NSWindow>>,
+}
+
+impl AppDelegate {
+ pub fn new(app: Application) -> Self {
+ AppDelegate {
+ app: Some(app),
+ windows: Default::default(),
+ }
+ }
+}
+
+objc_class! {
+ impl AppDelegate: NSObject /*<NSApplicationDelegate>*/ {
+ #[sel(applicationDidFinishLaunching:)]
+ fn application_did_finish_launching(&mut self, _notification: Ptr<cocoa::NSNotification>) {
+ // Activate the application (bringing windows to the active foreground later)
+ unsafe { cocoa::NSApplication::sharedApplication().activateIgnoringOtherApps_(runtime::YES) };
+
+ let mut first = true;
+ let mut windows = WindowRenderer::default();
+ let app = self.app.take().unwrap();
+ windows.rtl = app.rtl;
+ for window in app.windows {
+ let w = windows.render(window);
+ unsafe {
+ if first {
+ w.makeKeyAndOrderFront_(self.instance);
+ w.makeMainWindow();
+ first = false;
+ }
+ }
+ }
+ self.windows = windows.unwrap();
+
+ }
+
+ #[sel(applicationShouldTerminateAfterLastWindowClosed:)]
+ fn application_should_terminate_after_window_closed(&mut self, _app: Ptr<cocoa::NSApplication>) -> runtime::BOOL {
+ runtime::YES
+ }
+ }
+}
+
+struct Window {
+ modal: bool,
+ title: String,
+ style: model::ElementStyle,
+}
+
+objc_class! {
+ impl Window: NSWindow /*<NSWindowDelegate>*/ {
+ #[sel(init)]
+ fn init(&mut self) -> cocoa::id {
+ let style = &self.style;
+ let title = &self.title;
+ let w = cocoa::NSWindow(self.instance);
+
+ unsafe {
+ if w.initWithContentRect_styleMask_backing_defer_(
+ nsrect(
+ 0,
+ 0,
+ style.horizontal_size_request.unwrap_or(800),
+ style.vertical_size_request.unwrap_or(500),
+ ),
+ cocoa::NSWindowStyleMaskTitled
+ | cocoa::NSWindowStyleMaskClosable
+ | cocoa::NSWindowStyleMaskResizable
+ | cocoa::NSWindowStyleMaskMiniaturizable,
+ cocoa::NSBackingStoreBuffered,
+ runtime::NO,
+ ).is_null() {
+ return std::ptr::null_mut();
+ }
+
+ w.setDelegate_(self.instance as _);
+ w.setMinSize_(cocoa::NSSize {
+ width: style.horizontal_size_request.unwrap_or(0) as f64,
+ height: style.vertical_size_request.unwrap_or(0) as f64,
+ });
+
+ if !title.is_empty() {
+ w.setTitle_(nsstring(title.as_str()));
+ }
+ }
+
+ self.instance
+ }
+
+ #[sel(windowWillClose:)]
+ fn window_will_close(&mut self, _notification: Ptr<cocoa::NSNotification>) {
+ if self.modal {
+ unsafe {
+ let nsapp = cocoa::NSApplication::sharedApplication();
+ nsapp.stopModal();
+ }
+ }
+ }
+ }
+}
+
+impl From<Objc<Window>> for cocoa::NSWindow {
+ fn from(ptr: Objc<Window>) -> Self {
+ cocoa::NSWindow(ptr.instance)
+ }
+}
+
+struct Button {
+ element: model::Button,
+}
+
+impl Button {
+ pub fn with_title(self, title: &str) -> cocoa::NSButton {
+ let obj = self.into_object();
+ unsafe {
+ let () = msg_send![obj.instance, setTitle: nsstring(title)];
+ }
+ // # Safety
+ // NSButton is the superclass of Objc<Button>.
+ unsafe { std::mem::transmute(obj.autorelease()) }
+ }
+}
+
+objc_class! {
+ impl Button: NSButton {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setBezelStyle: cocoa::NSBezelStyleRounded];
+ let () = msg_send![object, setAction: sel!(didClick)];
+ let () = msg_send![object, setTarget: object];
+ object
+ }
+ }
+
+ #[sel(didClick)]
+ fn did_click(&mut self) {
+ self.element.click.fire(&());
+ }
+ }
+}
+
+struct Checkbox {
+ element: model::Checkbox,
+}
+
+objc_class! {
+ impl Checkbox: NSButton {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setButtonType: cocoa::NSButtonTypeSwitch];
+ if let Some(label) = &self.element.label {
+ let () = msg_send![object, setTitle: nsstring(label.as_str())];
+ }
+ let () = msg_send![object, setAction: sel!(didClick:)];
+ let () = msg_send![object, setTarget: object];
+
+ match &self.element.checked {
+ Property::Binding(s) => {
+ if *s.borrow() {
+ let () = msg_send![object, setState: NSControlStateValueOn];
+ }
+ }
+ Property::ReadOnly(_) => (),
+ Property::Static(_) => (),
+ }
+
+ object
+ }
+ }
+
+ #[sel(didClick:)]
+ fn did_click(&mut self, button: Objc<Checkbox>) {
+ match &self.element.checked {
+ Property::Binding(s) => {
+ let state = unsafe { std::mem::transmute::<_, cocoa::NSButton>(button).state() };
+ *s.borrow_mut() = state == NSControlStateValueOn;
+ }
+ Property::ReadOnly(_) => (),
+ Property::Static(_) => (),
+ }
+ }
+ }
+}
+
+impl Checkbox {
+ pub fn into_button(self) -> cocoa::NSButton {
+ let obj = self.into_object();
+ // # Safety
+ // NSButton is the superclass of Objc<Checkbox>.
+ unsafe { std::mem::transmute(obj.autorelease()) }
+ }
+}
+
+struct TextView;
+
+objc_class! {
+ impl TextView: NSTextView /*<NSTextViewDelegate>*/ {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSTextView)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setDelegate: self.instance];
+ object
+ }
+ }
+
+ #[sel(textView:doCommandBySelector:)]
+ fn do_command_by_selector(&mut self, text_view: Ptr<cocoa::NSTextView>, selector: runtime::Sel) -> runtime::BOOL {
+ let Ptr(text_view) = text_view;
+ // Make Tab/Backtab navigate to key views rather than inserting tabs in the text view.
+ // We can't use the `NSText` `fieldEditor` property to implement this behavior because
+ // that will disable the Enter key.
+ if selector == sel!(insertTab:) {
+ unsafe { text_view.window().selectNextKeyView_(text_view.0) };
+ return runtime::YES;
+ } else if selector == sel!(insertBacktab:) {
+ unsafe { text_view.window().selectPreviousKeyView_(text_view.0) };
+ return runtime::YES;
+ }
+ runtime::NO
+ }
+ }
+}
+
+impl From<Objc<TextView>> for cocoa::NSTextView {
+ fn from(tv: Objc<TextView>) -> Self {
+ // # Safety
+ // NSTextView is the superclass of Objc<TextView>.
+ unsafe { std::mem::transmute(tv) }
+ }
+}
+
+// For some reason the bindgen code for the nslayoutanchor subclasses doesn't have
+// `Into<NSLayoutAnchor>`, so we add our own.
+trait IntoNSLayoutAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor;
+}
+
+impl IntoNSLayoutAnchor for cocoa::NSLayoutXAxisAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor {
+ // # Safety
+ // NSLayoutXAxisAnchor is a subclass of NSLayoutAnchor
+ cocoa::NSLayoutAnchor(self.0)
+ }
+}
+
+impl IntoNSLayoutAnchor for cocoa::NSLayoutYAxisAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor {
+ // # Safety
+ // NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor
+ cocoa::NSLayoutAnchor(self.0)
+ }
+}
+
+unsafe fn constraint_equal<T, O>(anchor: T, to: O)
+where
+ T: INSLayoutAnchor<()> + std::ops::Deref,
+ T::Target: Message + Sized,
+ O: IntoNSLayoutAnchor,
+{
+ anchor
+ .constraintEqualToAnchor_(to.into_layout_anchor())
+ .setActive_(runtime::YES);
+}
+
+#[derive(Default)]
+struct WindowRenderer {
+ windows_to_retain: Vec<StrongRef<cocoa::NSWindow>>,
+ rtl: bool,
+}
+
+impl WindowRenderer {
+ pub fn unwrap(self) -> Vec<StrongRef<cocoa::NSWindow>> {
+ self.windows_to_retain
+ }
+
+ pub fn render(&mut self, window: TypedElement<model::Window>) -> StrongRef<cocoa::NSWindow> {
+ let style = window.style;
+ let model::Window {
+ close,
+ children,
+ content,
+ modal,
+ title,
+ } = window.element_type;
+
+ let w = Window {
+ modal,
+ title,
+ style,
+ }
+ .into_object();
+
+ let nswindow: StrongRef<cocoa::NSWindow> = w.clone().cast();
+
+ unsafe {
+ // Don't release windows when closed: we retain windows at the top-level.
+ nswindow.setReleasedWhenClosed_(runtime::NO);
+
+ if let Some(close) = close {
+ let nswindow = nswindow.weak();
+ close.subscribe(move |&()| {
+ if let Some(nswindow) = nswindow.lock() {
+ nswindow.close();
+ }
+ });
+ }
+
+ if let Some(e) = content {
+ // Use an NSBox as a container view so that the window's content can easily have
+ // constraints set up relative to the parent (they can't be set relative to the
+ // window).
+ let content_parent: StrongRef<cocoa::NSBox> = msg_send![class!(NSBox), new];
+ content_parent.setTitlePosition_(cocoa::NSNoTitle);
+ content_parent.setTransparent_(runtime::YES);
+ content_parent.setContentViewMargins_(cocoa::NSSize {
+ width: 5.0,
+ height: 5.0,
+ });
+ if ViewRenderer::new_with_selector(self.rtl, *content_parent, sel!(setContentView:))
+ .render(*e)
+ {
+ nswindow.setContentView_((*content_parent).into());
+ }
+ }
+
+ for child in children {
+ let modal = child.element_type.modal;
+ let visible = child.style.visible.clone();
+ let child_window = self.render(child);
+
+ #[derive(Clone, Copy)]
+ struct ShowChild {
+ modal: bool,
+ }
+
+ impl ShowChild {
+ pub fn show(&self, parent: cocoa::NSWindow, child: cocoa::NSWindow) {
+ unsafe {
+ parent.addChildWindow_ordered_(child, cocoa::NSWindowAbove);
+ child.makeKeyAndOrderFront_(parent.0);
+ if self.modal {
+ // Run the modal from the main nsapp.run() loop to prevent binding
+ // updates from being nested (as this will block until the modal is
+ // stopped).
+ enqueue(move || {
+ let nsapp = cocoa::NSApplication::sharedApplication();
+ nsapp.runModalForWindow_(child);
+ });
+ }
+ }
+ }
+ }
+
+ let show_child = ShowChild { modal };
+
+ match visible {
+ Property::Static(visible) => {
+ if visible {
+ show_child.show(*nswindow, *child_window);
+ }
+ }
+ Property::Binding(b) => {
+ let child = child_window.weak();
+ let parent = nswindow.weak();
+ b.on_change(move |visible| {
+ let Some((w, child_window)) = parent.lock().zip(child.lock()) else {
+ return;
+ };
+ if *visible {
+ show_child.show(*w, *child_window);
+ } else {
+ child_window.close();
+ }
+ });
+ if *b.borrow() {
+ show_child.show(*nswindow, *child_window);
+ }
+ }
+ Property::ReadOnly(_) => panic!("window visibility cannot be ReadOnly"),
+ }
+ }
+ }
+ self.windows_to_retain.push(nswindow.clone());
+ nswindow
+ }
+}
+
+struct ViewRenderer {
+ parent: cocoa::NSView,
+ add_subview: Box<dyn Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView)>,
+ ignore_vertical: bool,
+ ignore_horizontal: bool,
+ rtl: bool,
+}
+
+impl ViewRenderer {
+ /// add_subview should add the rendered child view.
+ pub fn new<F>(rtl: bool, parent: impl Into<cocoa::NSView>, add_subview: F) -> Self
+ where
+ F: Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView) + 'static,
+ {
+ ViewRenderer {
+ parent: parent.into(),
+ add_subview: Box::new(add_subview),
+ ignore_vertical: false,
+ ignore_horizontal: false,
+ rtl,
+ }
+ }
+
+ /// add_subview should be the selector to call on the parent view to add the rendered child view.
+ pub fn new_with_selector(
+ rtl: bool,
+ parent: impl Into<cocoa::NSView>,
+ add_subview: runtime::Sel,
+ ) -> Self {
+ Self::new(rtl, parent, move |parent, _style, child| {
+ let () = unsafe { (*parent.0).send_message(add_subview, (child,)) }.unwrap();
+ })
+ }
+
+ /// Ignore vertical layout settings when rendering views.
+ pub fn ignore_vertical(mut self, setting: bool) -> Self {
+ self.ignore_vertical = setting;
+ self
+ }
+
+ /// Ignore horizontal layout settings when rendering views.
+ pub fn ignore_horizontal(mut self, setting: bool) -> Self {
+ self.ignore_horizontal = setting;
+ self
+ }
+
+ /// Render the given element.
+ ///
+ /// Returns whether the element was rendered.
+ pub fn render(
+ &self,
+ Element {
+ style,
+ element_type,
+ }: Element,
+ ) -> bool {
+ let Some(view) = render_element(element_type, &style, self.rtl) else {
+ return false;
+ };
+
+ (self.add_subview)(self.parent, &style, view);
+
+ // Setting the content hugging priority to a high value causes stackviews to not stretch
+ // subviews during autolayout.
+ unsafe {
+ view.setContentHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationHorizontal,
+ );
+ view.setContentHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationVertical,
+ );
+ }
+
+ // Set layout and writing direction based on RTL.
+ unsafe {
+ view.setUserInterfaceLayoutDirection_(if self.rtl {
+ cocoa::NSUserInterfaceLayoutDirectionRightToLeft
+ } else {
+ cocoa::NSUserInterfaceLayoutDirectionLeftToRight
+ });
+ if let Ok(control) = cocoa::NSControl::try_from(view) {
+ control.setBaseWritingDirection_(if self.rtl {
+ cocoa::NSWritingDirectionRightToLeft
+ } else {
+ cocoa::NSWritingDirectionLeftToRight
+ });
+ }
+ }
+
+ let lmg = unsafe { self.parent.layoutMarginsGuide() };
+
+ if !matches!(style.horizontal_alignment, Alignment::Fill) {
+ if let Some(size) = style.horizontal_size_request {
+ unsafe {
+ view.widthAnchor()
+ .constraintGreaterThanOrEqualToConstant_(size as _)
+ .setActive_(runtime::YES);
+ }
+ }
+ }
+
+ if !self.ignore_horizontal {
+ unsafe {
+ let la = view.leadingAnchor();
+ let ta = view.trailingAnchor();
+ let pla = lmg.leadingAnchor();
+ let pta = lmg.trailingAnchor();
+ match style.horizontal_alignment {
+ Alignment::Fill => {
+ constraint_equal(la, pla);
+ constraint_equal(ta, pta);
+ // Without the autoresizing mask set, Text within Scroll doesn't display
+ // properly (it shrinks to 0-width, likely due to some specific interaction
+ // of NSScrollView with autolayout).
+ view.setAutoresizingMask_(cocoa::NSViewWidthSizable);
+ }
+ Alignment::Start => {
+ constraint_equal(la, pla);
+ }
+ Alignment::Center => {
+ let ca = view.centerXAnchor();
+ let pca = lmg.centerXAnchor();
+ constraint_equal(ca, pca);
+ }
+ Alignment::End => {
+ constraint_equal(ta, pta);
+ }
+ }
+ }
+ }
+
+ if !matches!(style.vertical_alignment, Alignment::Fill) {
+ if let Some(size) = style.vertical_size_request {
+ unsafe {
+ view.heightAnchor()
+ .constraintGreaterThanOrEqualToConstant_(size as _)
+ .setActive_(runtime::YES);
+ }
+ }
+ }
+
+ if !self.ignore_vertical {
+ unsafe {
+ let ta = view.topAnchor();
+ let ba = view.bottomAnchor();
+ let pta = lmg.topAnchor();
+ let pba = lmg.bottomAnchor();
+ match style.vertical_alignment {
+ Alignment::Fill => {
+ constraint_equal(ta, pta);
+ constraint_equal(ba, pba);
+ // Set the autoresizing mask to be consistent with the horizontal settings
+ // (see the comment there as to why it's necessary).
+ view.setAutoresizingMask_(cocoa::NSViewHeightSizable);
+ }
+ Alignment::Start => {
+ constraint_equal(ta, pta);
+ }
+ Alignment::Center => {
+ let ca = view.centerYAnchor();
+ let pca = lmg.centerYAnchor();
+ constraint_equal(ca, pca);
+ }
+ Alignment::End => {
+ constraint_equal(ba, pba);
+ }
+ }
+ }
+ }
+
+ match &style.visible {
+ Property::Static(ref v) => {
+ unsafe { view.setHidden_((!v).into()) };
+ }
+ Property::Binding(b) => {
+ b.on_change(move |&visible| unsafe {
+ view.setHidden_((!visible).into());
+ });
+ unsafe { view.setHidden_((!*b.borrow()).into()) };
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::visible doesn't support ReadOnly")
+ }
+ }
+
+ if let Ok(control) = cocoa::NSControl::try_from(view) {
+ match &style.enabled {
+ Property::Static(e) => {
+ unsafe { control.setEnabled_((*e).into()) };
+ }
+ Property::Binding(b) => {
+ b.on_change(move |&enabled| unsafe {
+ control.setEnabled_(enabled.into());
+ });
+ unsafe { control.setEnabled_((*b.borrow()).into()) };
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::enabled doesn't support ReadOnly")
+ }
+ }
+ } else if let Ok(text) = cocoa::NSText::try_from(view) {
+ let normally_editable = unsafe { text.isEditable() } == runtime::YES;
+ let normally_selectable = unsafe { text.isSelectable() } == runtime::YES;
+ let set_enabled = move |enabled: bool| unsafe {
+ if !enabled {
+ let mut range = text.selectedRange();
+ range.length = 0;
+ text.setSelectedRange_(range);
+ }
+ text.setEditable_((enabled && normally_editable).into());
+ text.setSelectable_((enabled && normally_selectable).into());
+ text.setBackgroundColor_(if enabled {
+ cocoa::NSColor::textBackgroundColor()
+ } else {
+ cocoa::NSColor::windowBackgroundColor()
+ });
+ text.setTextColor_(if enabled {
+ cocoa::NSColor::textColor()
+ } else {
+ cocoa::NSColor::disabledControlTextColor()
+ });
+ };
+ match &style.enabled {
+ Property::Static(e) => set_enabled(*e),
+ Property::Binding(b) => {
+ b.on_change(move |&enabled| set_enabled(enabled));
+ set_enabled(*b.borrow());
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::enabled doesn't support ReadOnly")
+ }
+ }
+ }
+
+ unsafe { view.setNeedsDisplay_(runtime::YES) };
+
+ true
+ }
+}
+
+fn render_element(
+ element_type: model::ElementType,
+ style: &model::ElementStyle,
+ rtl: bool,
+) -> Option<cocoa::NSView> {
+ use model::ElementType::*;
+ Some(match element_type {
+ VBox(model::VBox { items, spacing }) => {
+ let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationVertical);
+ sv.setAlignment_(cocoa::NSLayoutAttributeLeading);
+ sv.setSpacing_(spacing as _);
+ if style.vertical_alignment != Alignment::Fill {
+ // Make sure the vbox stays as small as its content.
+ sv.setHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationVertical,
+ );
+ }
+ }
+ let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| {
+ let gravity: cocoa::NSInteger = match style.vertical_alignment {
+ Alignment::Start | Alignment::Fill => 1,
+ Alignment::Center => 2,
+ Alignment::End => 3,
+ };
+ let parent: cocoa::NSStackView = parent.try_into().unwrap();
+ unsafe { parent.addView_inGravity_(child, gravity) };
+ })
+ .ignore_vertical(true);
+ for item in items {
+ renderer.render(item);
+ }
+ sv.into()
+ }
+ HBox(model::HBox {
+ mut items,
+ spacing,
+ affirmative_order,
+ }) => {
+ if affirmative_order {
+ items.reverse();
+ }
+ let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationHorizontal);
+ sv.setAlignment_(cocoa::NSLayoutAttributeTop);
+ sv.setSpacing_(spacing as _);
+ if style.horizontal_alignment != Alignment::Fill {
+ // Make sure the hbox stays as small as its content.
+ sv.setHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationHorizontal,
+ );
+ }
+ }
+ let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| {
+ let gravity: cocoa::NSInteger = match style.horizontal_alignment {
+ Alignment::Start | Alignment::Fill => 1,
+ Alignment::Center => 2,
+ Alignment::End => 3,
+ };
+ let parent: cocoa::NSStackView = parent.try_into().unwrap();
+ unsafe { parent.addView_inGravity_(child, gravity) };
+ })
+ .ignore_horizontal(true);
+ for item in items {
+ renderer.render(item);
+ }
+ sv.into()
+ }
+ Button(mut b) => {
+ if let Some(Label(model::Label {
+ text: Property::Static(text),
+ ..
+ })) = b.content.take().map(|e| e.element_type)
+ {
+ let button = self::Button { element: b }.with_title(text.as_str());
+ button.into()
+ } else {
+ return None;
+ }
+ }
+ Checkbox(cb) => {
+ let button = self::Checkbox { element: cb }.into_button();
+ button.into()
+ }
+ Label(model::Label { text, bold }) => {
+ let tf = cocoa::NSTextField(unsafe {
+ cocoa::NSTextField::wrappingLabelWithString_(nsstring(""))
+ });
+ unsafe { tf.setSelectable_(runtime::NO) };
+ if bold {
+ unsafe { tf.setFont_(cocoa::NSFont::boldSystemFontOfSize_(0.0)) };
+ }
+ match text {
+ Property::Static(text) => {
+ unsafe { tf.setStringValue_(nsstring(text.as_str())) };
+ }
+ Property::Binding(b) => {
+ unsafe { tf.setStringValue_(nsstring(b.borrow().as_str())) };
+ b.on_change(move |s| unsafe { tf.setStringValue_(nsstring(s)) });
+ }
+ Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"),
+ }
+ tf.into()
+ }
+ Progress(model::Progress { amount }) => {
+ fn update(progress: cocoa::NSProgressIndicator, value: Option<f32>) {
+ unsafe {
+ match value {
+ None => {
+ progress.setIndeterminate_(runtime::YES);
+ progress.startAnimation_(progress.0);
+ }
+ Some(v) => {
+ progress.setDoubleValue_(v as f64);
+ progress.setIndeterminate_(runtime::NO);
+ }
+ }
+ }
+ }
+
+ let progress = unsafe { StrongRef::new(cocoa::NSProgressIndicator::alloc()) };
+ unsafe {
+ progress.init();
+ progress.setMinValue_(0.0);
+ progress.setMaxValue_(1.0);
+ }
+ match amount {
+ Property::Static(v) => update(*progress, v),
+ Property::Binding(s) => {
+ update(*progress, *s.borrow());
+ let weak = progress.weak();
+ s.on_change(move |v| {
+ if let Some(r) = weak.lock() {
+ update(*r, *v);
+ }
+ });
+ }
+ Property::ReadOnly(_) => (),
+ }
+ progress.autorelease().into()
+ }
+ Scroll(model::Scroll { content }) => {
+ let sv = unsafe { StrongRef::new(cocoa::NSScrollView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setHasVerticalScroller_(runtime::YES);
+ }
+ if let Some(content) = content {
+ ViewRenderer::new_with_selector(rtl, sv, sel!(setDocumentView:))
+ .ignore_vertical(true)
+ .render(*content);
+ }
+ sv.into()
+ }
+ TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let tv: StrongRef<cocoa::NSTextView> = TextView.into_object().cast();
+ unsafe {
+ tv.setEditable_(editable.into());
+
+ cocoa::NSTextView_NSSharing::setAllowsUndo_(&*tv, runtime::YES);
+ tv.setVerticallyResizable_(runtime::YES);
+ if rtl {
+ let ps = StrongRef::new(cocoa::NSMutableParagraphStyle::alloc());
+ ps.init();
+ ps.setAlignment_(cocoa::NSTextAlignmentRight);
+ // We don't `use cocoa::NSTextView_NSSharing` because it has some methods which
+ // conflict with others that make it inconvenient.
+ cocoa::NSTextView_NSSharing::setDefaultParagraphStyle_(&*tv, (*ps).into());
+ }
+ {
+ let container = tv.textContainer();
+ container.setSize_(cocoa::NSSize {
+ width: f64::MAX,
+ height: f64::MAX,
+ });
+ container.setWidthTracksTextView_(runtime::YES);
+ }
+ if let Some(placeholder) = placeholder {
+ // It's unclear why dictionaryWithObject_forKey_ takes `u64` rather than `id`
+ // arguments.
+ let attrs = cocoa::NSDictionary(
+ <cocoa::NSDictionary as NSDictionary_NSDictionaryCreation<
+ cocoa::NSAttributedStringKey,
+ cocoa::id,
+ >>::dictionaryWithObject_forKey_(
+ cocoa::NSColor::placeholderTextColor().0 as u64,
+ cocoa::NSForegroundColorAttributeName.0 as u64,
+ ),
+ );
+ let string = StrongRef::new(cocoa::NSAttributedString(
+ cocoa::NSAttributedString::alloc()
+ .initWithString_attributes_(nsstring(placeholder.as_str()), attrs),
+ ));
+ // XXX: `setPlaceholderAttributedString` is undocumented (discovered at
+ // https://stackoverflow.com/a/43028577 and works identically to NSTextField),
+ // though hopefully it will be exposed in a public API some day.
+ tv.performSelector_withObject_(sel!(setPlaceholderAttributedString:), string.0);
+ }
+ }
+ match content {
+ Property::Static(s) => unsafe { tv.setString_(nsstring(s.as_str())) },
+ Property::ReadOnly(od) => {
+ let weak = tv.weak();
+ od.register(move |s| {
+ if let Some(tv) = weak.lock() {
+ *s = read_nsstring(unsafe { tv.string() });
+ }
+ });
+ }
+ Property::Binding(b) => {
+ let weak = tv.weak();
+ b.on_change(move |s| {
+ if let Some(tv) = weak.lock() {
+ unsafe { tv.setString_(nsstring(s.as_str())) };
+ }
+ });
+ unsafe { tv.setString_(nsstring(b.borrow().as_str())) };
+ }
+ }
+ tv.autorelease().into()
+ }
+ })
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/objc.rs b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs
new file mode 100644
index 0000000000..d4f3f1c419
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs
@@ -0,0 +1,242 @@
+/* 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/. */
+
+//! Objective-C bindings and helpers.
+
+// Forward all exports from the `objc` crate.
+pub use objc::*;
+
+/// An objc class instance which contains rust data `T`.
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+pub struct Objc<T> {
+ pub instance: cocoa::id,
+ _phantom: std::marker::PhantomData<*mut T>,
+}
+
+impl<T> Objc<T> {
+ pub fn new(instance: cocoa::id) -> Self {
+ Objc {
+ instance,
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ pub fn data(&self) -> &T {
+ let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T;
+ unsafe { &*data }
+ }
+
+ pub fn data_mut(&mut self) -> &mut T {
+ let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T;
+ unsafe { &mut *data }
+ }
+}
+
+impl<T> std::ops::Deref for Objc<T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ self.data()
+ }
+}
+
+impl<T> std::ops::DerefMut for Objc<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.data_mut()
+ }
+}
+
+unsafe impl<T> Encode for Objc<T> {
+ fn encode() -> Encoding {
+ cocoa::id::encode()
+ }
+}
+
+/// Wrapper to provide `Encode` for bindgen-generated types (bindgen should do this in the future).
+#[repr(transparent)]
+pub struct Ptr<T>(pub T);
+
+unsafe impl<T> Encode for Ptr<T> {
+ fn encode() -> Encoding {
+ cocoa::id::encode()
+ }
+}
+
+/// A strong objective-c reference to `T`.
+#[repr(transparent)]
+pub struct StrongRef<T> {
+ ptr: rc::StrongPtr,
+ _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> Clone for StrongRef<T> {
+ fn clone(&self) -> Self {
+ StrongRef {
+ ptr: self.ptr.clone(),
+ _phantom: self._phantom,
+ }
+ }
+}
+
+impl<T> std::ops::Deref for StrongRef<T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ let obj: &cocoa::id = &*self.ptr;
+ unsafe { std::mem::transmute(obj) }
+ }
+}
+
+impl<T> StrongRef<T> {
+ /// Assume the given pointer-wrapper is an already-retained strong reference.
+ ///
+ /// # Safety
+ /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id.
+ pub unsafe fn new(v: T) -> Self {
+ std::mem::transmute_copy(&v)
+ }
+
+ /// Retain the given pointer-wrapper.
+ ///
+ /// # Safety
+ /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id.
+ #[allow(dead_code)]
+ pub unsafe fn retain(v: T) -> Self {
+ let obj: cocoa::id = std::mem::transmute_copy(&v);
+ StrongRef {
+ ptr: rc::StrongPtr::retain(obj),
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ pub fn autorelease(self) -> T {
+ let obj = self.ptr.autorelease();
+ unsafe { std::mem::transmute_copy(&obj) }
+ }
+
+ pub fn weak(&self) -> WeakRef<T> {
+ WeakRef {
+ ptr: self.ptr.weak(),
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ /// Unwrap the StrongRef value without affecting reference counts.
+ ///
+ /// This is the opposite of `new`.
+ #[allow(dead_code)]
+ pub fn unwrap(self: Self) -> T {
+ let v = unsafe { std::mem::transmute_copy(&self) };
+ std::mem::forget(self);
+ v
+ }
+
+ /// Cast to a base class.
+ ///
+ /// Bindgen pointer-wrappers have trival `From<Derived> for Base` implementations.
+ pub fn cast<U: From<T>>(self) -> StrongRef<U> {
+ StrongRef {
+ ptr: self.ptr,
+ _phantom: std::marker::PhantomData,
+ }
+ }
+}
+
+/// A weak objective-c reference to `T`.
+#[derive(Clone)]
+#[repr(transparent)]
+pub struct WeakRef<T> {
+ ptr: rc::WeakPtr,
+ _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> WeakRef<T> {
+ pub fn lock(&self) -> Option<StrongRef<T>> {
+ let ptr = self.ptr.load();
+ if ptr.is_null() {
+ None
+ } else {
+ Some(StrongRef {
+ ptr,
+ _phantom: std::marker::PhantomData,
+ })
+ }
+ }
+}
+
+/// A macro for creating an objc class.
+///
+/// Classes _must_ be registered before use (`Objc<T>::register()`).
+///
+/// Example:
+/// ```
+/// struct Foo(u8);
+///
+/// objc_class! {
+/// impl Foo: NSObject {
+/// #[sel(mySelector:)]
+/// fn my_selector(&mut self, arg: u8) -> u8 {
+/// self.0 + arg
+/// }
+/// }
+/// }
+///
+/// fn make_foo() -> StrongRef<Objc<Foo>> {
+/// Foo(42).into_object()
+/// }
+/// ```
+///
+/// Call `T::into_object()` to create the objective-c class instance.
+macro_rules! objc_class {
+ ( impl $name:ident : $base:ident $(<$($protocol:ident),+>)? {
+ $(
+ #[sel($($sel:tt)+)]
+ fn $mname:ident (&mut $self:ident $(, $argname:ident : $argtype:ty )*) $(-> $rettype:ty)? $body:block
+ )*
+ }) => {
+ impl Objc<$name> {
+ pub fn register() {
+ let mut decl = declare::ClassDecl::new(concat!("CR", stringify!($name)), class!($base)).expect(concat!("failed to declare ", stringify!($name), " class"));
+ $($(decl.add_protocol(runtime::Protocol::get(stringify!($protocol)).expect(concat!("failed to find ",stringify!($protocol)," protocol")));)+)?
+ decl.add_ivar::<usize>("rust_self");
+ $({
+ extern fn method_impl(obj: &mut runtime::Object, _: runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)? {
+ Objc::<$name>::new(obj).$mname($($argname),*)
+ }
+ unsafe {
+ decl.add_method(sel!($($sel)+), method_impl as extern fn(&mut runtime::Object, runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)?);
+ }
+ })*
+ {
+ extern fn dealloc_impl(obj: &runtime::Object, _: runtime::Sel) {
+ drop(unsafe { Box::from_raw(*obj.get_ivar::<usize>("rust_self") as *mut $name) });
+ unsafe {
+ let _: () = msg_send![super(obj, class!(NSObject)), dealloc];
+ }
+ }
+ unsafe {
+ decl.add_method(sel!(dealloc), dealloc_impl as extern fn(&runtime::Object, runtime::Sel));
+ }
+ }
+ decl.register();
+ }
+
+ pub fn class() -> &'static runtime::Class {
+ runtime::Class::get(concat!("CR", stringify!($name))).expect("class not registered")
+ }
+
+ $(fn $mname (&mut $self $(, $argname : $argtype )*) $(-> $rettype)? $body)*
+ }
+
+ impl $name {
+ pub fn into_object(self) -> StrongRef<Objc<$name>> {
+ let obj: *mut runtime::Object = unsafe { msg_send![Objc::<Self>::class(), alloc] };
+ unsafe { (*obj).set_ivar("rust_self", Box::into_raw(Box::new(self)) as usize) };
+ let obj: *mut runtime::Object = unsafe { msg_send![obj, init] };
+ unsafe { StrongRef::new(Objc::new(obj)) }
+ }
+ }
+ }
+}
+
+pub(crate) use objc_class;
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/plist.rs b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs
new file mode 100644
index 0000000000..a5bbe0aa0a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs
@@ -0,0 +1,44 @@
+/* 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/. */
+
+//! The embedded Info.plist file.
+
+const DATA: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleDisplayName</key>
+ <string>Crash Reporter</string>
+ <key>CFBundleExecutable</key>
+ <string>crashreporter</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.mozilla.crashreporter</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>Crash Reporter</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSHasLocalizedDisplayName</key>
+ <true/>
+ <key>NSRequiresAquaSystemAppearance</key>
+ <false/>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>"#;
+
+const N: usize = DATA.len();
+
+const PTR: *const [u8; N] = DATA.as_ptr() as *const [u8; N];
+
+#[used]
+#[link_section = "__TEXT,__info_plist"]
+// # Safety
+// The array pointer is created from `DATA` (a slice pointer) with `DATA.len()` as the length.
+static PLIST: [u8; N] = unsafe { *PTR };
diff --git a/toolkit/crashreporter/client/app/src/ui/mod.rs b/toolkit/crashreporter/client/app/src/ui/mod.rs
new file mode 100644
index 0000000000..8464b6a9b3
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/mod.rs
@@ -0,0 +1,295 @@
+/* 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/. */
+
+//! The UI model, UI implementations, and functions using them.
+//!
+//! UIs must implement:
+//! * a `fn run_loop(&self, app: model::Application)` method which should display the UI and block while
+//! handling events until the application terminates,
+//! * a `fn invoke(&self, f: model::InvokeFn)` method which invokes the given function
+//! asynchronously (without blocking) on the UI loop thread.
+
+use crate::std::{rc::Rc, sync::Arc};
+use crate::{
+ async_task::AsyncTask, config::Config, data, logic::ReportCrash, settings::Settings, std,
+ thread_bound::ThreadBound,
+};
+use model::{ui, Application};
+use ui_impl::UI;
+
+mod model;
+
+#[cfg(all(not(test), any(target_os = "linux", target_os = "windows")))]
+mod icon {
+ // Must be DWORD-aligned for Win32 CreateIconFromResource.
+ #[repr(align(4))]
+ struct Aligned<Bytes: ?Sized>(Bytes);
+ static PNG_DATA_ALIGNMENT: &'static Aligned<[u8]> =
+ &Aligned(*include_bytes!("crashreporter.png"));
+ pub static PNG_DATA: &'static [u8] = &PNG_DATA_ALIGNMENT.0;
+}
+
+#[cfg(test)]
+pub mod test {
+ pub mod model {
+ pub use crate::ui::model::*;
+ }
+}
+
+cfg_if::cfg_if! {
+ if #[cfg(test)] {
+ #[path = "test.rs"]
+ pub mod ui_impl;
+ } else if #[cfg(target_os = "linux")] {
+ #[path = "gtk.rs"]
+ mod ui_impl;
+ } else if #[cfg(target_os = "windows")] {
+ #[path = "windows/mod.rs"]
+ mod ui_impl;
+ } else if #[cfg(target_os = "macos")] {
+ #[path = "macos/mod.rs"]
+ mod ui_impl;
+ } else {
+ mod ui_impl {
+ #[derive(Default)]
+ pub struct UI;
+
+ impl UI {
+ pub fn run_loop(&self, _app: super::model::Application) {
+ unimplemented!();
+ }
+
+ pub fn invoke(&self, _f: super::model::InvokeFn) {
+ unimplemented!();
+ }
+ }
+ }
+ }
+}
+
+/// Display an error dialog with the given message.
+#[cfg_attr(mock, allow(unused))]
+pub fn error_dialog<M: std::fmt::Display>(config: &Config, message: M) {
+ let close = data::Event::default();
+ // Config may not have localized strings
+ let string_or = |name, fallback: &str| {
+ if config.strings.is_none() {
+ fallback.into()
+ } else {
+ config.string(name)
+ }
+ };
+
+ let details = if config.strings.is_none() {
+ format!("Details: {}", message)
+ } else {
+ config
+ .build_string("crashreporter-error-details")
+ .arg("details", message.to_string())
+ .get()
+ };
+
+ let window = ui! {
+ Window title(string_or("crashreporter-branded-title", "Firefox Crash Reporter")) hsize(600) vsize(400)
+ close_when(&close) halign(Alignment::Fill) valign(Alignment::Fill) {
+ VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
+ Label text(string_or(
+ "crashreporter-error",
+ "The application had a problem and crashed. \
+ Unfortunately, the crash reporter is unable to submit a report for the crash."
+ )),
+ Label text(details),
+ Button["close"] halign(Alignment::End) on_click(move || close.fire(&())) {
+ Label text(string_or("crashreporter-button-close", "Close"))
+ }
+ }
+ }
+ };
+
+ UI::default().run_loop(Application {
+ windows: vec![window],
+ rtl: config.is_rtl(),
+ });
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub enum SubmitState {
+ #[default]
+ Initial,
+ InProgress,
+ Success,
+ Failure,
+}
+
+/// The UI for the main crash reporter windows.
+pub struct ReportCrashUI {
+ state: Arc<ThreadBound<ReportCrashUIState>>,
+ ui: Arc<UI>,
+ config: Arc<Config>,
+ logic: Rc<AsyncTask<ReportCrash>>,
+}
+
+/// The state of the creash UI.
+pub struct ReportCrashUIState {
+ pub send_report: data::Synchronized<bool>,
+ pub include_address: data::Synchronized<bool>,
+ pub show_details: data::Synchronized<bool>,
+ pub details: data::Synchronized<String>,
+ pub comment: data::OnDemand<String>,
+ pub submit_state: data::Synchronized<SubmitState>,
+ pub close_window: data::Event<()>,
+}
+
+impl ReportCrashUI {
+ pub fn new(
+ initial_settings: &Settings,
+ config: Arc<Config>,
+ logic: AsyncTask<ReportCrash>,
+ ) -> Self {
+ let send_report = data::Synchronized::new(initial_settings.submit_report);
+ let include_address = data::Synchronized::new(initial_settings.include_url);
+
+ ReportCrashUI {
+ state: Arc::new(ThreadBound::new(ReportCrashUIState {
+ send_report,
+ include_address,
+ show_details: Default::default(),
+ details: Default::default(),
+ comment: Default::default(),
+ submit_state: Default::default(),
+ close_window: Default::default(),
+ })),
+ ui: Default::default(),
+ config,
+ logic: Rc::new(logic),
+ }
+ }
+
+ pub fn async_task(&self) -> AsyncTask<ReportCrashUIState> {
+ let state = self.state.clone();
+ let ui = Arc::downgrade(&self.ui);
+ AsyncTask::new(move |f| {
+ let Some(ui) = ui.upgrade() else { return };
+ ui.invoke(Box::new(cc! { (state) move || {
+ f(state.borrow());
+ }}));
+ })
+ }
+
+ pub fn run(&self) {
+ let ReportCrashUI {
+ state,
+ ui,
+ config,
+ logic,
+ } = self;
+ let ReportCrashUIState {
+ send_report,
+ include_address,
+ show_details,
+ details,
+ comment,
+ submit_state,
+ close_window,
+ } = state.borrow();
+
+ send_report.on_change(cc! { (logic) move |v| {
+ let v = *v;
+ logic.push(move |s| s.settings.borrow_mut().submit_report = v);
+ }});
+ include_address.on_change(cc! { (logic) move |v| {
+ let v = *v;
+ logic.push(move |s| s.settings.borrow_mut().include_url = v);
+ }});
+
+ let input_enabled = submit_state.mapped(|s| s == &SubmitState::Initial);
+ let send_report_and_input_enabled =
+ data::Synchronized::join(send_report, &input_enabled, |s, e| *s && *e);
+
+ let submit_status_text = submit_state.mapped(cc! { (config) move |s| {
+ config.string(match s {
+ SubmitState::Initial => "crashreporter-submit-status",
+ SubmitState::InProgress => "crashreporter-submit-in-progress",
+ SubmitState::Success => "crashreporter-submit-success",
+ SubmitState::Failure => "crashreporter-submit-failure",
+ })
+ }});
+
+ let progress_visible = submit_state.mapped(|s| s == &SubmitState::InProgress);
+
+ let details_window = ui! {
+ Window["crash-details-window"] title(config.string("crashreporter-view-report-title"))
+ visible(show_details) modal(true) hsize(600) vsize(400)
+ halign(Alignment::Fill) valign(Alignment::Fill)
+ {
+ VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
+ Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
+ TextBox["details-text"] content(details) halign(Alignment::Fill) valign(Alignment::Fill)
+ },
+ Button["close-details"] halign(Alignment::End) on_click(cc! { (show_details) move || *show_details.borrow_mut() = false }) {
+ Label text(config.string("crashreporter-button-ok"))
+ }
+ }
+ }
+ };
+
+ let main_window = ui! {
+ Window title(config.string("crashreporter-branded-title")) hsize(600) vsize(400)
+ halign(Alignment::Fill) valign(Alignment::Fill) close_when(close_window)
+ child_window(details_window)
+ {
+ VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
+ Label text(config.string("crashreporter-apology")) bold(true),
+ Label text(config.string("crashreporter-crashed-and-restore")),
+ Label text(config.string("crashreporter-plea")),
+ Checkbox["send"] checked(send_report) label(config.string("crashreporter-send-report"))
+ enabled(&input_enabled),
+ VBox margin_start(20) spacing(5) halign(Alignment::Fill) valign(Alignment::Fill) {
+ Button["details"] enabled(&send_report_and_input_enabled) on_click(cc! { (config, details, show_details, logic) move || {
+ // Immediately display the window to feel responsive, even if forming
+ // the details string takes a little while (it really shouldn't
+ // though).
+ *details.borrow_mut() = config.string("crashreporter-loading-details");
+ logic.push(|s| s.update_details());
+ *show_details.borrow_mut() = true;
+ }})
+ {
+ Label text(config.string("crashreporter-button-details"))
+ },
+ Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
+ TextBox["comment"] placeholder(config.string("crashreporter-comment-prompt"))
+ content(comment)
+ editable(true)
+ enabled(&send_report_and_input_enabled)
+ halign(Alignment::Fill) valign(Alignment::Fill)
+ },
+ Checkbox["include-url"] checked(include_address)
+ label(config.string("crashreporter-include-url")) enabled(&send_report_and_input_enabled),
+ Label text(&submit_status_text) margin_top(20),
+ Progress halign(Alignment::Fill) visible(&progress_visible),
+ },
+ HBox valign(Alignment::End) halign(Alignment::End) spacing(10) affirmative_order(true)
+ {
+ Button["restart"] visible(config.restart_command.is_some())
+ on_click(cc! { (logic) move || logic.push(|s| s.restart()) })
+ enabled(&input_enabled) hsize(160)
+ {
+ Label text(config.string("crashreporter-button-restart"))
+ },
+ Button["quit"] on_click(cc! { (logic) move || logic.push(|s| s.quit()) })
+ enabled(&input_enabled) hsize(160)
+ {
+ Label text(config.string("crashreporter-button-quit"))
+ }
+ }
+ }
+ }
+ };
+
+ ui.run_loop(Application {
+ windows: vec![main_window],
+ rtl: config.is_rtl(),
+ });
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/button.rs b/toolkit/crashreporter/client/app/src/ui/model/button.rs
new file mode 100644
index 0000000000..d522fad6fc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/button.rs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+use crate::data::Event;
+
+/// A clickable button.
+#[derive(Default, Debug)]
+pub struct Button {
+ pub content: Option<Box<Element>>,
+ pub click: Event<()>,
+}
+
+impl ElementBuilder<Button> {
+ pub fn on_click<F>(&mut self, f: F)
+ where
+ F: Fn() + 'static,
+ {
+ self.element_type.click.subscribe(move |_| f());
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs
new file mode 100644
index 0000000000..8923e33558
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A checkbox (with optional label).
+#[derive(Default, Debug)]
+pub struct Checkbox {
+ pub checked: Property<bool>,
+ pub label: Option<String>,
+}
+
+impl super::ElementBuilder<Checkbox> {
+ pub fn checked(&mut self, value: impl Into<Property<bool>>) {
+ self.element_type.checked = value.into();
+ }
+
+ pub fn label<S: Into<String>>(&mut self, label: S) {
+ self.element_type.label = Some(label.into());
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/hbox.rs b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs
new file mode 100644
index 0000000000..b6c0e27e8c
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs
@@ -0,0 +1,34 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A box which lays out contents horizontally.
+#[derive(Default, Debug)]
+pub struct HBox {
+ pub items: Vec<Element>,
+ pub spacing: u32,
+ pub affirmative_order: bool,
+}
+
+impl ElementBuilder<HBox> {
+ pub fn spacing(&mut self, value: u32) {
+ self.element_type.spacing = value;
+ }
+
+ /// Whether children are in affirmative order (and should be reordered based on platform
+ /// conventions).
+ ///
+ /// The children passed to `add_child` should be in most-affirmative to least-affirmative order
+ /// (e.g., "OK" then "Cancel" buttons).
+ ///
+ /// This is mainly useful for dialog buttons.
+ pub fn affirmative_order(&mut self, value: bool) {
+ self.element_type.affirmative_order = value;
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ self.element_type.items.push(child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/label.rs b/toolkit/crashreporter/client/app/src/ui/model/label.rs
new file mode 100644
index 0000000000..096ce022e3
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/label.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A text label.
+#[derive(Debug, Default)]
+pub struct Label {
+ pub text: Property<String>,
+ pub bold: bool,
+}
+
+impl super::ElementBuilder<Label> {
+ pub fn text(&mut self, s: impl Into<Property<String>>) {
+ self.element_type.text = s.into();
+ }
+
+ pub fn bold(&mut self, value: bool) {
+ self.element_type.bold = value;
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/mod.rs b/toolkit/crashreporter/client/app/src/ui/model/mod.rs
new file mode 100644
index 0000000000..5ea2ddc59a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/mod.rs
@@ -0,0 +1,344 @@
+/* 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/. */
+
+//! The UI model.
+//!
+//! Model elements should generally be declared as types with all fields `pub` (to be accessed by
+//! UI implementations), though accessor methods are acceptable if needed. An
+//! `ElementBuilder<TYPE>` impl should be provided to create methods that will be used in the
+//! [`ui!`] macro. The model types are accessible when being _consumed_ by a UI implementation,
+//! whereas the `ElementBuilder` types are accessible when the model is being _created_.
+//!
+//! All elements should be listed in the `element_types!` macro in this file (note that [`Window`],
+//! while an element, isn't listed here as it cannot be a child element). This populates the
+//! `ElementType` enum and generates `From<Element>` for `ElementType`, and `TryFrom<ElementType>`
+//! for the element (as well as reference `TryFrom`).
+//!
+//! The model is written to accommodate layout and text direction differences (e.g. for RTL
+//! languages), and UI implementations are expected to account for this correctly.
+
+use crate::data::Property;
+pub use button::Button;
+pub use checkbox::Checkbox;
+pub use hbox::HBox;
+pub use label::Label;
+pub use progress::Progress;
+pub use scroll::Scroll;
+pub use textbox::TextBox;
+pub use vbox::VBox;
+pub use window::Window;
+
+mod button;
+mod checkbox;
+mod hbox;
+mod label;
+mod progress;
+mod scroll;
+mod textbox;
+mod vbox;
+mod window;
+
+/// A GUI element, including general style attributes and a more specific type.
+///
+/// `From<ElementBuilder<...>>` is implemented for all elements listed in `element_types!`.
+#[derive(Debug)]
+pub struct Element {
+ pub style: ElementStyle,
+ pub element_type: ElementType,
+}
+
+// This macro creates the `ElementType` enum and corresponding `From<ElementBuilder>` impls for
+// Element. The `ElementType` discriminants match the element type names.
+macro_rules! element_types {
+ ( $($name:ident),* ) => {
+ /// A type of GUI element.
+ #[derive(Debug)]
+ pub enum ElementType {
+ $($name($name)),*
+ }
+
+ $(
+ impl From<$name> for ElementType {
+ fn from(e: $name) -> ElementType {
+ ElementType::$name(e)
+ }
+ }
+
+ impl TryFrom<ElementType> for $name {
+ type Error = &'static str;
+
+ fn try_from(et: ElementType) -> Result<Self, Self::Error> {
+ if let ElementType::$name(v) = et {
+ Ok(v)
+ } else {
+ Err(concat!("ElementType was not ", stringify!($name)))
+ }
+ }
+ }
+
+ impl<'a> TryFrom<&'a ElementType> for &'a $name {
+ type Error = &'static str;
+
+ fn try_from(et: &'a ElementType) -> Result<Self, Self::Error> {
+ if let ElementType::$name(v) = et {
+ Ok(v)
+ } else {
+ Err(concat!("ElementType was not ", stringify!($name)))
+ }
+ }
+ }
+
+ impl From<ElementBuilder<$name>> for Element {
+ fn from(b: ElementBuilder<$name>) -> Self {
+ Element {
+ style: b.style,
+ element_type: b.element_type.into(),
+ }
+ }
+ }
+ )*
+ }
+}
+element_types! {
+ Button, Checkbox, HBox, Label, Progress, Scroll, TextBox, VBox
+}
+
+/// Common element style values.
+#[derive(Debug)]
+pub struct ElementStyle {
+ pub horizontal_alignment: Alignment,
+ pub vertical_alignment: Alignment,
+ pub horizontal_size_request: Option<u32>,
+ pub vertical_size_request: Option<u32>,
+ pub margin: Margin,
+ pub visible: Property<bool>,
+ pub enabled: Property<bool>,
+ #[cfg(test)]
+ pub id: Option<String>,
+}
+
+impl Default for ElementStyle {
+ fn default() -> Self {
+ ElementStyle {
+ horizontal_alignment: Default::default(),
+ vertical_alignment: Default::default(),
+ horizontal_size_request: Default::default(),
+ vertical_size_request: Default::default(),
+ margin: Default::default(),
+ visible: true.into(),
+ enabled: true.into(),
+ #[cfg(test)]
+ id: Default::default(),
+ }
+ }
+}
+
+/// A builder for `Element`s.
+///
+/// Each element should add an `impl ElementBuilder<TYPE>` to add methods to their builder.
+#[derive(Debug, Default)]
+pub struct ElementBuilder<T> {
+ pub style: ElementStyle,
+ pub element_type: T,
+}
+
+impl<T> ElementBuilder<T> {
+ /// Set horizontal alignment.
+ pub fn halign(&mut self, alignment: Alignment) {
+ self.style.horizontal_alignment = alignment;
+ }
+
+ /// Set vertical alignment.
+ pub fn valign(&mut self, alignment: Alignment) {
+ self.style.vertical_alignment = alignment;
+ }
+
+ /// Set the horizontal size request.
+ pub fn hsize(&mut self, value: u32) {
+ assert!(value <= i32::MAX as u32);
+ self.style.horizontal_size_request = Some(value);
+ }
+
+ /// Set the vertical size request.
+ pub fn vsize(&mut self, value: u32) {
+ assert!(value <= i32::MAX as u32);
+ self.style.vertical_size_request = Some(value);
+ }
+
+ /// Set start margin.
+ pub fn margin_start(&mut self, amount: u32) {
+ self.style.margin.start = amount;
+ }
+
+ /// Set end margin.
+ pub fn margin_end(&mut self, amount: u32) {
+ self.style.margin.end = amount;
+ }
+
+ /// Set start and end margins.
+ pub fn margin_horizontal(&mut self, amount: u32) {
+ self.margin_start(amount);
+ self.margin_end(amount)
+ }
+
+ /// Set top margin.
+ pub fn margin_top(&mut self, amount: u32) {
+ self.style.margin.top = amount;
+ }
+
+ /// Set bottom margin.
+ pub fn margin_bottom(&mut self, amount: u32) {
+ self.style.margin.bottom = amount;
+ }
+
+ /// Set top and bottom margins.
+ pub fn margin_vertical(&mut self, amount: u32) {
+ self.margin_top(amount);
+ self.margin_bottom(amount)
+ }
+
+ /// Set all margins.
+ pub fn margin(&mut self, amount: u32) {
+ self.margin_horizontal(amount);
+ self.margin_vertical(amount)
+ }
+
+ /// Set visibility.
+ pub fn visible(&mut self, value: impl Into<Property<bool>>) {
+ self.style.visible = value.into();
+ }
+
+ /// Set whether an element is enabled.
+ ///
+ /// This generally should enable/disable interaction with an element.
+ pub fn enabled(&mut self, value: impl Into<Property<bool>>) {
+ self.style.enabled = value.into();
+ }
+
+ /// Set the element identifier.
+ #[cfg(test)]
+ pub fn id(&mut self, value: impl Into<String>) {
+ self.style.id = Some(value.into());
+ }
+
+ /// Set the element identifier (stub).
+ #[cfg(not(test))]
+ pub fn id(&mut self, _value: impl Into<String>) {}
+
+ fn single_child(slot: &mut Option<Box<Element>>, child: Element) {
+ if slot.replace(Box::new(child)).is_some() {
+ panic!("{} can only have one child", std::any::type_name::<T>());
+ }
+ }
+}
+
+/// A typed [`Element`].
+///
+/// This is useful for the [`ui!`] macro when a method should accept a specific element type, since
+/// the macro always creates [`ElementBuilder<T>`](ElementBuilder) and ends with a `.into()` (and this implements
+/// `From<ElementBuilder<T>>`).
+#[derive(Debug, Default)]
+pub struct TypedElement<T> {
+ pub style: ElementStyle,
+ pub element_type: T,
+}
+
+impl<T> From<ElementBuilder<T>> for TypedElement<T> {
+ fn from(b: ElementBuilder<T>) -> Self {
+ TypedElement {
+ style: b.style,
+ element_type: b.element_type,
+ }
+ }
+}
+
+/// The alignment of an element in one direction.
+///
+/// Note that rather than `Left`/`Right`, this class has `Start`/`End` as it is meant to be
+/// layout-direction-aware.
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+#[allow(dead_code)]
+pub enum Alignment {
+ /// Align to the start of the direction.
+ #[default]
+ Start,
+ /// Align to the center of the direction.
+ Center,
+ /// Align to the end of the direction.
+ End,
+ /// Fill all available space.
+ Fill,
+}
+
+/// The margins of an element.
+///
+/// These are RTL-aware: for instance, `start` is the left margin in left-to-right languages and
+/// the right margin in right-to-left languages.
+#[derive(Default, Debug)]
+pub struct Margin {
+ pub start: u32,
+ pub end: u32,
+ pub top: u32,
+ pub bottom: u32,
+}
+
+/// A macro to allow a convenient syntax for creating elements.
+///
+/// The macro expects the following syntax:
+/// ```
+/// ElementTypeName some_method(arg1, arg2) other_method() {
+/// Child ...,
+/// Child2 ...
+/// }
+/// ```
+///
+/// The type is wrapped in an `ElementBuilder`, and methods are called on this builder with a
+/// mutable reference. This means that element types must implement Default and must implement
+/// builder methods on `ElementBuilder<ElementTypeName>`. The children block is optional, and calls
+/// `add_child(child: Element)` for each provided child (so implement this method if desired).
+///
+/// For testing, a string identifier can be set on any element with a `["my_identifier"]` following
+/// the element type.
+macro_rules! ui {
+ ( $el:ident
+ $([ $id:literal ])?
+ $( $method:ident $methodargs:tt )*
+ $({ $($contents:tt)* })?
+ ) => {
+ {
+ #[allow(unused_imports)]
+ use $crate::ui::model::*;
+ let mut el: ElementBuilder<$el> = Default::default();
+ $( el.id($id); )?
+ $( el.$method $methodargs ; )*
+ $( ui! { @children (el) $($contents)* } )?
+ el.into()
+ }
+ };
+ ( @children ($parent:expr) ) => {};
+ ( @children ($parent:expr)
+ $el:ident
+ $([ $id:literal ])?
+ $( $method:ident $methodargs:tt )*
+ $({ $($contents:tt)* })?
+ $(, $($rest:tt)* )?
+ ) => {
+ $parent.add_child(ui!( $el $([$id])? $( $method $methodargs )* $({ $($contents)* })? ));
+ $(ui!( @children ($parent) $($rest)* ))?
+ };
+}
+
+pub(crate) use ui;
+
+/// An application, defined as a set of windows.
+///
+/// When all windows are closed, the application is considered complete (and loops should exit).
+pub struct Application {
+ pub windows: Vec<TypedElement<Window>>,
+ /// Whether the text direction should be right-to-left.
+ pub rtl: bool,
+}
+
+/// A function to be invoked in the UI loop.
+pub type InvokeFn = Box<dyn FnOnce() + Send + 'static>;
diff --git a/toolkit/crashreporter/client/app/src/ui/model/progress.rs b/toolkit/crashreporter/client/app/src/ui/model/progress.rs
new file mode 100644
index 0000000000..f3e4e4bf77
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/progress.rs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A progress indicator.
+#[derive(Debug, Default)]
+pub struct Progress {
+ /// Progress between 0 and 1, or None if indeterminate.
+ pub amount: Property<Option<f32>>,
+}
+
+impl super::ElementBuilder<Progress> {
+ #[allow(dead_code)]
+ pub fn amount(&mut self, value: impl Into<Property<Option<f32>>>) {
+ self.element_type.amount = value.into();
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/scroll.rs b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs
new file mode 100644
index 0000000000..47efa4a81e
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs
@@ -0,0 +1,17 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A scrollable region.
+#[derive(Debug, Default)]
+pub struct Scroll {
+ pub content: Option<Box<Element>>,
+}
+
+impl ElementBuilder<Scroll> {
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/textbox.rs b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs
new file mode 100644
index 0000000000..08cd9ca1bc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs
@@ -0,0 +1,27 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A text box.
+#[derive(Debug, Default)]
+pub struct TextBox {
+ pub placeholder: Option<String>,
+ pub content: Property<String>,
+ pub editable: bool,
+}
+
+impl super::ElementBuilder<TextBox> {
+ pub fn placeholder(&mut self, text: impl Into<String>) {
+ self.element_type.placeholder = Some(text.into());
+ }
+
+ pub fn content(&mut self, value: impl Into<Property<String>>) {
+ self.element_type.content = value.into();
+ }
+
+ pub fn editable(&mut self, value: bool) {
+ self.element_type.editable = value;
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/vbox.rs b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs
new file mode 100644
index 0000000000..6f1b09b1e2
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A box which lays out contents vertically.
+#[derive(Debug, Default)]
+pub struct VBox {
+ pub items: Vec<Element>,
+ pub spacing: u32,
+}
+
+impl ElementBuilder<VBox> {
+ pub fn spacing(&mut self, value: u32) {
+ self.element_type.spacing = value;
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ self.element_type.items.push(child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/window.rs b/toolkit/crashreporter/client/app/src/ui/model/window.rs
new file mode 100644
index 0000000000..b56071ca19
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/window.rs
@@ -0,0 +1,46 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder, TypedElement};
+use crate::data::Event;
+
+/// A window.
+#[derive(Debug, Default)]
+pub struct Window {
+ pub title: String,
+ /// The window content is the first element.
+ pub content: Option<Box<Element>>,
+ /// Logical child windows.
+ pub children: Vec<TypedElement<Self>>,
+ pub modal: bool,
+ pub close: Option<Event<()>>,
+}
+
+impl ElementBuilder<Window> {
+ /// Set the window title.
+ pub fn title(&mut self, s: impl Into<String>) {
+ self.element_type.title = s.into();
+ }
+
+ /// Set whether the window is modal (blocking interaction with other windows when displayed).
+ pub fn modal(&mut self, value: bool) {
+ self.element_type.modal = value;
+ }
+
+ /// Register an event to close the window.
+ pub fn close_when(&mut self, event: &Event<()>) {
+ self.element_type.close = Some(event.clone());
+ }
+
+ /// Add a window as a logical child of this one.
+ ///
+ /// Logical children are always displayed above their parents.
+ pub fn child_window(&mut self, window: TypedElement<Window>) {
+ self.element_type.children.push(window);
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/test.rs b/toolkit/crashreporter/client/app/src/ui/test.rs
new file mode 100644
index 0000000000..db98c072da
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/test.rs
@@ -0,0 +1,270 @@
+/* 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/. */
+
+//! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic
+//! interaction.
+//!
+//! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can
+//! be used to access the element in this UI.
+//!
+//! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on
+//! the same thread as the UI.
+//!
+//! See how this UI is used in [`crate::test`].
+
+use super::model::{self, Application, Element};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::sync::{
+ atomic::{AtomicBool, AtomicU8, Ordering::Relaxed},
+ mpsc, Arc, Condvar, Mutex,
+};
+
+thread_local! {
+ static INTERACT: RefCell<Option<Arc<State>>> = Default::default();
+}
+
+/// A test UI which allows access to the UI elements.
+#[derive(Default)]
+pub struct UI {
+ interface: Mutex<Option<UIInterface>>,
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ let (tx, rx) = mpsc::channel();
+ let interface = UIInterface { work: tx };
+
+ let elements = id_elements(&app);
+ INTERACT.with(cc! { (interface) move |r| {
+ if let Some(state) = &*r.borrow() {
+ state.set_interface(interface);
+ }
+ }});
+ *self.interface.lock().unwrap() = Some(interface.clone());
+
+ // Close the UI when the root windows are closed.
+ // Use a bitfield rather than a count in case the `close` event is fired multiple times.
+ assert!(app.windows.len() <= 8);
+ let mut windows = 0u8;
+ for i in 0..app.windows.len() {
+ windows |= 1 << i;
+ }
+ let windows = Arc::new(AtomicU8::new(windows));
+ for (index, window) in app.windows.iter().enumerate() {
+ if let Some(c) = &window.element_type.close {
+ c.subscribe(cc! { (interface, windows) move |&()| {
+ let old = windows
+ .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
+ .unwrap();
+ if old == 1u8 << index {
+ interface.work.send(Command::Finish).unwrap();
+ }
+ }});
+ } else {
+ // No close event, so we must assume a closed state (and assume that _some_ window
+ // will have a close event registered so we don't drop the interface now).
+ windows
+ .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
+ .unwrap();
+ }
+ }
+
+ while let Ok(f) = rx.recv() {
+ match f {
+ Command::Invoke(f) => f(),
+ Command::Interact(f) => f(&elements),
+ Command::Finish => break,
+ }
+ }
+
+ *self.interface.lock().unwrap() = None;
+ INTERACT.with(|r| {
+ if let Some(state) = &*r.borrow() {
+ state.clear_interface();
+ }
+ });
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ let guard = self.interface.lock().unwrap();
+ if let Some(interface) = &*guard {
+ let _ = interface.work.send(Command::Invoke(f));
+ }
+ }
+}
+
+/// Test interaction hook.
+#[derive(Clone)]
+pub struct Interact {
+ state: Arc<State>,
+}
+
+impl Interact {
+ /// Create an interaction hook for the test UI.
+ ///
+ /// This should be done before running the UI, and must be done on the same thread that
+ /// later runs it.
+ pub fn hook() -> Self {
+ let v = Interact {
+ state: Default::default(),
+ };
+ {
+ let state = v.state.clone();
+ INTERACT.with(move |r| *r.borrow_mut() = Some(state));
+ }
+ v
+ }
+
+ /// Wait for the render thread to be ready for interaction.
+ pub fn wait_for_ready(&self) {
+ let mut guard = self.state.interface.lock().unwrap();
+ while guard.is_none() && !self.state.cancel.load(Relaxed) {
+ guard = self.state.waiting_for_interface.wait(guard).unwrap();
+ }
+ }
+
+ /// Cancel an Interact (which causes `wait_for_ready` to always return).
+ pub fn cancel(&self) {
+ self.state.cancel.store(true, Relaxed);
+ self.state.waiting_for_interface.notify_all();
+ }
+
+ /// Run the given function on the element with the given type and identity.
+ ///
+ /// Panics if either the id is missing or the type is incorrect.
+ pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ &'b T: TryFrom<&'b model::ElementType>,
+ F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ self.interact(id, move |element: &IdElement| match element {
+ IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)),
+ IdElement::Window(_) => None,
+ })
+ .expect("incorrect element type")
+ }
+
+ /// Run the given function on the window with the given identity.
+ ///
+ /// Panics if the id is missing or the type is incorrect.
+ pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ self.interact(id, move |element| match element {
+ IdElement::Window(e) => Some(f(&e.style, &e.element_type)),
+ IdElement::Generic(_) => None,
+ })
+ .expect("incorrect element type")
+ }
+
+ fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ F: FnOnce(&IdElement<'b>) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ let (send, recv) = std::sync::mpsc::sync_channel(0);
+ {
+ let f: Box<dyn FnOnce(&IdElements<'b>) + Send + 'a> = Box::new(move |elements| {
+ let _ = send.send(elements.get(id).map(f));
+ });
+
+ // # Safety
+ // The function is run while `'a` is still valid (we wait here for it to complete).
+ let f: Box<dyn FnOnce(&IdElements) + Send + 'static> =
+ unsafe { std::mem::transmute(f) };
+
+ let guard = self.state.interface.lock().unwrap();
+ let interface = guard.as_ref().expect("renderer is not running");
+ let _ = interface.work.send(Command::Interact(f));
+ }
+ recv.recv().unwrap().expect("failed to get element")
+ }
+}
+
+#[derive(Clone)]
+struct UIInterface {
+ work: mpsc::Sender<Command>,
+}
+
+enum Command {
+ Invoke(Box<dyn FnOnce() + Send + 'static>),
+ Interact(Box<dyn FnOnce(&IdElements) + Send + 'static>),
+ Finish,
+}
+
+enum IdElement<'a> {
+ Generic(&'a Element),
+ Window(&'a model::TypedElement<model::Window>),
+}
+
+type IdElements<'a> = HashMap<String, IdElement<'a>>;
+
+#[derive(Default)]
+struct State {
+ interface: Mutex<Option<UIInterface>>,
+ waiting_for_interface: Condvar,
+ cancel: AtomicBool,
+}
+
+impl State {
+ /// Set the interface for the interaction client to use.
+ pub fn set_interface(&self, interface: UIInterface) {
+ *self.interface.lock().unwrap() = Some(interface);
+ self.waiting_for_interface.notify_all();
+ }
+
+ /// Clear the UI interface.
+ pub fn clear_interface(&self) {
+ *self.interface.lock().unwrap() = None;
+ }
+}
+
+fn id_elements<'a>(app: &'a Application) -> IdElements<'a> {
+ let mut elements: IdElements<'a> = Default::default();
+
+ let mut windows_to_visit: Vec<_> = app.windows.iter().collect();
+
+ let mut to_visit: Vec<&'a Element> = Vec::new();
+ while let Some(window) = windows_to_visit.pop() {
+ if let Some(id) = &window.style.id {
+ elements.insert(id.to_owned(), IdElement::Window(window));
+ }
+ windows_to_visit.extend(&window.element_type.children);
+
+ if let Some(content) = &window.element_type.content {
+ to_visit.push(content);
+ }
+ }
+
+ while let Some(el) = to_visit.pop() {
+ if let Some(id) = &el.style.id {
+ elements.insert(id.to_owned(), IdElement::Generic(el));
+ }
+
+ use model::ElementType::*;
+ match &el.element_type {
+ Button(model::Button {
+ content: Some(content),
+ ..
+ })
+ | Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ to_visit.push(content);
+ }
+ VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => {
+ for item in items {
+ to_visit.push(item)
+ }
+ }
+ _ => (),
+ }
+ }
+
+ elements
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/font.rs b/toolkit/crashreporter/client/app/src/ui/windows/font.rs
new file mode 100644
index 0000000000..3ec48316eb
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/font.rs
@@ -0,0 +1,56 @@
+/* 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/. */
+
+use windows_sys::Win32::{Foundation::S_OK, Graphics::Gdi, UI::Controls};
+
+/// Windows font handle (`HFONT`).
+pub struct Font(Gdi::HFONT);
+
+impl Font {
+ /// Get the system theme caption font.
+ ///
+ /// Panics if the font cannot be retrieved.
+ pub fn caption() -> Self {
+ unsafe {
+ let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
+ success!(hresult
+ Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font)
+ );
+ Font(success!(pointer Gdi::CreateFontIndirectW(&font)))
+ }
+ }
+
+ /// Get the system theme bold caption font.
+ ///
+ /// Returns `None` if the font cannot be retrieved.
+ pub fn caption_bold() -> Option<Self> {
+ unsafe {
+ let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
+ if Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font) != S_OK {
+ return None;
+ }
+ font.lfWeight = Gdi::FW_BOLD as i32;
+
+ let ptr = Gdi::CreateFontIndirectW(&font);
+ if ptr == 0 {
+ return None;
+ }
+ Some(Font(ptr))
+ }
+ }
+}
+
+impl std::ops::Deref for Font {
+ type Target = Gdi::HFONT;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Drop for Font {
+ fn drop(&mut self) {
+ unsafe { Gdi::DeleteObject(self.0 as _) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs
new file mode 100644
index 0000000000..89828987bc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs
@@ -0,0 +1,43 @@
+/* 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/. */
+
+//! GDI helpers.
+
+use windows_sys::Win32::{
+ Foundation::HWND,
+ Graphics::Gdi::{self, GDI_ERROR, HDC, HGDIOBJ},
+};
+
+/// A GDI drawing context.
+pub struct DC {
+ hwnd: HWND,
+ hdc: HDC,
+}
+
+impl DC {
+ /// Create a new DC.
+ pub fn new(hwnd: HWND) -> Option<Self> {
+ let hdc = unsafe { Gdi::GetDC(hwnd) };
+ (hdc != 0).then_some(DC { hwnd, hdc })
+ }
+
+ /// Call the given function with a gdi object selected.
+ pub fn with_object_selected<R>(&self, object: HGDIOBJ, f: impl FnOnce(HDC) -> R) -> Option<R> {
+ let old_object = unsafe { Gdi::SelectObject(self.hdc, object) };
+ if old_object == 0 || old_object == GDI_ERROR as isize {
+ return None;
+ }
+ let ret = f(self.hdc);
+ // The prior object must be selected before releasing the DC. Ignore errors; this is
+ // best-effort.
+ unsafe { Gdi::SelectObject(self.hdc, old_object) };
+ Some(ret)
+ }
+}
+
+impl Drop for DC {
+ fn drop(&mut self) {
+ unsafe { Gdi::ReleaseDC(self.hwnd, self.hdc) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/layout.rs b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs
new file mode 100644
index 0000000000..7563b6b2f0
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs
@@ -0,0 +1,436 @@
+/* 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/. */
+
+//! Helpers for window layout.
+
+use super::{
+ model::{self, Alignment, Element, ElementStyle, Margin},
+ ElementRef, WideString,
+};
+use crate::data::Property;
+use std::collections::HashMap;
+use windows_sys::Win32::{
+ Foundation::{HWND, SIZE},
+ Graphics::Gdi,
+ UI::WindowsAndMessaging as win,
+};
+
+pub(super) type ElementMapping = HashMap<ElementRef, HWND>;
+
+/// Handles the layout of windows.
+///
+/// This is done in two passes. The first pass calculates the sizes for all elements in the tree.
+/// Once sizes are known, the second pass can appropriately position the elements (taking alignment
+/// into account).
+///
+/// Currently, the resize/reposition logic is tied into the methods here, which is an inconvenient
+/// design because when adding support for a new element type you have to add new information in
+/// disparate locations.
+pub struct Layout<'a> {
+ elements: &'a ElementMapping,
+ sizes: HashMap<ElementRef, Size>,
+ last_positioned: Option<HWND>,
+}
+
+// Unfortunately, there's no good way to get these margins. I just guessed and the first guesses
+// seemed to be close enough.
+const BUTTON_MARGIN: Margin = Margin {
+ start: 5,
+ end: 5,
+ top: 5,
+ bottom: 5,
+};
+const CHECKBOX_MARGIN: Margin = Margin {
+ start: 15,
+ end: 0,
+ top: 0,
+ bottom: 0,
+};
+
+impl<'a> Layout<'a> {
+ pub(super) fn new(elements: &'a ElementMapping) -> Self {
+ Layout {
+ elements,
+ sizes: Default::default(),
+ last_positioned: None,
+ }
+ }
+
+ /// Perform a layout of the element and all child elements.
+ pub fn layout(mut self, element: &Element, max_width: u32, max_height: u32) {
+ let max_size = Size {
+ width: max_width,
+ height: max_height,
+ };
+ self.resize(element, &max_size);
+ self.reposition(element, &Position::default(), &max_size);
+ }
+
+ fn resize(&mut self, element: &Element, max_size: &Size) -> Size {
+ let style = &element.style;
+
+ let mut inner_size = max_size.inner_size(style);
+ let mut content_size = None;
+
+ if !is_visible(style) {
+ self.sizes
+ .insert(ElementRef::new(element), Default::default());
+ return Default::default();
+ }
+
+ // Resize inner content.
+ //
+ // These cases should result in `content_size` being set, if relevant.
+ use model::ElementType::*;
+ match &element.element_type {
+ Button(model::Button {
+ content: Some(content),
+ ..
+ }) => {
+ // Special case for buttons with a label.
+ if let Label(model::Label {
+ text: Property::Static(text),
+ ..
+ }) = &content.element_type
+ {
+ let mut size = inner_size.less_margin(&BUTTON_MARGIN);
+ self.measure_text(text.as_str(), element, &mut size);
+ content_size = Some(size.plus_margin(&BUTTON_MARGIN));
+ }
+ }
+ Checkbox(model::Checkbox {
+ label: Some(label), ..
+ }) => {
+ let mut size = inner_size.less_margin(&CHECKBOX_MARGIN);
+ self.measure_text(label.as_str(), element, &mut size);
+ content_size = Some(size.plus_margin(&CHECKBOX_MARGIN));
+ }
+ Label(model::Label { text, bold: _ }) => {
+ let mut size = inner_size.clone();
+ match text {
+ Property::Static(text) => self.measure_text(text.as_str(), element, &mut size),
+ Property::Binding(b) => {
+ self.measure_text(b.borrow().as_str(), element, &mut size)
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("Label::text does not support ReadOnly")
+ }
+ }
+ content_size = Some(size);
+ }
+ VBox(model::VBox { items, spacing }) => {
+ let mut height = 0;
+ let mut max_width = 0;
+ let mut remaining_size = inner_size.clone();
+ let mut resize_child = |c| {
+ let child_size = self.resize(c, &remaining_size);
+ height += child_size.height;
+ max_width = std::cmp::max(child_size.width, max_width);
+ remaining_size.height = remaining_size
+ .height
+ .saturating_sub(child_size.height + spacing);
+ };
+ // First resize all non-Fill items; Fill items get the remaining space.
+ for item in items
+ .iter()
+ .filter(|i| i.style.vertical_alignment != Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ for item in items
+ .iter()
+ .filter(|i| i.style.vertical_alignment == Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ content_size = Some(Size {
+ width: max_width,
+ height: height + spacing * (items.len().saturating_sub(1) as u32),
+ });
+ }
+ HBox(model::HBox {
+ items,
+ spacing,
+ affirmative_order: _,
+ }) => {
+ let mut width = 0;
+ let mut max_height = 0;
+ let mut remaining_size = inner_size.clone();
+ let mut resize_child = |c| {
+ let child_size = self.resize(c, &remaining_size);
+ width += child_size.width;
+ max_height = std::cmp::max(child_size.height, max_height);
+ remaining_size.width = remaining_size
+ .width
+ .saturating_sub(child_size.width + spacing);
+ };
+ // First resize all non-Fill items; Fill items get the remaining space.
+ for item in items
+ .iter()
+ .filter(|i| i.style.horizontal_alignment != Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ for item in items
+ .iter()
+ .filter(|i| i.style.horizontal_alignment == Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ content_size = Some(Size {
+ width: width + spacing * (items.len().saturating_sub(1) as u32),
+ height: max_height,
+ });
+ }
+ Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ content_size = Some(self.resize(content, &inner_size));
+ }
+ Progress(model::Progress { .. }) => {
+ // Min size recommended by windows uxguide
+ content_size = Some(Size {
+ width: 160,
+ height: 15,
+ });
+ }
+ // We don't support sizing by textbox content yet (need to read from the HWND due to
+ // Property::ReadOnly).
+ TextBox(_) => (),
+ _ => (),
+ }
+
+ // Adjust from content size.
+ if let Some(content_size) = content_size {
+ inner_size.from_content_size(style, &content_size);
+ }
+
+ // Compute/store (outer) size and return.
+ let size = inner_size.plus_margin(&style.margin);
+ self.sizes.insert(ElementRef::new(element), size);
+ size
+ }
+
+ fn get_size(&self, element: &Element) -> &Size {
+ self.sizes
+ .get(&ElementRef::new(element))
+ .expect("element not resized")
+ }
+
+ fn reposition(&mut self, element: &Element, position: &Position, parent_size: &Size) {
+ let style = &element.style;
+ if !is_visible(style) {
+ return;
+ }
+ let size = self.get_size(element);
+
+ let start_offset = match style.horizontal_alignment {
+ Alignment::Fill | Alignment::Start => 0,
+ Alignment::Center => parent_size.width.saturating_sub(size.width) / 2,
+ Alignment::End => parent_size.width.saturating_sub(size.width),
+ };
+ let top_offset = match style.vertical_alignment {
+ Alignment::Fill | Alignment::Start => 0,
+ Alignment::Center => parent_size.height.saturating_sub(size.height) / 2,
+ Alignment::End => parent_size.height.saturating_sub(size.height),
+ };
+
+ let inner_position = Position {
+ start: position.start + start_offset,
+ top: position.top + top_offset,
+ }
+ .less_margin(&style.margin);
+ let inner_size = size.less_margin(&style.margin);
+
+ // Set the window size/position if there is a handle associated with the element.
+ if let Some(&hwnd) = self.elements.get(&ElementRef::new(element)) {
+ unsafe {
+ win::SetWindowPos(
+ hwnd,
+ self.last_positioned.unwrap_or(win::HWND_TOP),
+ inner_position.start.try_into().unwrap(),
+ inner_position.top.try_into().unwrap(),
+ inner_size.width.try_into().unwrap(),
+ inner_size.height.try_into().unwrap(),
+ 0,
+ );
+ Gdi::InvalidateRect(hwnd, std::ptr::null(), 1);
+ }
+ self.last_positioned = Some(hwnd);
+ }
+
+ // Reposition content.
+ match &element.element_type {
+ model::ElementType::VBox(model::VBox { items, spacing }) => {
+ let mut position = inner_position;
+ let mut size = inner_size;
+ for item in items {
+ self.reposition(item, &position, &size);
+ let consumed = self.get_size(item).height + spacing;
+ if item.style.vertical_alignment != Alignment::End {
+ position.top += consumed;
+ }
+ size.height = size.height.saturating_sub(consumed);
+ }
+ }
+ model::ElementType::HBox(model::HBox {
+ items,
+ spacing,
+ // The default ordering matches the windows platform order
+ affirmative_order: _,
+ }) => {
+ let mut position = inner_position;
+ let mut size = inner_size;
+ for item in items {
+ self.reposition(item, &position, &inner_size);
+ let consumed = self.get_size(item).width + spacing;
+ if item.style.horizontal_alignment != Alignment::End {
+ position.start += consumed;
+ }
+ size.width = size.width.saturating_sub(consumed);
+ }
+ }
+ model::ElementType::Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ self.reposition(content, &inner_position, &inner_size);
+ }
+ _ => (),
+ }
+ }
+
+ /// The `size` represents the maximum size permitted for the text (which is used for word
+ /// breaking), and it will be set to the precise width and height of the text. The width should
+ /// not exceed the input `size` width, but the height may.
+ fn measure_text(&mut self, text: &str, element: &Element, size: &mut Size) {
+ let Some(&window) = self.elements.get(&ElementRef::new(element)) else {
+ return;
+ };
+ let hdc = unsafe { Gdi::GetDC(window) };
+ unsafe { Gdi::SelectObject(hdc, win::SendMessageW(window, win::WM_GETFONT, 0, 0) as _) };
+ let mut height: u32 = 0;
+ let mut max_width: u32 = 0;
+ let mut char_fit = 0i32;
+ let mut win_size = unsafe { std::mem::zeroed::<SIZE>() };
+ for mut line in text.lines() {
+ if line.is_empty() {
+ line = " ";
+ }
+ let text = WideString::new(line);
+ let mut text = text.as_slice();
+ let mut extents = vec![0i32; text.len()];
+ while !text.is_empty() {
+ unsafe {
+ Gdi::GetTextExtentExPointW(
+ hdc,
+ text.as_ptr(),
+ text.len() as i32,
+ size.width.try_into().unwrap(),
+ &mut char_fit,
+ extents.as_mut_ptr(),
+ &mut win_size,
+ );
+ }
+ if char_fit == 0 {
+ return;
+ }
+ let mut split = char_fit as usize;
+ let mut split_end = split.saturating_sub(1);
+ if (char_fit as usize) < text.len() {
+ for i in (0..char_fit as usize).rev() {
+ // FIXME safer utf16 handling?
+ if text[i] == b' ' as u16 {
+ split = i + 1;
+ split_end = i.saturating_sub(1);
+ break;
+ }
+ }
+ }
+ text = &text[split..];
+ max_width = std::cmp::max(max_width, extents[split_end].try_into().unwrap());
+ let measured_height: u32 = win_size.cy.try_into().unwrap();
+ height += measured_height;
+ }
+ }
+ unsafe { Gdi::ReleaseDC(window, hdc) };
+
+ assert!(max_width <= size.width);
+ size.width = max_width;
+ size.height = height;
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Size {
+ pub width: u32,
+ pub height: u32,
+}
+
+impl Size {
+ pub fn inner_size(&self, style: &ElementStyle) -> Self {
+ let mut ret = self.less_margin(&style.margin);
+ if let Some(width) = style.horizontal_size_request {
+ ret.width = width;
+ }
+ if let Some(height) = style.vertical_size_request {
+ ret.height = height;
+ }
+ ret
+ }
+
+ pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) {
+ if style.horizontal_size_request.is_none() && style.horizontal_alignment != Alignment::Fill
+ {
+ self.width = content_size.width;
+ }
+ if style.vertical_size_request.is_none() && style.vertical_alignment != Alignment::Fill {
+ self.height = content_size.height;
+ }
+ }
+
+ pub fn plus_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.width += margin.start + margin.end;
+ ret.height += margin.top + margin.bottom;
+ ret
+ }
+
+ pub fn less_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.width = ret.width.saturating_sub(margin.start + margin.end);
+ ret.height = ret.height.saturating_sub(margin.top + margin.bottom);
+ ret
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Position {
+ pub start: u32,
+ pub top: u32,
+}
+
+impl Position {
+ #[allow(dead_code)]
+ pub fn plus_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.start = ret.start.saturating_sub(margin.start);
+ ret.top = ret.top.saturating_sub(margin.top);
+ ret
+ }
+
+ pub fn less_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.start += margin.start;
+ ret.top += margin.top;
+ ret
+ }
+}
+
+fn is_visible(style: &ElementStyle) -> bool {
+ match &style.visible {
+ Property::Static(v) => *v,
+ Property::Binding(s) => *s.borrow(),
+ _ => true,
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/mod.rs b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs
new file mode 100644
index 0000000000..c2f396b80d
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs
@@ -0,0 +1,949 @@
+/* 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/. */
+
+//! A UI using the windows API.
+//!
+//! This UI contains some edge cases that aren't implemented, for instance:
+//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button
+//! containing Label, Scroll behavior, etc).
+//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, TextBox
+//! doesn't handle Binding, etc).
+//!
+//! The error handling is also a _little_ fast-and-loose, as many functions return an error value
+//! that is acceptable to following logic (though it still would be a good idea to improve this).
+//!
+//! The rendering treats VBox, HBox, and Scroll as strictly layout-only: they do not create any
+//! associated windows, and the layout logic handles their behavior.
+
+// Our windows-targets doesn't link uxtheme correctly for GetThemeSysFont/GetThemeSysColor.
+// This was working in windows-sys 0.48.
+#[link(name = "uxtheme", kind = "static")]
+extern "C" {}
+
+use super::model::{self, Application, Element, ElementStyle, TypedElement};
+use crate::data::Property;
+use font::Font;
+use once_cell::sync::Lazy;
+use quit_token::QuitToken;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::pin::Pin;
+use std::rc::Rc;
+use widestring::WideString;
+use window::{CustomWindowClass, Window, WindowBuilder};
+use windows_sys::Win32::{
+ Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM},
+ Graphics::Gdi,
+ System::{LibraryLoader::GetModuleHandleW, SystemServices, Threading::GetCurrentThreadId},
+ UI::{Controls, Input::KeyboardAndMouse, Shell, WindowsAndMessaging as win},
+};
+
+macro_rules! success {
+ ( nonzero $e:expr ) => {{
+ let value = $e;
+ assert_ne!(value, 0);
+ value
+ }};
+ ( lasterror $e:expr ) => {{
+ unsafe { windows_sys::Win32::Foundation::SetLastError(0) };
+ let value = $e;
+ assert!(value != 0 || windows_sys::Win32::Foundation::GetLastError() == 0);
+ value
+ }};
+ ( hresult $e:expr ) => {
+ assert_eq!($e, windows_sys::Win32::Foundation::S_OK);
+ };
+ ( pointer $e:expr ) => {{
+ let ptr = $e;
+ assert_ne!(ptr, 0);
+ ptr
+ }};
+}
+
+mod font;
+mod gdi;
+mod layout;
+mod quit_token;
+mod twoway;
+mod widestring;
+mod window;
+
+/// A Windows API UI implementation.
+pub struct UI {
+ thread_id: u32,
+}
+
+/// Custom user messages.
+#[repr(u32)]
+enum UserMessage {
+ Invoke = win::WM_USER,
+}
+
+fn get_invoke(msg: &win::MSG) -> Option<Box<model::InvokeFn>> {
+ if msg.message == UserMessage::Invoke as u32 {
+ Some(unsafe { Box::from_raw(msg.lParam as *mut model::InvokeFn) })
+ } else {
+ None
+ }
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ // Initialize common controls.
+ {
+ let icc = Controls::INITCOMMONCONTROLSEX {
+ dwSize: std::mem::size_of::<Controls::INITCOMMONCONTROLSEX>() as _,
+ // Buttons, edit controls, and static controls are all included in 'standard'.
+ dwICC: Controls::ICC_STANDARD_CLASSES | Controls::ICC_PROGRESS_CLASS,
+ };
+ success!(nonzero unsafe { Controls::InitCommonControlsEx(&icc) });
+ }
+
+ // Enable font smoothing (per
+ // https://learn.microsoft.com/en-us/windows/win32/gdi/cleartype-antialiasing ).
+ unsafe {
+ // We don't check for failure on these, they are best-effort.
+ win::SystemParametersInfoW(
+ win::SPI_SETFONTSMOOTHING,
+ 1,
+ std::ptr::null_mut(),
+ win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
+ );
+ win::SystemParametersInfoW(
+ win::SPI_SETFONTSMOOTHINGTYPE,
+ 0,
+ win::FE_FONTSMOOTHINGCLEARTYPE as _,
+ win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
+ );
+ }
+
+ // Enable correct layout direction.
+ if unsafe { win::SetProcessDefaultLayout(if app.rtl { Gdi::LAYOUT_RTL } else { 0 }) } == 0 {
+ log::warn!("failed to set process layout direction");
+ }
+
+ let module: HINSTANCE = unsafe { GetModuleHandleW(std::ptr::null()) };
+
+ // Register custom classes.
+ AppWindow::register(module).expect("failed to register AppWindow window class");
+
+ {
+ // The quit token is cloned for each top-level window and dropped at the end of this
+ // scope.
+ let quit_token = QuitToken::new();
+
+ for window in app.windows {
+ let name = WideString::new(window.element_type.title.as_str());
+ let w = top_level_window(
+ module,
+ AppWindow::new(
+ WindowRenderer::new(module, window.element_type, &window.style),
+ Some(quit_token.clone()),
+ ),
+ &name,
+ &window.style,
+ );
+
+ unsafe { win::ShowWindow(w.handle, win::SW_NORMAL) };
+ unsafe { Gdi::UpdateWindow(w.handle) };
+ }
+ }
+
+ // Run the event loop.
+ let mut msg = unsafe { std::mem::zeroed::<win::MSG>() };
+ while unsafe { win::GetMessageW(&mut msg, 0, 0, 0) } > 0 {
+ if let Some(f) = get_invoke(&msg) {
+ f();
+ continue;
+ }
+
+ unsafe {
+ // IsDialogMessageW is necessary to handle niceties like tab navigation
+ if win::IsDialogMessageW(win::GetAncestor(msg.hwnd, win::GA_ROOT), &mut msg) == 0 {
+ win::TranslateMessage(&msg);
+ win::DispatchMessageW(&msg);
+ }
+ }
+ }
+
+ // Flush queue to properly drop late invokes (this is a very unlikely case)
+ while unsafe { win::PeekMessageW(&mut msg, 0, 0, 0, win::PM_REMOVE) } > 0 {
+ if let Some(f) = get_invoke(&msg) {
+ drop(f);
+ }
+ }
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ let ptr: *mut model::InvokeFn = Box::into_raw(Box::new(f));
+ if unsafe {
+ win::PostThreadMessageW(self.thread_id, UserMessage::Invoke as u32, 0, ptr as _)
+ } == 0
+ {
+ let _ = unsafe { Box::from_raw(ptr) };
+ log::warn!("failed to invoke function on thread message queue");
+ }
+ }
+}
+
+impl Default for UI {
+ fn default() -> Self {
+ UI {
+ thread_id: unsafe { GetCurrentThreadId() },
+ }
+ }
+}
+
+/// A reference to an Element.
+#[derive(PartialEq, Eq, Hash, Clone, Copy)]
+struct ElementRef(*const Element);
+
+impl ElementRef {
+ pub fn new(element: &Element) -> Self {
+ ElementRef(element as *const Element)
+ }
+
+ /// # Safety
+ /// You must ensure the reference is still valid.
+ pub unsafe fn get(&self) -> &Element {
+ &*self.0
+ }
+}
+
+// Equivalent of win32 HIWORD macro
+fn hiword(v: u32) -> u16 {
+ (v >> 16) as u16
+}
+
+// Equivalent of win32 LOWORD macro
+fn loword(v: u32) -> u16 {
+ v as u16
+}
+
+// Equivalent of win32 MAKELONG macro
+fn makelong(low: u16, high: u16) -> u32 {
+ (high as u32) << 16 | low as u32
+}
+
+fn top_level_window<W: window::WindowClass + window::WindowData>(
+ module: HINSTANCE,
+ class: W,
+ title: &WideString,
+ style: &ElementStyle,
+) -> Window<W> {
+ class
+ .builder(module)
+ .name(title)
+ .style(win::WS_OVERLAPPEDWINDOW)
+ .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
+ .size(
+ style
+ .horizontal_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ style
+ .vertical_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ )
+ .create()
+}
+
+window::basic_window_classes! {
+ /// Static control (text, image, etc) class.
+ struct Static => "STATIC";
+
+ /// Button control class.
+ struct Button => "BUTTON";
+
+ /// Edit control class.
+ struct Edit => "EDIT";
+
+ /// Progress control class.
+ struct Progress => "msctls_progress32";
+}
+
+/// A top-level application window.
+///
+/// This is used for the main window and modal windows.
+struct AppWindow {
+ renderer: WindowRenderer,
+ _quit_token: Option<QuitToken>,
+}
+
+impl AppWindow {
+ pub fn new(renderer: WindowRenderer, quit_token: Option<QuitToken>) -> Self {
+ AppWindow {
+ renderer,
+ _quit_token: quit_token,
+ }
+ }
+}
+
+impl window::WindowClass for AppWindow {
+ fn class_name() -> WideString {
+ WideString::new("App Window")
+ }
+}
+
+impl CustomWindowClass for AppWindow {
+ fn icon() -> win::HICON {
+ static ICON: Lazy<win::HICON> = Lazy::new(|| unsafe {
+ // If CreateIconFromResource fails it returns NULL, which is fine (a default icon will be
+ // used).
+ win::CreateIconFromResource(
+ // We take advantage of the fact that since Windows Vista, an RT_ICON resource entry
+ // can simply be a PNG image.
+ super::icon::PNG_DATA.as_ptr(),
+ super::icon::PNG_DATA.len() as u32,
+ true.into(),
+ // The 0x00030000 constant isn't available anywhere; the docs basically say to just
+ // pass it...
+ 0x00030000,
+ )
+ });
+
+ *ICON
+ }
+
+ fn message(
+ data: &RefCell<Self>,
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> Option<LRESULT> {
+ let me = data.borrow();
+ let model = me.renderer.model();
+ match umsg {
+ win::WM_CREATE => {
+ if let Some(close) = &model.close {
+ close.subscribe(move |&()| unsafe {
+ win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0);
+ });
+ }
+
+ let mut renderer = me.renderer.child_renderer(hwnd);
+ if let Some(child) = &model.content {
+ renderer.render_child(child);
+ }
+
+ drop(model);
+ let children = std::mem::take(&mut me.renderer.model_mut().children);
+ for child in children {
+ renderer.render_window(child);
+ }
+ }
+ win::WM_CLOSE => {
+ if model.modal {
+ // Modal windows should hide themselves rather than closing/destroying.
+ unsafe { win::ShowWindow(hwnd, win::SW_HIDE) };
+ return Some(0);
+ }
+ }
+ win::WM_SHOWWINDOW => {
+ if model.modal {
+ // Modal windows should disable/enable their parent as they are shown/hid,
+ // respectively.
+ let shown = wparam != 0;
+ unsafe {
+ KeyboardAndMouse::EnableWindow(
+ win::GetWindow(hwnd, win::GW_OWNER),
+ (!shown).into(),
+ )
+ };
+ return Some(0);
+ }
+ }
+ win::WM_GETMINMAXINFO => {
+ let minmaxinfo = unsafe { (lparam as *mut win::MINMAXINFO).as_mut().unwrap() };
+ minmaxinfo.ptMinTrackSize.x = me.renderer.min_size.0.try_into().unwrap();
+ minmaxinfo.ptMinTrackSize.y = me.renderer.min_size.1.try_into().unwrap();
+ return Some(0);
+ }
+ win::WM_SIZE => {
+ // When resized, recompute the layout.
+ let width = loword(lparam as _) as u32;
+ let height = hiword(lparam as _) as u32;
+
+ if let Some(child) = &model.content {
+ me.renderer.layout(child, width, height);
+ unsafe { Gdi::UpdateWindow(hwnd) };
+ }
+ return Some(0);
+ }
+ win::WM_GETFONT => return Some(**me.renderer.font() as _),
+ win::WM_COMMAND => {
+ let child = lparam as HWND;
+ let windows = me.renderer.windows.borrow();
+ if let Some(&element) = windows.reverse().get(&child) {
+ // # Safety
+ // The ElementRefs all pertain to the model stored in the renderer.
+ let element = unsafe { element.get() };
+ // Handle button presses.
+ use model::ElementType::*;
+ match &element.element_type {
+ Button(model::Button { click, .. }) => {
+ let code = hiword(wparam as _) as u32;
+ if code == win::BN_CLICKED {
+ click.fire(&());
+ return Some(0);
+ }
+ }
+ Checkbox(model::Checkbox { checked, .. }) => {
+ let code = hiword(wparam as _) as u32;
+ if code == win::BN_CLICKED {
+ let check_state =
+ unsafe { win::SendMessageW(child, win::BM_GETCHECK, 0, 0) };
+ if let Property::Binding(s) = checked {
+ *s.borrow_mut() = check_state == Controls::BST_CHECKED as isize;
+ }
+ return Some(0);
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ _ => (),
+ }
+ None
+ }
+}
+
+/// State used while creating and updating windows.
+struct WindowRenderer {
+ // We wrap with an Rc to get weak references in property callbacks (like that of
+ // `ElementStyle::visible`).
+ inner: Rc<WindowRendererInner>,
+}
+
+impl std::ops::Deref for WindowRenderer {
+ type Target = WindowRendererInner;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+struct WindowRendererInner {
+ pub module: HINSTANCE,
+ /// The model is pinned and boxed to ensure that references in `windows` remain valid.
+ ///
+ /// We need to keep the model around so we can correctly perform layout as the window size
+ /// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform
+ /// layout.
+ pub model: RefCell<Pin<Box<model::Window>>>,
+ pub min_size: (u32, u32),
+ /// Mapping between model elements and windows.
+ ///
+ /// Element references pertain to elements in `model`.
+ pub windows: RefCell<twoway::TwoWay<ElementRef, HWND>>,
+ pub font: Font,
+ pub bold_font: Font,
+}
+
+impl WindowRenderer {
+ pub fn new(module: HINSTANCE, model: model::Window, style: &model::ElementStyle) -> Self {
+ WindowRenderer {
+ inner: Rc::new(WindowRendererInner {
+ module,
+ model: RefCell::new(Box::pin(model)),
+ min_size: (
+ style.horizontal_size_request.unwrap_or(0),
+ style.vertical_size_request.unwrap_or(0),
+ ),
+ windows: Default::default(),
+ font: Font::caption(),
+ bold_font: Font::caption_bold().unwrap_or_else(Font::caption),
+ }),
+ }
+ }
+
+ pub fn child_renderer(&self, window: HWND) -> WindowChildRenderer {
+ WindowChildRenderer {
+ renderer: &self.inner,
+ window,
+ child_id: 0,
+ scroll: false,
+ }
+ }
+
+ pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) {
+ layout::Layout::new(self.inner.windows.borrow().forward())
+ .layout(element, max_width, max_height);
+ }
+
+ pub fn model(&self) -> std::cell::Ref<'_, model::Window> {
+ std::cell::Ref::map(self.inner.model.borrow(), |b| &**b)
+ }
+
+ pub fn model_mut(&self) -> std::cell::RefMut<'_, model::Window> {
+ std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b)
+ }
+
+ pub fn font(&self) -> &Font {
+ &self.inner.font
+ }
+}
+
+struct WindowChildRenderer<'a> {
+ renderer: &'a Rc<WindowRendererInner>,
+ window: HWND,
+ child_id: i32,
+ scroll: bool,
+}
+
+impl<'a> WindowChildRenderer<'a> {
+ fn add_child<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
+ let builder = class
+ .builder(self.renderer.module)
+ .style(win::WS_CHILD | win::WS_VISIBLE)
+ .parent(self.window)
+ .child_id(self.child_id);
+ self.child_id += 1;
+ builder
+ }
+
+ fn add_window<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
+ class
+ .builder(self.renderer.module)
+ .style(win::WS_OVERLAPPEDWINDOW)
+ .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
+ .parent(self.window)
+ }
+
+ fn render_window(&mut self, model: TypedElement<model::Window>) -> Window {
+ let name = WideString::new(model.element_type.title.as_str());
+ let style = model.style;
+ let w = self
+ .add_window(AppWindow::new(
+ WindowRenderer::new(self.renderer.module, model.element_type, &style),
+ None,
+ ))
+ .size(
+ style
+ .horizontal_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ style
+ .vertical_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ )
+ .name(&name)
+ .create();
+
+ enabled_property(&style.enabled, w.handle);
+
+ let hwnd = w.handle;
+ let set_visible = move |visible| unsafe {
+ win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
+ };
+
+ match &style.visible {
+ Property::Static(false) => set_visible(false),
+ Property::Binding(s) => {
+ s.on_change(move |v| set_visible(*v));
+ if !*s.borrow() {
+ set_visible(false);
+ }
+ }
+ _ => (),
+ }
+
+ w.generic()
+ }
+
+ fn render_child(&mut self, element: &Element) {
+ if let Some(mut window) = self.render_element_type(&element.element_type) {
+ window.set_default_font(&self.renderer.font);
+
+ // Store the element to handle mapping.
+ self.renderer
+ .windows
+ .borrow_mut()
+ .insert(ElementRef::new(element), window.handle);
+
+ enabled_property(&element.style.enabled, window.handle);
+ }
+
+ // Handle visibility properties.
+ match &element.style.visible {
+ Property::Static(false) => {
+ set_visibility(element, false, self.renderer.windows.borrow().forward())
+ }
+ Property::Binding(s) => {
+ let weak_renderer = Rc::downgrade(self.renderer);
+ let element_ref = ElementRef::new(element);
+ let parent = self.window;
+ s.on_change(move |visible| {
+ let Some(renderer) = weak_renderer.upgrade() else {
+ return;
+ };
+ // # Safety
+ // ElementRefs are valid as long as the renderer is (and we have a strong
+ // reference to it).
+ let element = unsafe { element_ref.get() };
+ set_visibility(element, *visible, renderer.windows.borrow().forward());
+ // Send WM_SIZE so that the parent recomputes the layout.
+ unsafe {
+ let mut rect = std::mem::zeroed::<RECT>();
+ win::GetClientRect(parent, &mut rect);
+ win::SendMessageW(
+ parent,
+ win::WM_SIZE,
+ 0,
+ makelong(
+ (rect.right - rect.left) as u16,
+ (rect.bottom - rect.top) as u16,
+ ) as isize,
+ );
+ }
+ });
+ if !*s.borrow() {
+ set_visibility(element, false, self.renderer.windows.borrow().forward());
+ }
+ }
+ _ => (),
+ }
+ }
+
+ fn render_element_type(&mut self, element_type: &model::ElementType) -> Option<Window> {
+ use model::ElementType as ET;
+ match element_type {
+ ET::Label(model::Label { text, bold }) => {
+ let mut window = match text {
+ Property::Static(text) => {
+ let text = WideString::new(text.as_str());
+ self.add_child(Static)
+ .name(&text)
+ .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
+ .create()
+ }
+ Property::Binding(b) => {
+ let text = WideString::new(b.borrow().as_str());
+ let window = self
+ .add_child(Static)
+ .name(&text)
+ .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
+ .create();
+ let handle = window.handle;
+ b.on_change(move |text| {
+ let text = WideString::new(text.as_str());
+ unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
+ });
+ window
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ReadOnly property not supported for Label::text")
+ }
+ };
+ if *bold {
+ window.set_font(&self.renderer.bold_font);
+ }
+ Some(window.generic())
+ }
+ ET::TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let scroll = self.scroll;
+ let window = self
+ .add_child(Edit)
+ .add_style(
+ (win::ES_LEFT
+ | win::ES_MULTILINE
+ | win::ES_WANTRETURN
+ | if *editable { 0 } else { win::ES_READONLY })
+ as u32
+ | win::WS_BORDER
+ | win::WS_TABSTOP
+ | if scroll { win::WS_VSCROLL } else { 0 },
+ )
+ .create();
+
+ fn to_control_text(s: &str) -> String {
+ s.replace("\n", "\r\n")
+ }
+
+ fn from_control_text(s: &str) -> String {
+ s.replace("\r\n", "\n")
+ }
+
+ struct SubClassData {
+ placeholder: Option<WideString>,
+ }
+
+ // EM_SETCUEBANNER doesn't work with multiline edit controls (for no particular
+ // reason?), so we have to draw it ourselves.
+ unsafe extern "system" fn subclass_proc(
+ hwnd: HWND,
+ msg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ _uidsubclass: usize,
+ dw_ref_data: usize,
+ ) -> LRESULT {
+ let ret = Shell::DefSubclassProc(hwnd, msg, wparam, lparam);
+ if msg == win::WM_PAINT
+ && KeyboardAndMouse::GetFocus() != hwnd
+ && win::GetWindowTextLengthW(hwnd) == 0
+ {
+ let data = (dw_ref_data as *const SubClassData).as_ref().unwrap();
+ if let Some(placeholder) = &data.placeholder {
+ let mut rect = std::mem::zeroed::<RECT>();
+ win::GetClientRect(hwnd, &mut rect);
+ Gdi::InflateRect(&mut rect, -2, -2);
+
+ let dc = gdi::DC::new(hwnd).expect("failed to create GDI DC");
+ dc.with_object_selected(
+ win::SendMessageW(hwnd, win::WM_GETFONT, 0, 0) as _,
+ |hdc| {
+ Gdi::SetTextColor(
+ hdc,
+ Controls::GetThemeSysColor(0, Gdi::COLOR_GRAYTEXT),
+ );
+ Gdi::SetBkMode(hdc, Gdi::TRANSPARENT as i32);
+ success!(nonzero Gdi::DrawTextW(
+ hdc,
+ placeholder.pcwstr(),
+ -1,
+ &mut rect,
+ Gdi::DT_LEFT | Gdi::DT_TOP | Gdi::DT_WORDBREAK,
+ ));
+ },
+ )
+ .expect("failed to select font gdi object");
+ }
+ }
+
+ // Multiline edit controls capture the tab key. We want it to work as usual in
+ // the dialog (focusing the next input control).
+ if msg == win::WM_GETDLGCODE && wparam == KeyboardAndMouse::VK_TAB as usize {
+ return 0;
+ }
+
+ if msg == win::WM_DESTROY {
+ drop(unsafe { Box::from_raw(dw_ref_data as *mut SubClassData) });
+ }
+ return ret;
+ }
+
+ let subclassdata = Box::into_raw(Box::new(SubClassData {
+ placeholder: placeholder
+ .as_ref()
+ .map(|s| WideString::new(to_control_text(s))),
+ }));
+
+ unsafe {
+ Shell::SetWindowSubclass(
+ window.handle,
+ Some(subclass_proc),
+ 0,
+ subclassdata as _,
+ );
+ }
+
+ // Set up content property.
+ match content {
+ Property::ReadOnly(od) => {
+ let handle = window.handle;
+ od.register(move |target| {
+ // GetWindowText requires the buffer be large enough for the terminating
+ // null character (otherwise it truncates the string), but
+ // GetWindowTextLength returns the length without the null character, so we
+ // add 1.
+ let length = unsafe { win::GetWindowTextLengthW(handle) } + 1;
+ let mut buf = vec![0u16; length as usize];
+ unsafe { win::GetWindowTextW(handle, buf.as_mut_ptr(), length) };
+ buf.pop(); // null character; `String` doesn't want that
+ *target = from_control_text(&String::from_utf16_lossy(&buf));
+ });
+ }
+ Property::Static(s) => {
+ let text = WideString::new(to_control_text(s));
+ unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
+ }
+ Property::Binding(b) => {
+ let handle = window.handle;
+ b.on_change(move |text| {
+ let text = WideString::new(to_control_text(text.as_str()));
+ unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
+ });
+ let text = WideString::new(to_control_text(b.borrow().as_str()));
+ unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
+ }
+ }
+ Some(window.generic())
+ }
+ ET::Scroll(model::Scroll { content }) => {
+ if let Some(content) = content {
+ // Scrolling is implemented in a cooperative, non-universal way right now.
+ self.scroll = true;
+ self.render_child(content);
+ self.scroll = false;
+ }
+ None
+ }
+ ET::Button(model::Button { content, .. }) => {
+ if let Some(ET::Label(model::Label {
+ text: Property::Static(text),
+ ..
+ })) = content.as_ref().map(|e| &e.element_type)
+ {
+ let text = WideString::new(text);
+
+ let window = self
+ .add_child(Button)
+ .add_style(win::BS_PUSHBUTTON as u32 | win::WS_TABSTOP)
+ .name(&text)
+ .create();
+ Some(window.generic())
+ } else {
+ None
+ }
+ }
+ ET::Checkbox(model::Checkbox { checked, label }) => {
+ let label = label.as_ref().map(WideString::new);
+ let mut builder = self
+ .add_child(Button)
+ .add_style((win::BS_AUTOCHECKBOX | win::BS_MULTILINE) as u32 | win::WS_TABSTOP);
+ if let Some(label) = &label {
+ builder = builder.name(label);
+ }
+ let window = builder.create();
+
+ fn set_check(handle: HWND, value: bool) {
+ unsafe {
+ win::SendMessageW(
+ handle,
+ win::BM_SETCHECK,
+ if value {
+ Controls::BST_CHECKED
+ } else {
+ Controls::BST_UNCHECKED
+ } as usize,
+ 0,
+ );
+ }
+ }
+
+ match checked {
+ Property::Static(checked) => set_check(window.handle, *checked),
+ Property::Binding(s) => {
+ let handle = window.handle;
+ s.on_change(move |v| {
+ set_check(handle, *v);
+ });
+ set_check(window.handle, *s.borrow());
+ }
+ _ => unimplemented!("ReadOnly properties not supported for Checkbox"),
+ }
+
+ Some(window.generic())
+ }
+ ET::Progress(model::Progress { amount }) => {
+ let window = self
+ .add_child(Progress)
+ .add_style(Controls::PBS_MARQUEE)
+ .create();
+
+ fn set_amount(handle: HWND, value: Option<f32>) {
+ match value {
+ None => unsafe {
+ win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 1, 0);
+ },
+ Some(v) => unsafe {
+ win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 0, 0);
+ win::SendMessageW(
+ handle,
+ Controls::PBM_SETPOS,
+ (v.clamp(0f32, 1f32) * 100f32) as usize,
+ 0,
+ );
+ },
+ }
+ }
+
+ match amount {
+ Property::Static(v) => set_amount(window.handle, *v),
+ Property::Binding(s) => {
+ let handle = window.handle;
+ s.on_change(move |v| set_amount(handle, *v));
+ set_amount(window.handle, *s.borrow());
+ }
+ _ => unimplemented!("ReadOnly properties not supported for Progress"),
+ }
+
+ Some(window.generic())
+ }
+ // VBox/HBox are virtual, their behaviors are implemented entirely in the renderer layout.
+ // No need for additional windows.
+ ET::VBox(model::VBox { items, .. }) => {
+ for item in items {
+ self.render_child(item);
+ }
+ None
+ }
+ ET::HBox(model::HBox { items, .. }) => {
+ for item in items {
+ self.render_child(item);
+ }
+ None
+ }
+ }
+ }
+}
+
+/// Handle the enabled property.
+///
+/// This function assumes the default state of the window is enabled.
+fn enabled_property(enabled: &Property<bool>, window: HWND) {
+ match enabled {
+ Property::Static(false) => unsafe {
+ KeyboardAndMouse::EnableWindow(window, false.into());
+ },
+ Property::Binding(s) => {
+ let handle = window;
+ s.on_change(move |enabled| {
+ unsafe { KeyboardAndMouse::EnableWindow(handle, (*enabled).into()) };
+ });
+ if !*s.borrow() {
+ unsafe { KeyboardAndMouse::EnableWindow(window, false.into()) };
+ }
+ }
+ _ => (),
+ }
+}
+
+/// Set the visibility of the given element. This recurses down the element tree and hides children
+/// as necessary.
+fn set_visibility(element: &Element, visible: bool, windows: &HashMap<ElementRef, HWND>) {
+ if let Some(&hwnd) = windows.get(&ElementRef::new(element)) {
+ unsafe {
+ win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
+ }
+ } else {
+ match &element.element_type {
+ model::ElementType::VBox(model::VBox { items, .. }) => {
+ for item in items {
+ set_visibility(item, visible, windows);
+ }
+ }
+ model::ElementType::HBox(model::HBox { items, .. }) => {
+ for item in items {
+ set_visibility(item, visible, windows);
+ }
+ }
+ model::ElementType::Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ set_visibility(&*content, visible, windows);
+ }
+ _ => (),
+ }
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs
new file mode 100644
index 0000000000..f952db3db4
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs
@@ -0,0 +1,33 @@
+/* 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/. */
+
+use std::rc::Rc;
+use windows_sys::Win32::UI::WindowsAndMessaging::PostQuitMessage;
+
+/// A Cloneable token which will post a quit message (with code 0) to the main loop when the last
+/// instance is dropped.
+#[derive(Clone, Default)]
+pub struct QuitToken(#[allow(dead_code)] Rc<QuitTokenInternal>);
+
+impl QuitToken {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl std::fmt::Debug for QuitToken {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct(std::any::type_name::<Self>())
+ .finish_non_exhaustive()
+ }
+}
+
+#[derive(Default)]
+struct QuitTokenInternal;
+
+impl Drop for QuitTokenInternal {
+ fn drop(&mut self) {
+ unsafe { PostQuitMessage(0) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs
new file mode 100644
index 0000000000..8a18162e08
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs
@@ -0,0 +1,36 @@
+/* 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/. */
+
+use std::collections::HashMap;
+
+/// A two-way hashmap.
+#[derive(Debug)]
+pub struct TwoWay<K, V> {
+ forward: HashMap<K, V>,
+ reverse: HashMap<V, K>,
+}
+
+impl<K, V> Default for TwoWay<K, V> {
+ fn default() -> Self {
+ TwoWay {
+ forward: Default::default(),
+ reverse: Default::default(),
+ }
+ }
+}
+
+impl<K: Eq + std::hash::Hash + Clone, V: Eq + std::hash::Hash + Clone> TwoWay<K, V> {
+ pub fn insert(&mut self, key: K, value: V) {
+ self.forward.insert(key.clone(), value.clone());
+ self.reverse.insert(value, key);
+ }
+
+ pub fn forward(&self) -> &HashMap<K, V> {
+ &self.forward
+ }
+
+ pub fn reverse(&self) -> &HashMap<V, K> {
+ &self.reverse
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs
new file mode 100644
index 0000000000..0dc713352b
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs
@@ -0,0 +1,36 @@
+/* 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/. */
+
+use std::ffi::OsStr;
+use std::os::windows::ffi::OsStrExt;
+use windows_sys::core::PCWSTR;
+
+/// Windows wide strings.
+///
+/// These are utf16 encoded with a terminating null character (0).
+pub struct WideString(Vec<u16>);
+
+impl WideString {
+ pub fn new(os_str: impl AsRef<OsStr>) -> Self {
+ // TODO: doesn't check whether the OsStr contains a null character, which could be treated
+ // as an error (as `CString::new` does).
+ WideString(
+ os_str
+ .as_ref()
+ .encode_wide()
+ .chain(std::iter::once(0))
+ // Remove unicode BIDI markers (from fluent) which aren't rendered correctly.
+ .filter(|c| *c != 0x2068 && *c != 0x2069)
+ .collect(),
+ )
+ }
+
+ pub fn pcwstr(&self) -> PCWSTR {
+ self.0.as_ptr()
+ }
+
+ pub fn as_slice(&self) -> &[u16] {
+ &self.0
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/window.rs b/toolkit/crashreporter/client/app/src/ui/windows/window.rs
new file mode 100644
index 0000000000..7e5e8f3f2a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/window.rs
@@ -0,0 +1,302 @@
+/* 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/. */
+
+//! Types and helpers relating to windows and window classes.
+
+use super::Font;
+use super::WideString;
+use std::cell::RefCell;
+use windows_sys::Win32::{
+ Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
+ Graphics::Gdi::{self, HBRUSH},
+ UI::WindowsAndMessaging::{self as win, HCURSOR, HICON},
+};
+
+/// Types representing a window class.
+pub trait WindowClass: Sized + 'static {
+ fn class_name() -> WideString;
+
+ fn builder(self, module: HINSTANCE) -> WindowBuilder<'static, Self> {
+ WindowBuilder {
+ name: None,
+ style: None,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ parent: None,
+ child_id: 0,
+ module,
+ data: self,
+ }
+ }
+}
+
+/// Window classes which have their own message handler.
+///
+/// A type implementing this trait provides its data to a single window.
+///
+/// `register` must be called before use.
+pub trait CustomWindowClass: WindowClass {
+ /// Handle a message.
+ fn message(
+ data: &RefCell<Self>,
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> Option<LRESULT>;
+
+ /// The class's default background brush.
+ fn background() -> HBRUSH {
+ (Gdi::COLOR_3DFACE + 1) as HBRUSH
+ }
+
+ /// The class's default cursor.
+ fn cursor() -> HCURSOR {
+ unsafe { win::LoadCursorW(0, win::IDC_ARROW) }
+ }
+
+ /// The class's default icon.
+ fn icon() -> HICON {
+ 0
+ }
+
+ /// Register the class.
+ fn register(module: HINSTANCE) -> anyhow::Result<()> {
+ unsafe extern "system" fn wnd_proc<W: CustomWindowClass>(
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> LRESULT {
+ if umsg == win::WM_CREATE {
+ let create_struct = &*(lparam as *const win::CREATESTRUCTW);
+ success!(lasterror win::SetWindowLongPtrW(hwnd, 0, create_struct.lpCreateParams as _));
+ // Changes made with SetWindowLongPtr don't take effect until SetWindowPos is called...
+ success!(nonzero win::SetWindowPos(
+ hwnd,
+ win::HWND_TOP,
+ 0,
+ 0,
+ 0,
+ 0,
+ win::SWP_NOMOVE | win::SWP_NOSIZE | win::SWP_NOZORDER | win::SWP_FRAMECHANGED,
+ ));
+ }
+
+ let result = unsafe { W::get(hwnd).as_ref() }
+ .and_then(|data| W::message(data, hwnd, umsg, wparam, lparam));
+ if umsg == win::WM_DESTROY {
+ drop(Box::from_raw(
+ win::GetWindowLongPtrW(hwnd, 0) as *mut RefCell<W>
+ ));
+ }
+ result.unwrap_or_else(|| win::DefWindowProcW(hwnd, umsg, wparam, lparam))
+ }
+
+ let class_name = Self::class_name();
+ let window_class = win::WNDCLASSW {
+ lpfnWndProc: Some(wnd_proc::<Self>),
+ hInstance: module,
+ lpszClassName: class_name.pcwstr(),
+ hbrBackground: Self::background(),
+ hIcon: Self::icon(),
+ hCursor: Self::cursor(),
+ cbWndExtra: std::mem::size_of::<isize>() as i32,
+ ..unsafe { std::mem::zeroed() }
+ };
+
+ if unsafe { win::RegisterClassW(&window_class) } == 0 {
+ anyhow::bail!("RegisterClassW failed")
+ }
+ Ok(())
+ }
+
+ /// Get the window data from a window created with this class.
+ ///
+ /// # Safety
+ /// This must only be called on window handles which were created with this class.
+ unsafe fn get(hwnd: HWND) -> *const RefCell<Self> {
+ win::GetWindowLongPtrW(hwnd, 0) as *const RefCell<Self>
+ }
+}
+
+/// Types that can be stored as associated window data.
+pub trait WindowData: Sized {
+ fn to_ptr(self) -> *mut RefCell<Self> {
+ std::ptr::null_mut()
+ }
+}
+
+impl<T: CustomWindowClass> WindowData for T {
+ fn to_ptr(self) -> *mut RefCell<Self> {
+ Box::into_raw(Box::new(RefCell::new(self)))
+ }
+}
+
+macro_rules! basic_window_classes {
+ () => {};
+ ( $(#[$attr:meta])* struct $name:ident => $class:expr; $($rest:tt)* ) => {
+ #[derive(Default)]
+ $(#[$attr])*
+ struct $name;
+
+ impl $crate::ui::ui_impl::window::WindowClass for $name {
+ fn class_name() -> $crate::ui::ui_impl::WideString {
+ $crate::ui::ui_impl::WideString::new($class)
+ }
+ }
+
+ impl $crate::ui::ui_impl::window::WindowData for $name {}
+
+ $crate::ui::ui_impl::window::basic_window_classes!($($rest)*);
+ }
+}
+
+pub(crate) use basic_window_classes;
+
+pub struct WindowBuilder<'a, W> {
+ name: Option<&'a WideString>,
+ style: Option<u32>,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ parent: Option<HWND>,
+ child_id: i32,
+ module: HINSTANCE,
+ data: W,
+}
+
+impl<'a, W> WindowBuilder<'a, W> {
+ #[must_use]
+ pub fn name<'b>(self, s: &'b WideString) -> WindowBuilder<'b, W> {
+ WindowBuilder {
+ name: Some(s),
+ style: self.style,
+ x: self.x,
+ y: self.y,
+ width: self.width,
+ height: self.height,
+ parent: self.parent,
+ child_id: self.child_id,
+ module: self.module,
+ data: self.data,
+ }
+ }
+
+ #[must_use]
+ pub fn style(mut self, d: u32) -> Self {
+ self.style = Some(d);
+ self
+ }
+
+ #[must_use]
+ pub fn add_style(mut self, d: u32) -> Self {
+ *self.style.get_or_insert(0) |= d;
+ self
+ }
+
+ #[must_use]
+ pub fn pos(mut self, x: i32, y: i32) -> Self {
+ self.x = x;
+ self.y = y;
+ self
+ }
+
+ #[must_use]
+ pub fn size(mut self, width: i32, height: i32) -> Self {
+ self.width = width;
+ self.height = height;
+ self
+ }
+
+ #[must_use]
+ pub fn parent(mut self, parent: HWND) -> Self {
+ self.parent = Some(parent);
+ self
+ }
+
+ #[must_use]
+ pub fn child_id(mut self, id: i32) -> Self {
+ self.child_id = id;
+ self
+ }
+
+ pub fn create(self) -> Window<W>
+ where
+ W: WindowClass + WindowData,
+ {
+ let class_name = W::class_name();
+ let handle = unsafe {
+ win::CreateWindowExW(
+ 0,
+ class_name.pcwstr(),
+ self.name.map(|n| n.pcwstr()).unwrap_or(std::ptr::null()),
+ self.style.unwrap_or_default(),
+ self.x,
+ self.y,
+ self.width,
+ self.height,
+ self.parent.unwrap_or_default(),
+ self.child_id as _,
+ self.module,
+ self.data.to_ptr() as _,
+ )
+ };
+ assert!(handle != 0);
+
+ Window {
+ handle,
+ child_id: self.child_id,
+ font_set: false,
+ _class: std::marker::PhantomData,
+ }
+ }
+}
+
+/// A window handle with a known class type.
+///
+/// Without a type parameter (defaulting to `()`), the window handle is generic (class type
+/// unknown).
+pub struct Window<W: 'static = ()> {
+ pub handle: HWND,
+ pub child_id: i32,
+ font_set: bool,
+ _class: std::marker::PhantomData<&'static RefCell<W>>,
+}
+
+impl<W: CustomWindowClass> Window<W> {
+ /// Get the window data of the window.
+ #[allow(dead_code)]
+ pub fn data(&self) -> &RefCell<W> {
+ unsafe { W::get(self.handle).as_ref().unwrap() }
+ }
+}
+
+impl<W> Window<W> {
+ /// Get a generic window handle.
+ pub fn generic(self) -> Window {
+ Window {
+ handle: self.handle,
+ child_id: self.child_id,
+ font_set: self.font_set,
+ _class: std::marker::PhantomData,
+ }
+ }
+
+ /// Set a window's font.
+ pub fn set_font(&mut self, font: &Font) {
+ unsafe { win::SendMessageW(self.handle, win::WM_SETFONT, **font as _, 1 as _) };
+ self.font_set = true;
+ }
+
+ /// Set a window's font if not already set.
+ pub fn set_default_font(&mut self, font: &Font) {
+ if !self.font_set {
+ self.set_font(font);
+ }
+ }
+}
diff --git a/toolkit/crashreporter/client/cocoabind/Cargo.toml b/toolkit/crashreporter/client/cocoabind/Cargo.toml
new file mode 100644
index 0000000000..70a0f36582
--- /dev/null
+++ b/toolkit/crashreporter/client/cocoabind/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "cocoabind"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+block = "0.1"
+objc = "0.2"
+
+[build-dependencies]
+bindgen = { version = "0.69", default-features = false, features = ["runtime"] }
+mozbuild = "0.1.0"
diff --git a/toolkit/crashreporter/client/cocoabind/build.rs b/toolkit/crashreporter/client/cocoabind/build.rs
new file mode 100644
index 0000000000..ba91a87e97
--- /dev/null
+++ b/toolkit/crashreporter/client/cocoabind/build.rs
@@ -0,0 +1,74 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+use mozbuild::config::CC_BASE_FLAGS as CFLAGS;
+
+const TYPES: &[&str] = &[
+ "ActionCell",
+ "Application",
+ "Array",
+ "AttributedString",
+ "Box",
+ "Button",
+ "ButtonCell",
+ "Cell",
+ "ClassDescription",
+ "Control",
+ "DefaultRunLoopMode",
+ "Dictionary",
+ "ForegroundColorAttributeName",
+ "LayoutDimension",
+ "LayoutGuide",
+ "LayoutXAxisAnchor",
+ "LayoutYAxisAnchor",
+ "MutableAttributedString",
+ "MutableParagraphStyle",
+ "MutableString",
+ "ModalPanelRunLoopMode",
+ "Panel",
+ "ProcessInfo",
+ "ProgressIndicator",
+ "Proxy",
+ "RunLoop",
+ "ScrollView",
+ "StackView",
+ "String",
+ "TextField",
+ "TextView",
+ "Value",
+ "View",
+ "Window",
+];
+
+fn main() {
+ let mut builder = bindgen::Builder::default()
+ .header_contents(
+ "cocoa_bindings.h",
+ "#define self self_
+ #import <Cocoa/Cocoa.h>
+ ",
+ )
+ .generate_block(true)
+ .prepend_enum_name(false)
+ .clang_args(CFLAGS)
+ .clang_args(["-x", "objective-c"])
+ .clang_arg("-fblocks")
+ .derive_default(true)
+ .allowlist_item("TransformProcessType");
+ for name in TYPES {
+ // (I|P) covers generated traits (interfaces and protocols). `(_.*)?` covers categories
+ // (which are generated as `CLASS_CATEGORY`).
+ builder = builder.allowlist_item(format!("(I|P)?NS{name}(_.*)?"));
+ }
+ let bindings = builder
+ .generate()
+ .expect("unable to generate cocoa bindings");
+ let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("cocoa_bindings.rs"))
+ .expect("failed to write cocoa bindings");
+ println!("cargo:rustc-link-lib=framework=AppKit");
+ println!("cargo:rustc-link-lib=framework=Cocoa");
+ println!("cargo:rustc-link-lib=framework=Foundation");
+}
diff --git a/toolkit/crashreporter/client/cocoabind/src/lib.rs b/toolkit/crashreporter/client/cocoabind/src/lib.rs
new file mode 100644
index 0000000000..b7271d9d92
--- /dev/null
+++ b/toolkit/crashreporter/client/cocoabind/src/lib.rs
@@ -0,0 +1,10 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(unused_imports)]
+
+include!(concat!(env!("OUT_DIR"), "/cocoa_bindings.rs"));
diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp
deleted file mode 100644
index 2887b16170..0000000000
--- a/toolkit/crashreporter/client/crashreporter.cpp
+++ /dev/null
@@ -1,852 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#include "crashreporter.h"
-
-#ifdef _MSC_VER
-// Disable exception handler warnings.
-# pragma warning(disable : 4530)
-#endif
-
-#include <fstream>
-#include <iomanip>
-#include <sstream>
-#include <memory>
-#include <ctime>
-#include <cstdlib>
-#include <cstring>
-#include <string>
-#include <utility>
-
-#ifdef XP_LINUX
-# include <dlfcn.h>
-#endif
-
-#include "json/json.h"
-#include "nss.h"
-#include "sechash.h"
-
-using std::ifstream;
-using std::ios;
-using std::istream;
-using std::istringstream;
-using std::ofstream;
-using std::ostream;
-using std::ostringstream;
-using std::string;
-using std::unique_ptr;
-using std::vector;
-
-namespace CrashReporter {
-
-StringTable gStrings;
-Json::Value gData;
-string gSettingsPath;
-string gEventsPath;
-string gPingPath;
-int gArgc;
-char** gArgv;
-bool gAutoSubmit;
-
-enum SubmissionResult { Succeeded, Failed };
-
-static unique_ptr<ofstream> gLogStream(nullptr);
-static string gReporterDumpFile;
-static string gExtraFile;
-static string gMemoryFile;
-
-static const char kExtraDataExtension[] = ".extra";
-static const char kMemoryReportExtension[] = ".memory.json.gz";
-
-void UIError(const string& message) {
- if (gAutoSubmit) {
- return;
- }
-
- string errorMessage;
- if (!gStrings[ST_CRASHREPORTERERROR].empty()) {
- char buf[2048];
- snprintf(buf, 2048, gStrings[ST_CRASHREPORTERERROR].c_str(),
- message.c_str());
- errorMessage = buf;
- } else {
- errorMessage = message;
- }
-
- UIError_impl(errorMessage);
-}
-
-static string Unescape(const string& str) {
- string ret;
- for (string::const_iterator iter = str.begin(); iter != str.end(); iter++) {
- if (*iter == '\\') {
- iter++;
- if (*iter == '\\') {
- ret.push_back('\\');
- } else if (*iter == 'n') {
- ret.push_back('\n');
- } else if (*iter == 't') {
- ret.push_back('\t');
- }
- } else {
- ret.push_back(*iter);
- }
- }
-
- return ret;
-}
-
-bool ReadStrings(istream& in, StringTable& strings, bool unescape) {
- while (!in.eof()) {
- string line;
- std::getline(in, line);
- int sep = line.find('=');
- if (sep >= 0) {
- string key, value;
- key = line.substr(0, sep);
- value = line.substr(sep + 1);
- if (unescape) value = Unescape(value);
- strings[key] = value;
- }
- }
-
- return true;
-}
-
-bool ReadStringsFromFile(const string& path, StringTable& strings,
- bool unescape) {
- ifstream* f = UIOpenRead(path, ios::in);
- bool success = false;
- if (f->is_open()) {
- success = ReadStrings(*f, strings, unescape);
- f->close();
- }
-
- delete f;
- return success;
-}
-
-static bool ReadExtraFile(const string& aExtraDataPath, Json::Value& aExtra) {
- bool success = false;
- ifstream* f = UIOpenRead(aExtraDataPath, ios::in);
- if (f->is_open()) {
- Json::CharReaderBuilder builder;
- success = parseFromStream(builder, *f, &aExtra, nullptr);
- }
-
- delete f;
- return success;
-}
-
-static string Basename(const string& file) {
- string::size_type slashIndex = file.rfind(UI_DIR_SEPARATOR);
- if (slashIndex != string::npos) {
- return file.substr(slashIndex + 1);
- }
- return file;
-}
-
-static bool ReadEventFile(const string& aPath, string& aEventVersion,
- string& aTime, string& aUuid, Json::Value& aData) {
- bool res = false;
- ifstream* f = UIOpenRead(aPath, ios::binary);
-
- if (f->is_open()) {
- std::getline(*f, aEventVersion, '\n');
- res = f->good();
- std::getline(*f, aTime, '\n');
- res &= f->good();
- std::getline(*f, aUuid, '\n');
- res &= f->good();
-
- if (res) {
- Json::CharReaderBuilder builder;
- res = parseFromStream(builder, *f, &aData, nullptr);
- }
- }
-
- delete f;
- return res;
-}
-
-static void OverwriteEventFile(const string& aPath, const string& aEventVersion,
- const string& aTime, const string& aUuid,
- const Json::Value& aData) {
- ofstream* f = UIOpenWrite(aPath, ios::binary | ios::trunc);
- if (f->is_open()) {
- f->write(aEventVersion.c_str(), aEventVersion.length()) << '\n';
- f->write(aTime.c_str(), aTime.length()) << '\n';
- f->write(aUuid.c_str(), aUuid.length()) << '\n';
-
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
- writer->write(aData, f);
- *f << "\n";
- }
-
- delete f;
-}
-
-static void UpdateEventFile(const Json::Value& aExtraData, const string& aHash,
- const string& aPingUuid) {
- if (gEventsPath.empty()) {
- // If there is no path for finding the crash event, skip this step.
- return;
- }
-
- string localId = CrashReporter::GetDumpLocalID();
- string path = gEventsPath + UI_DIR_SEPARATOR + localId;
- string eventVersion;
- string crashTime;
- string crashUuid;
- Json::Value eventData;
-
- if (!ReadEventFile(path, eventVersion, crashTime, crashUuid, eventData)) {
- return;
- }
-
- if (!aHash.empty()) {
- eventData["MinidumpSha256Hash"] = aHash;
- }
-
- if (!aPingUuid.empty()) {
- eventData["CrashPingUUID"] = aPingUuid;
- }
-
- if (aExtraData.isMember("StackTraces")) {
- eventData["StackTraces"] = aExtraData["StackTraces"];
- }
-
- OverwriteEventFile(path, eventVersion, crashTime, crashUuid, eventData);
-}
-
-static void WriteSubmissionEvent(SubmissionResult result,
- const string& remoteId) {
- if (gEventsPath.empty()) {
- // If there is no path for writing the submission event, skip it.
- return;
- }
-
- string localId = CrashReporter::GetDumpLocalID();
- string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission";
- ofstream* f = UIOpenWrite(fpath, ios::binary);
- time_t tm;
- time(&tm);
-
- if (f->is_open()) {
- *f << "crash.submission.1\n";
- *f << tm << "\n";
- *f << localId << "\n";
- *f << (result == Succeeded ? "true" : "false") << "\n";
- *f << remoteId;
-
- f->close();
- }
-
- delete f;
-}
-
-void LogMessage(const std::string& message) {
- if (gLogStream.get()) {
- char date[64];
- time_t tm;
- time(&tm);
- if (strftime(date, sizeof(date) - 1, "%c", localtime(&tm)) == 0)
- date[0] = '\0';
- (*gLogStream) << "[" << date << "] " << message << '\n';
- }
-}
-
-static void OpenLogFile() {
- string logPath = gSettingsPath + UI_DIR_SEPARATOR + "submit.log";
- gLogStream.reset(UIOpenWrite(logPath, ios::app));
-}
-
-static bool ReadConfig() {
- string iniPath;
- if (!UIGetIniPath(iniPath)) {
- return false;
- }
-
- if (!ReadStringsFromFile(iniPath, gStrings, true)) return false;
-
- // See if we have a string override file, if so process it
- char* overrideEnv = getenv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE");
- if (overrideEnv && *overrideEnv && UIFileExists(overrideEnv))
- ReadStringsFromFile(overrideEnv, gStrings, true);
-
- return true;
-}
-
-static string GetAdditionalFilename(const string& dumpfile,
- const char* extension) {
- string filename(dumpfile);
- int dot = filename.rfind('.');
- if (dot < 0) return "";
-
- filename.replace(dot, filename.length() - dot, extension);
- return filename;
-}
-
-static bool MoveCrashData(const string& toDir, string& dumpfile,
- string& extrafile, string& memoryfile) {
- if (!UIEnsurePathExists(toDir)) {
- UIError(gStrings[ST_ERROR_CREATEDUMPDIR]);
- return false;
- }
-
- string newDump = toDir + UI_DIR_SEPARATOR + Basename(dumpfile);
- string newExtra = toDir + UI_DIR_SEPARATOR + Basename(extrafile);
- string newMemory = toDir + UI_DIR_SEPARATOR + Basename(memoryfile);
-
- if (!UIMoveFile(dumpfile, newDump)) {
- UIError(gStrings[ST_ERROR_DUMPFILEMOVE]);
- return false;
- }
-
- if (!UIMoveFile(extrafile, newExtra)) {
- UIError(gStrings[ST_ERROR_EXTRAFILEMOVE]);
- return false;
- }
-
- if (!memoryfile.empty()) {
- // Ignore errors from moving the memory file
- if (!UIMoveFile(memoryfile, newMemory)) {
- UIDeleteFile(memoryfile);
- newMemory.erase();
- }
- memoryfile = newMemory;
- }
-
- dumpfile = newDump;
- extrafile = newExtra;
-
- return true;
-}
-
-static bool AddSubmittedReport(const string& serverResponse) {
- StringTable responseItems;
- istringstream in(serverResponse);
- ReadStrings(in, responseItems, false);
-
- if (responseItems.find("StopSendingReportsFor") != responseItems.end()) {
- // server wants to tell us to stop sending reports for a certain version
- string reportPath = gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" +
- responseItems["StopSendingReportsFor"];
-
- ofstream* reportFile = UIOpenWrite(reportPath, ios::trunc);
- if (reportFile->is_open()) {
- // don't really care about the contents
- *reportFile << 1 << "\n";
- reportFile->close();
- }
- delete reportFile;
- }
-
- if (responseItems.find("Discarded") != responseItems.end()) {
- // server discarded this report... save it so the user can resubmit it
- // manually
- return false;
- }
-
- if (responseItems.find("CrashID") == responseItems.end()) return false;
-
- string submittedDir = gSettingsPath + UI_DIR_SEPARATOR + "submitted";
- if (!UIEnsurePathExists(submittedDir)) {
- return false;
- }
-
- string path =
- submittedDir + UI_DIR_SEPARATOR + responseItems["CrashID"] + ".txt";
-
- ofstream* file = UIOpenWrite(path, ios::trunc);
- if (!file->is_open()) {
- delete file;
- return false;
- }
-
- char buf[1024];
- snprintf(buf, 1024, gStrings["CrashID"].c_str(),
- responseItems["CrashID"].c_str());
- *file << buf << "\n";
-
- if (responseItems.find("ViewURL") != responseItems.end()) {
- snprintf(buf, 1024, gStrings["CrashDetailsURL"].c_str(),
- responseItems["ViewURL"].c_str());
- *file << buf << "\n";
- }
-
- file->close();
- delete file;
-
- WriteSubmissionEvent(Succeeded, responseItems["CrashID"]);
- return true;
-}
-
-void DeleteDump() {
- const char* noDelete = getenv("MOZ_CRASHREPORTER_NO_DELETE_DUMP");
- if (!noDelete || *noDelete == '\0') {
- if (!gReporterDumpFile.empty()) UIDeleteFile(gReporterDumpFile);
- if (!gExtraFile.empty()) UIDeleteFile(gExtraFile);
- if (!gMemoryFile.empty()) UIDeleteFile(gMemoryFile);
- }
-}
-
-void SendCompleted(bool success, const string& serverResponse) {
- if (success) {
- if (AddSubmittedReport(serverResponse)) {
- DeleteDump();
- } else {
- string directory = gReporterDumpFile;
- int slashpos = directory.find_last_of("/\\");
- if (slashpos < 2) return;
- directory.resize(slashpos);
- UIPruneSavedDumps(directory);
- WriteSubmissionEvent(Failed, "");
- }
- } else {
- WriteSubmissionEvent(Failed, "");
- }
-}
-
-static string ComputeDumpHash() {
-#ifdef XP_LINUX
- // On Linux we rely on the system-provided libcurl which uses nss so we have
- // to also use the system-provided nss instead of the ones we have bundled.
- const char* libnssNames[] = {
- "libnss3.so",
-# ifndef HAVE_64BIT_BUILD
- // 32-bit versions on 64-bit hosts
- "/usr/lib32/libnss3.so",
-# endif
- };
- void* lib = nullptr;
-
- for (const char* libname : libnssNames) {
- lib = dlopen(libname, RTLD_NOW);
-
- if (lib) {
- break;
- }
- }
-
- if (!lib) {
- return "";
- }
-
- SECStatus (*NSS_Initialize)(const char*, const char*, const char*,
- const char*, PRUint32);
- HASHContext* (*HASH_Create)(HASH_HashType);
- void (*HASH_Destroy)(HASHContext*);
- void (*HASH_Begin)(HASHContext*);
- void (*HASH_Update)(HASHContext*, const unsigned char*, unsigned int);
- void (*HASH_End)(HASHContext*, unsigned char*, unsigned int*, unsigned int);
-
- *(void**)(&NSS_Initialize) = dlsym(lib, "NSS_Initialize");
- *(void**)(&HASH_Create) = dlsym(lib, "HASH_Create");
- *(void**)(&HASH_Destroy) = dlsym(lib, "HASH_Destroy");
- *(void**)(&HASH_Begin) = dlsym(lib, "HASH_Begin");
- *(void**)(&HASH_Update) = dlsym(lib, "HASH_Update");
- *(void**)(&HASH_End) = dlsym(lib, "HASH_End");
-
- if (!HASH_Create || !HASH_Destroy || !HASH_Begin || !HASH_Update ||
- !HASH_End) {
- return "";
- }
-#endif
- // Minimal NSS initialization so we can use the hash functions
- const PRUint32 kNssFlags = NSS_INIT_READONLY | NSS_INIT_NOROOTINIT |
- NSS_INIT_NOMODDB | NSS_INIT_NOCERTDB;
- if (NSS_Initialize(nullptr, "", "", "", kNssFlags) != SECSuccess) {
- return "";
- }
-
- HASHContext* hashContext = HASH_Create(HASH_AlgSHA256);
-
- if (!hashContext) {
- return "";
- }
-
- HASH_Begin(hashContext);
-
- ifstream* f = UIOpenRead(gReporterDumpFile, ios::binary);
- bool error = false;
-
- // Read the minidump contents
- if (f->is_open()) {
- uint8_t buff[4096];
-
- do {
- f->read((char*)buff, sizeof(buff));
-
- if (f->bad()) {
- error = true;
- break;
- }
-
- HASH_Update(hashContext, buff, f->gcount());
- } while (!f->eof());
-
- f->close();
- } else {
- error = true;
- }
-
- delete f;
-
- // Finalize the hash computation
- uint8_t result[SHA256_LENGTH];
- uint32_t resultLen = 0;
-
- HASH_End(hashContext, result, &resultLen, SHA256_LENGTH);
-
- if (resultLen != SHA256_LENGTH) {
- error = true;
- }
-
- HASH_Destroy(hashContext);
-
- if (!error) {
- ostringstream hash;
-
- for (size_t i = 0; i < SHA256_LENGTH; i++) {
- hash << std::setw(2) << std::setfill('0') << std::hex
- << static_cast<unsigned int>(result[i]);
- }
-
- return hash.str();
- }
- return ""; // If we encountered an error, return an empty hash
-}
-
-string GetDumpLocalID() {
- string localId = Basename(gReporterDumpFile);
- string::size_type dot = localId.rfind('.');
-
- if (dot == string::npos) return "";
-
- return localId.substr(0, dot);
-}
-
-string GetProgramPath(const string& exename) {
- string path = gArgv[0];
- size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX);
- path.erase(pos);
-#ifdef XP_MACOSX
- // On macOS the crash reporter client is shipped as an application bundle
- // contained within Firefox' main application bundle. So when it's invoked
- // its current working directory looks like:
- // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/
- // The other applications we ship with Firefox are stored in the main bundle
- // (Firefox.app/Contents/MacOS/) so we we need to go back three directories
- // to reach them.
- path.erase(pos - 1);
- for (size_t i = 0; i < 3; i++) {
- pos = path.rfind(UI_DIR_SEPARATOR, pos - 1);
- }
-
- path.erase(pos + 1);
-#endif // XP_MACOSX
- path.append(exename + BIN_SUFFIX);
-
- return path;
-}
-
-} // namespace CrashReporter
-
-using namespace CrashReporter;
-
-Json::Value kEmptyJsonString("");
-
-void RewriteStrings(Json::Value& aExtraData) {
- // rewrite some UI strings with the values from the query parameters
- string product = aExtraData.get("ProductName", kEmptyJsonString).asString();
- Json::Value mozilla("Mozilla");
- string vendor = aExtraData.get("Vendor", mozilla).asString();
-
- char buf[4096];
- snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERVENDORTITLE].c_str(),
- vendor.c_str());
- gStrings[ST_CRASHREPORTERTITLE] = buf;
-
- string str = gStrings[ST_CRASHREPORTERPRODUCTERROR];
- // Only do the replacement here if the string has two
- // format specifiers to start. Otherwise
- // we assume it has the product name hardcoded.
- string::size_type pos = str.find("%s");
- if (pos != string::npos) pos = str.find("%s", pos + 2);
- if (pos != string::npos) {
- // Leave a format specifier for UIError to fill in
- snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERPRODUCTERROR].c_str(),
- product.c_str(), "%s");
- gStrings[ST_CRASHREPORTERERROR] = buf;
- } else {
- // product name is hardcoded
- gStrings[ST_CRASHREPORTERERROR] = str;
- }
-
- snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(),
- product.c_str());
- gStrings[ST_CRASHREPORTERDESCRIPTION] = buf;
-
- snprintf(buf, sizeof(buf), gStrings[ST_CHECKSUBMIT].c_str(), vendor.c_str());
- gStrings[ST_CHECKSUBMIT] = buf;
-
- snprintf(buf, sizeof(buf), gStrings[ST_RESTART].c_str(), product.c_str());
- gStrings[ST_RESTART] = buf;
-
- snprintf(buf, sizeof(buf), gStrings[ST_QUIT].c_str(), product.c_str());
- gStrings[ST_QUIT] = buf;
-
- snprintf(buf, sizeof(buf), gStrings[ST_ERROR_ENDOFLIFE].c_str(),
- product.c_str());
- gStrings[ST_ERROR_ENDOFLIFE] = buf;
-}
-
-bool CheckEndOfLifed(const Json::Value& aVersion) {
- if (!aVersion.isString()) {
- return false;
- }
-
- string reportPath =
- gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + aVersion.asString();
- return UIFileExists(reportPath);
-}
-
-int main(int argc, char** argv) {
- gArgc = argc;
- gArgv = argv;
-
- string autoSubmitEnv = UIGetEnv("MOZ_CRASHREPORTER_AUTO_SUBMIT");
- gAutoSubmit = !autoSubmitEnv.empty();
-
- if (!ReadConfig()) {
- UIError("Couldn't read configuration.");
- return 0;
- }
-
- if (!UIInit()) {
- return 0;
- }
-
- if (argc > 1) {
- gReporterDumpFile = argv[1];
- }
-
- if (gReporterDumpFile.empty()) {
- // no dump file specified, run the default UI
- if (!gAutoSubmit) {
- UIShowDefaultUI();
- }
- } else {
- // Start by running minidump analyzer to gather stack traces.
- string reporterDumpFile = gReporterDumpFile;
- vector<string> args = {reporterDumpFile};
- string dumpAllThreadsEnv = UIGetEnv("MOZ_CRASHREPORTER_DUMP_ALL_THREADS");
- if (!dumpAllThreadsEnv.empty()) {
- args.insert(args.begin(), "--full");
- }
- UIRunProgram(CrashReporter::GetProgramPath(UI_MINIDUMP_ANALYZER_FILENAME),
- args,
- /* wait */ true);
-
- // go ahead with the crash reporter
- gExtraFile = GetAdditionalFilename(gReporterDumpFile, kExtraDataExtension);
- if (gExtraFile.empty()) {
- UIError(gStrings[ST_ERROR_BADARGUMENTS]);
- return 0;
- }
-
- if (!UIFileExists(gExtraFile)) {
- UIError(gStrings[ST_ERROR_EXTRAFILEEXISTS]);
- return 0;
- }
-
- gMemoryFile =
- GetAdditionalFilename(gReporterDumpFile, kMemoryReportExtension);
- if (!UIFileExists(gMemoryFile)) {
- gMemoryFile.erase();
- }
-
- Json::Value extraData;
- if (!ReadExtraFile(gExtraFile, extraData)) {
- UIError(gStrings[ST_ERROR_EXTRAFILEREAD]);
- return 0;
- }
-
- if (!extraData.isMember("ProductName")) {
- UIError(gStrings[ST_ERROR_NOPRODUCTNAME]);
- return 0;
- }
-
- // There is enough information in the extra file to rewrite strings
- // to be product specific
- RewriteStrings(extraData);
-
- if (!extraData.isMember("ServerURL")) {
- UIError(gStrings[ST_ERROR_NOSERVERURL]);
- return 0;
- }
-
- // Hopefully the settings path exists in the environment. Try that before
- // asking the platform-specific code to guess.
- gSettingsPath = UIGetEnv("MOZ_CRASHREPORTER_DATA_DIRECTORY");
- if (gSettingsPath.empty()) {
- string product =
- extraData.get("ProductName", kEmptyJsonString).asString();
- string vendor = extraData.get("Vendor", kEmptyJsonString).asString();
-
- if (!UIGetSettingsPath(vendor, product, gSettingsPath)) {
- gSettingsPath.clear();
- }
- }
-
- if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) {
- UIError(gStrings[ST_ERROR_NOSETTINGSPATH]);
- return 0;
- }
-
- OpenLogFile();
-
- gEventsPath = UIGetEnv("MOZ_CRASHREPORTER_EVENTS_DIRECTORY");
- gPingPath = UIGetEnv("MOZ_CRASHREPORTER_PING_DIRECTORY");
-
- // Assemble and send the crash ping
- string hash = ComputeDumpHash();
-
- string pingUuid;
- SendCrashPing(extraData, hash, pingUuid, gPingPath);
- UpdateEventFile(extraData, hash, pingUuid);
-
- if (!UIFileExists(gReporterDumpFile)) {
- UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]);
- return 0;
- }
-
- string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending";
- if (!MoveCrashData(pendingDir, gReporterDumpFile, gExtraFile,
- gMemoryFile)) {
- return 0;
- }
-
- string sendURL = extraData.get("ServerURL", kEmptyJsonString).asString();
- // we don't need to actually send these
- extraData.removeMember("ServerURL");
- extraData.removeMember("StackTraces");
-
- extraData["SubmittedFrom"] = "Client";
- extraData["Throttleable"] = "1";
-
- // re-set XUL_APP_FILE for xulrunner wrapped apps
- const char* appfile = getenv("MOZ_CRASHREPORTER_RESTART_XUL_APP_FILE");
- if (appfile && *appfile) {
- const char prefix[] = "XUL_APP_FILE=";
- char* env = (char*)malloc(strlen(appfile) + strlen(prefix) + 1);
- if (!env) {
- UIError("Out of memory");
- return 0;
- }
- strcpy(env, prefix);
- strcat(env, appfile);
- putenv(env);
- free(env);
- }
-
- vector<string> restartArgs;
-
- if (!extraData.isMember("WindowsErrorReporting")) {
- // We relaunch the application associated with the client, but only when
- // we encountered a crash caught by the exception handler. Crashes handled
- // by WER are prevented from directly restarting the application.
- string programPath = GetProgramPath(MOZ_APP_NAME);
-#ifndef XP_WIN
- const char* moz_app_launcher = getenv("MOZ_APP_LAUNCHER");
- if (moz_app_launcher) {
- programPath = moz_app_launcher;
- }
-#endif // XP_WIN
-
- restartArgs.push_back(programPath);
-
- ostringstream paramName;
- int i = 1;
- paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++;
- const char* param = getenv(paramName.str().c_str());
- while (param && *param) {
- restartArgs.push_back(param);
-
- paramName.str("");
- paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++;
- param = getenv(paramName.str().c_str());
- }
- }
-
- // allow override of the server url via environment variable
- // XXX: remove this in the far future when our robot
- // masters force everyone to use XULRunner
- char* urlEnv = getenv("MOZ_CRASHREPORTER_URL");
- if (urlEnv && *urlEnv) {
- sendURL = urlEnv;
- }
-
- // see if this version has been end-of-lifed
-
- if (extraData.isMember("Version") &&
- CheckEndOfLifed(extraData["Version"])) {
- UIError(gStrings[ST_ERROR_ENDOFLIFE]);
- DeleteDump();
- return 0;
- }
-
- StringTable files;
- files["upload_file_minidump"] = gReporterDumpFile;
- if (!gMemoryFile.empty()) {
- files["memory_report"] = gMemoryFile;
- }
-
- if (!UIShowCrashUI(files, extraData, sendURL, restartArgs)) {
- DeleteDump();
- }
- }
-
- UIShutdown();
-
- return 0;
-}
-
-#if defined(XP_WIN) && !defined(__GNUC__)
-# include <windows.h>
-
-// We need WinMain in order to not be a console app. This function is unused
-// if we are a console application.
-int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR args, int) {
- // Remove everything except close window from the context menu
- {
- HKEY hkApp;
- RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0,
- nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
- &hkApp, nullptr);
- RegCloseKey(hkApp);
- if (RegCreateKeyExW(HKEY_CURRENT_USER,
- L"Software\\Classes\\Applications\\crashreporter.exe",
- 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr,
- &hkApp, nullptr) == ERROR_SUCCESS) {
- RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0);
- RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0);
- RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0);
- RegCloseKey(hkApp);
- }
- }
-
- char** argv = static_cast<char**>(malloc(__argc * sizeof(char*)));
- for (int i = 0; i < __argc; i++) {
- argv[i] = strdup(WideToUTF8(__wargv[i]).c_str());
- }
-
- // Do the real work.
- return main(__argc, argv);
-}
-#endif
diff --git a/toolkit/crashreporter/client/crashreporter.exe.manifest b/toolkit/crashreporter/client/crashreporter.exe.manifest
deleted file mode 100644
index 81aa1465c6..0000000000
--- a/toolkit/crashreporter/client/crashreporter.exe.manifest
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-<assemblyIdentity
- version="1.0.0.0"
- processorArchitecture="*"
- name="CrashReporter"
- type="win32"
-/>
-<description>Crash Reporter</description>
-<dependency>
- <dependentAssembly>
- <assemblyIdentity
- type="win32"
- name="Microsoft.Windows.Common-Controls"
- version="6.0.0.0"
- processorArchitecture="*"
- publicKeyToken="6595b64144ccf1df"
- language="*"
- />
- </dependentAssembly>
-</dependency>
-<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
- <ms_asmv3:security>
- <ms_asmv3:requestedPrivileges>
- <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
- </ms_asmv3:requestedPrivileges>
- </ms_asmv3:security>
-</ms_asmv3:trustInfo>
- <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
- <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
- <gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
- </ms_asmv3:windowsSettings>
- </ms_asmv3:application>
- <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
- <application>
- <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
- <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
- <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
- <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
- </application>
- </compatibility>
-</assembly>
diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h
deleted file mode 100644
index fa7085da18..0000000000
--- a/toolkit/crashreporter/client/crashreporter.h
+++ /dev/null
@@ -1,158 +0,0 @@
-/* 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/. */
-
-#ifndef CRASHREPORTER_H__
-#define CRASHREPORTER_H__
-
-#ifdef _MSC_VER
-# pragma warning(push)
-// Disable exception handler warnings.
-# pragma warning(disable : 4530)
-#endif
-
-#include <string>
-#include <map>
-#include <vector>
-#include <stdlib.h>
-#include <stdio.h>
-#include <fstream>
-
-#define MAX_COMMENT_LENGTH 10000
-
-#if defined(XP_WIN)
-
-# include <windows.h>
-
-# define UI_DIR_SEPARATOR "\\"
-
-std::string WideToUTF8(const std::wstring& wide, bool* success = 0);
-
-#else
-
-# define UI_DIR_SEPARATOR "/"
-
-#endif
-
-#include "json/json.h"
-
-#define UI_CRASH_REPORTER_FILENAME "crashreporter"
-#define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer"
-#define UI_PING_SENDER_FILENAME "pingsender"
-
-typedef std::map<std::string, std::string> StringTable;
-
-#define ST_CRASHREPORTERTITLE "CrashReporterTitle"
-#define ST_CRASHREPORTERVENDORTITLE "CrashReporterVendorTitle"
-#define ST_CRASHREPORTERERROR "CrashReporterErrorText"
-#define ST_CRASHREPORTERPRODUCTERROR "CrashReporterProductErrorText2"
-#define ST_CRASHREPORTERHEADER "CrashReporterSorry"
-#define ST_CRASHREPORTERDESCRIPTION "CrashReporterDescriptionText2"
-#define ST_CRASHREPORTERDEFAULT "CrashReporterDefault"
-#define ST_VIEWREPORT "Details"
-#define ST_VIEWREPORTTITLE "ViewReportTitle"
-#define ST_COMMENTGRAYTEXT "CommentGrayText"
-#define ST_EXTRAREPORTINFO "ExtraReportInfo"
-#define ST_CHECKSUBMIT "CheckSendReport"
-#define ST_CHECKURL "CheckIncludeURL"
-#define ST_REPORTPRESUBMIT "ReportPreSubmit2"
-#define ST_REPORTDURINGSUBMIT "ReportDuringSubmit2"
-#define ST_REPORTSUBMITSUCCESS "ReportSubmitSuccess"
-#define ST_SUBMITFAILED "ReportSubmitFailed"
-#define ST_QUIT "Quit2"
-#define ST_RESTART "Restart"
-#define ST_OK "Ok"
-#define ST_CLOSE "Close"
-
-#define ST_ERROR_BADARGUMENTS "ErrorBadArguments"
-#define ST_ERROR_EXTRAFILEEXISTS "ErrorExtraFileExists"
-#define ST_ERROR_EXTRAFILEREAD "ErrorExtraFileRead"
-#define ST_ERROR_EXTRAFILEMOVE "ErrorExtraFileMove"
-#define ST_ERROR_DUMPFILEEXISTS "ErrorDumpFileExists"
-#define ST_ERROR_DUMPFILEMOVE "ErrorDumpFileMove"
-#define ST_ERROR_NOPRODUCTNAME "ErrorNoProductName"
-#define ST_ERROR_NOSERVERURL "ErrorNoServerURL"
-#define ST_ERROR_NOSETTINGSPATH "ErrorNoSettingsPath"
-#define ST_ERROR_CREATEDUMPDIR "ErrorCreateDumpDir"
-#define ST_ERROR_ENDOFLIFE "ErrorEndOfLife"
-
-//=============================================================================
-// implemented in crashreporter.cpp and ping.cpp
-//=============================================================================
-
-namespace CrashReporter {
-extern StringTable gStrings;
-extern std::string gSettingsPath;
-extern std::string gEventsPath;
-extern int gArgc;
-extern char** gArgv;
-extern bool gAutoSubmit;
-
-void UIError(const std::string& message);
-
-// The UI finished sending the report
-void SendCompleted(bool success, const std::string& serverResponse);
-
-bool ReadStrings(std::istream& in, StringTable& strings, bool unescape);
-bool ReadStringsFromFile(const std::string& path, StringTable& strings,
- bool unescape);
-void LogMessage(const std::string& message);
-void DeleteDump();
-
-std::string GetDumpLocalID();
-std::string GetProgramPath(const std::string& exename);
-
-// Telemetry ping
-bool SendCrashPing(Json::Value& extra, const std::string& hash,
- std::string& pingUuid, const std::string& pingDir);
-
-static const unsigned int kSaveCount = 10;
-} // namespace CrashReporter
-
-//=============================================================================
-// implemented in the platform-specific files
-//=============================================================================
-
-bool UIInit();
-void UIShutdown();
-
-// Run the UI for when the app was launched without a dump file
-void UIShowDefaultUI();
-
-// Run the UI for when the app was launched with a dump file
-// Return true if the user sent (or tried to send) the crash report,
-// false if they chose not to, and it should be deleted.
-bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
- const std::string& sendURL,
- const std::vector<std::string>& restartArgs);
-
-void UIError_impl(const std::string& message);
-
-bool UIGetIniPath(std::string& path);
-bool UIGetSettingsPath(const std::string& vendor, const std::string& product,
- std::string& settingsPath);
-bool UIEnsurePathExists(const std::string& path);
-bool UIFileExists(const std::string& path);
-bool UIMoveFile(const std::string& oldfile, const std::string& newfile);
-bool UIDeleteFile(const std::string& oldfile);
-std::ifstream* UIOpenRead(const std::string& filename,
- std::ios_base::openmode mode);
-std::ofstream* UIOpenWrite(const std::string& filename,
- std::ios_base::openmode mode);
-void UIPruneSavedDumps(const std::string& directory);
-
-// Run the program specified by exename, passing it the parameters in arg.
-// If wait is true, wait for the program to terminate execution before
-// returning. Returns true if the program was launched correctly, false
-// otherwise.
-bool UIRunProgram(const std::string& exename,
- const std::vector<std::string>& args, bool wait = false);
-
-// Read the environment variable specified by name
-std::string UIGetEnv(const std::string& name);
-
-#ifdef _MSC_VER
-# pragma warning(pop)
-#endif
-
-#endif
diff --git a/toolkit/crashreporter/client/crashreporter.ico b/toolkit/crashreporter/client/crashreporter.ico
deleted file mode 100644
index 29ac3c6189..0000000000
--- a/toolkit/crashreporter/client/crashreporter.ico
+++ /dev/null
Binary files differ
diff --git a/toolkit/crashreporter/client/crashreporter.rc b/toolkit/crashreporter/client/crashreporter.rc
deleted file mode 100755
index f6042bf2e5..0000000000
--- a/toolkit/crashreporter/client/crashreporter.rc
+++ /dev/null
@@ -1,143 +0,0 @@
-/* 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/. */
-
-// Microsoft Visual C++ generated resource script.
-//
-#include "resource.h"
-
-#define APSTUDIO_READONLY_SYMBOLS
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 2 resource.
-//
-#include "winresrc.h"
-
-/////////////////////////////////////////////////////////////////////////////
-#undef APSTUDIO_READONLY_SYMBOLS
-
-/////////////////////////////////////////////////////////////////////////////
-// English (U.S.) resources
-
-#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
-#ifdef _WIN32
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-#pragma code_page(1252)
-#endif //_WIN32
-
-#ifdef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// TEXTINCLUDE
-//
-
-1 TEXTINCLUDE
-BEGIN
- "resource.h\0"
-END
-
-2 TEXTINCLUDE
-BEGIN
- "#include ""winresrc.h""\r\n"
- "\0"
-END
-
-3 TEXTINCLUDE
-BEGIN
- "\r\n"
- "\0"
-END
-
-#endif // APSTUDIO_INVOKED
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Icon
-//
-
-// Icon with lowest ID value placed first to ensure application icon
-// remains consistent on all systems.
-IDI_MAINICON ICON "crashreporter.ico"
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// AVI
-//
-
-IDR_THROBBER AVI "Throbber-small.avi"
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Dialog
-//
-
-IDD_SENDDIALOG DIALOGEX 0, 0, 241, 187
-STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
-EXSTYLE WS_EX_APPWINDOW
-CAPTION "Sending Crash Report..."
-FONT 8, "MS Shell Dlg", 400, 0, 0x1
-BEGIN
- CONTROL "",IDC_DESCRIPTIONTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY,8,7,226,12,WS_EX_TRANSPARENT
- CONTROL "tell mozilla about this crash so they can fix it",IDC_SUBMITREPORTCHECK,
- "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,25,222,10
- CHECKBOX "details...",IDC_VIEWREPORTBUTTON,24,40,54,14,BS_PUSHLIKE
- EDITTEXT IDC_COMMENTTEXT,24,59,210,43,ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL
- CONTROL "include the address of the page i was on",IDC_INCLUDEURLCHECK,
- "Button",BS_AUTOCHECKBOX | WS_TABSTOP,24,107,210,10
- CONTROL "",IDC_THROBBER,"SysAnimate32",ACS_TRANSPARENT | NOT WS_VISIBLE | WS_TABSTOP,4,152,16,16
- LTEXT "your crash report will be submitted when you restart",IDC_PROGRESSTEXT,24,152,210,10,SS_NOPREFIX
- DEFPUSHBUTTON "restart firefox",IDC_RESTARTBUTTON,84,166,68,14
- PUSHBUTTON "quit without sending",IDC_CLOSEBUTTON,157,166,77,14
-END
-
-IDD_VIEWREPORTDIALOG DIALOGEX 0, 0, 208, 126
-STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION
-CAPTION "view report"
-FONT 8, "MS Shell Dlg", 400, 0, 0x1
-BEGIN
- CONTROL "",IDC_VIEWREPORTTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP,7,7,194,92
- DEFPUSHBUTTON "OK",IDOK,151,105,50,14
-END
-
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// DESIGNINFO
-//
-
-#ifdef APSTUDIO_INVOKED
-GUIDELINES DESIGNINFO
-BEGIN
- IDD_SENDDIALOG, DIALOG
- BEGIN
- LEFTMARGIN, 8
- RIGHTMARGIN, 234
- TOPMARGIN, 7
- BOTTOMMARGIN, 180
- END
-
- IDD_VIEWREPORTDIALOG, DIALOG
- BEGIN
- LEFTMARGIN, 7
- RIGHTMARGIN, 201
- TOPMARGIN, 7
- BOTTOMMARGIN, 119
- END
-END
-#endif // APSTUDIO_INVOKED
-
-#endif // English (U.S.) resources
-/////////////////////////////////////////////////////////////////////////////
-
-
-
-#ifndef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 3 resource.
-//
-
-
-/////////////////////////////////////////////////////////////////////////////
-#endif // not APSTUDIO_INVOKED
-
diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.cpp b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp
deleted file mode 100644
index b9a957cfd9..0000000000
--- a/toolkit/crashreporter/client/crashreporter_gtk_common.cpp
+++ /dev/null
@@ -1,361 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#include <unistd.h>
-#include <dlfcn.h>
-#include <errno.h>
-#include <glib.h>
-#include <gtk/gtk.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <gdk/gdkkeysyms.h>
-
-#include <algorithm>
-#include <string>
-#include <vector>
-
-#include "common/linux/http_upload.h"
-#include "crashreporter.h"
-#include "crashreporter_gtk_common.h"
-
-#ifndef GDK_KEY_Escape
-# define GDK_KEY_Escape GDK_Escape
-#endif
-
-using std::string;
-using std::vector;
-
-using namespace CrashReporter;
-
-GtkWidget* gWindow = 0;
-GtkWidget* gSubmitReportCheck = 0;
-GtkWidget* gIncludeURLCheck = 0;
-GtkWidget* gThrobber = 0;
-GtkWidget* gProgressLabel = 0;
-GtkWidget* gCloseButton = 0;
-GtkWidget* gRestartButton = 0;
-
-bool gInitialized = false;
-bool gDidTrySend = false;
-StringTable gFiles;
-Json::Value gQueryParameters;
-string gHttpProxy;
-string gAuth;
-string gCACertificateFile;
-string gSendURL;
-string gURLParameter;
-vector<string> gRestartArgs;
-GThread* gSendThreadID;
-
-// From crashreporter_linux.cpp
-void SendReport();
-void DisableGUIAndSendReport();
-void TryInitGnome();
-void UpdateSubmit();
-
-static bool RestartApplication() {
- char** argv = reinterpret_cast<char**>(
- malloc(sizeof(char*) * (gRestartArgs.size() + 1)));
-
- if (!argv) return false;
-
- unsigned int i;
- for (i = 0; i < gRestartArgs.size(); i++) {
- argv[i] = (char*)gRestartArgs[i].c_str();
- }
- argv[i] = 0;
-
- pid_t pid = fork();
- if (pid == -1) {
- free(argv);
- return false;
- }
-
- if (pid == 0) {
- (void)execv(argv[0], argv);
- _exit(1);
- }
-
- free(argv);
-
- return true;
-}
-
-// Quit the app, used as a timeout callback
-gboolean CloseApp(gpointer data) {
- if (!gAutoSubmit) {
- gtk_main_quit();
- }
- g_thread_join(gSendThreadID);
- return FALSE;
-}
-
-static gboolean ReportCompleted(gpointer success) {
- gtk_widget_hide(gThrobber);
- string str =
- success ? gStrings[ST_REPORTSUBMITSUCCESS] : gStrings[ST_SUBMITFAILED];
- gtk_label_set_text(GTK_LABEL(gProgressLabel), str.c_str());
- g_timeout_add(5000, CloseApp, 0);
- return FALSE;
-}
-
-#define HTTP_PROXY_DIR "/system/http_proxy"
-
-void LoadProxyinfo() {
- class GConfClient;
- typedef GConfClient* (*_gconf_default_fn)();
- typedef gboolean (*_gconf_bool_fn)(GConfClient*, const gchar*, GError**);
- typedef gint (*_gconf_int_fn)(GConfClient*, const gchar*, GError**);
- typedef gchar* (*_gconf_string_fn)(GConfClient*, const gchar*, GError**);
-
- if (getenv("http_proxy"))
- return; // libcurl can use the value from the environment
-
- static void* gconfLib = dlopen("libgconf-2.so.4", RTLD_LAZY);
- if (!gconfLib) return;
-
- _gconf_default_fn gconf_client_get_default =
- (_gconf_default_fn)dlsym(gconfLib, "gconf_client_get_default");
- _gconf_bool_fn gconf_client_get_bool =
- (_gconf_bool_fn)dlsym(gconfLib, "gconf_client_get_bool");
- _gconf_int_fn gconf_client_get_int =
- (_gconf_int_fn)dlsym(gconfLib, "gconf_client_get_int");
- _gconf_string_fn gconf_client_get_string =
- (_gconf_string_fn)dlsym(gconfLib, "gconf_client_get_string");
-
- if (!(gconf_client_get_default && gconf_client_get_bool &&
- gconf_client_get_int && gconf_client_get_string)) {
- dlclose(gconfLib);
- return;
- }
-
- GConfClient* conf = gconf_client_get_default();
-
- if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_http_proxy", nullptr)) {
- gint port;
- gchar *host = nullptr, *httpproxy = nullptr;
-
- host = gconf_client_get_string(conf, HTTP_PROXY_DIR "/host", nullptr);
- port = gconf_client_get_int(conf, HTTP_PROXY_DIR "/port", nullptr);
-
- if (port && host && *host != '\0') {
- httpproxy = g_strdup_printf("http://%s:%d/", host, port);
- gHttpProxy = httpproxy;
- }
-
- g_free(host);
- g_free(httpproxy);
-
- if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_authentication",
- nullptr)) {
- gchar *user, *password, *auth = nullptr;
-
- user = gconf_client_get_string(
- conf, HTTP_PROXY_DIR "/authentication_user", nullptr);
- password = gconf_client_get_string(
- conf, HTTP_PROXY_DIR "/authentication_password", nullptr);
-
- if (user && password) {
- auth = g_strdup_printf("%s:%s", user, password);
- gAuth = auth;
- }
-
- g_free(user);
- g_free(password);
- g_free(auth);
- }
- }
-
- g_object_unref(conf);
-
- // Don't dlclose gconfLib as libORBit-2 uses atexit().
-}
-
-gpointer SendThread(gpointer args) {
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- string parameters(writeString(builder, gQueryParameters));
-
- string response, error;
- long response_code;
-
- bool success = google_breakpad::HTTPUpload::SendRequest(
- gSendURL, parameters, gFiles, gHttpProxy, gAuth, gCACertificateFile,
- &response, &response_code, &error);
- if (success) {
- LogMessage("Crash report submitted successfully");
- } else {
- LogMessage("Crash report submission failed: " + error);
- }
-
- SendCompleted(success, response);
-
- if (!gAutoSubmit) {
- // Apparently glib is threadsafe, and will schedule this
- // on the main thread, see:
- // http://library.gnome.org/devel/gtk-faq/stable/x499.html
- g_idle_add(ReportCompleted, (gpointer)success);
- }
-
- return nullptr;
-}
-
-gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData) {
- SaveSettings();
- gtk_main_quit();
- return TRUE;
-}
-
-gboolean check_escape(GtkWidget* window, GdkEventKey* event,
- gpointer userData) {
- if (event->keyval == GDK_KEY_Escape) {
- gtk_main_quit();
- return TRUE;
- }
- return FALSE;
-}
-
-static void MaybeSubmitReport() {
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) {
- gDidTrySend = true;
- DisableGUIAndSendReport();
- } else {
- gtk_main_quit();
- }
-}
-
-void CloseClicked(GtkButton* button, gpointer userData) {
- SaveSettings();
- MaybeSubmitReport();
-}
-
-void RestartClicked(GtkButton* button, gpointer userData) {
- SaveSettings();
- RestartApplication();
- MaybeSubmitReport();
-}
-
-static void UpdateURL() {
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck))) {
- gQueryParameters["URL"] = gURLParameter;
- } else {
- gQueryParameters.removeMember("URL");
- }
-}
-
-void SubmitReportChecked(GtkButton* sender, gpointer userData) {
- UpdateSubmit();
-}
-
-void IncludeURLClicked(GtkButton* sender, gpointer userData) { UpdateURL(); }
-
-/* === Crashreporter UI Functions === */
-
-bool UIInit() {
- // breakpad probably left us with blocked signals, unblock them here
- sigset_t signals, old;
- sigfillset(&signals);
- sigprocmask(SIG_UNBLOCK, &signals, &old);
-
- // tell glib we're going to use threads
- g_thread_init(nullptr);
-
- if (gtk_init_check(&gArgc, &gArgv)) {
- gInitialized = true;
-
- if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
- gtk_widget_set_default_direction(GTK_TEXT_DIR_RTL);
-
- return true;
- }
-
- return false;
-}
-
-void UIShowDefaultUI() {
- GtkWidget* errorDialog = gtk_message_dialog_new(
- nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
- gStrings[ST_CRASHREPORTERDEFAULT].c_str());
-
- gtk_window_set_title(GTK_WINDOW(errorDialog),
- gStrings[ST_CRASHREPORTERTITLE].c_str());
- gtk_dialog_run(GTK_DIALOG(errorDialog));
-}
-
-void UIError_impl(const string& message) {
- if (!gInitialized) {
- // Didn't initialize, this is the best we can do
- printf("Error: %s\n", message.c_str());
- return;
- }
-
- GtkWidget* errorDialog =
- gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
- GTK_BUTTONS_CLOSE, "%s", message.c_str());
-
- gtk_window_set_title(GTK_WINDOW(errorDialog),
- gStrings[ST_CRASHREPORTERTITLE].c_str());
- gtk_dialog_run(GTK_DIALOG(errorDialog));
-}
-
-bool UIGetIniPath(string& path) {
- path = gArgv[0];
- path.append(".ini");
-
- return true;
-}
-
-/*
- * Settings are stored in ~/.vendor/product, or
- * ~/.product if vendor is empty.
- */
-bool UIGetSettingsPath(const string& vendor, const string& product,
- string& settingsPath) {
- char* home = getenv("HOME");
-
- if (!home) return false;
-
- settingsPath = home;
- settingsPath += "/.";
- if (!vendor.empty()) {
- string lc_vendor;
- std::transform(vendor.begin(), vendor.end(), back_inserter(lc_vendor),
- (int (*)(int))std::tolower);
- settingsPath += lc_vendor + "/";
- }
- string lc_product;
- std::transform(product.begin(), product.end(), back_inserter(lc_product),
- (int (*)(int))std::tolower);
- settingsPath += lc_product + "/Crash Reports";
- return true;
-}
-
-bool UIMoveFile(const string& file, const string& newfile) {
- if (!rename(file.c_str(), newfile.c_str())) return true;
- if (errno != EXDEV) return false;
-
- // use system /bin/mv instead, time to fork
- pid_t pID = vfork();
- if (pID < 0) {
- // Failed to fork
- return false;
- }
- if (pID == 0) {
- char* const args[4] = {const_cast<char*>("mv"), strdup(file.c_str()),
- strdup(newfile.c_str()), 0};
- if (args[1] && args[2]) execve("/bin/mv", args, 0);
- free(args[1]);
- free(args[2]);
- exit(-1);
- }
- int status;
- waitpid(pID, &status, 0);
- return UIFileExists(newfile);
-}
diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.h b/toolkit/crashreporter/client/crashreporter_gtk_common.h
deleted file mode 100644
index 208c7ba6b0..0000000000
--- a/toolkit/crashreporter/client/crashreporter_gtk_common.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/* 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/. */
-
-#ifndef CRASHREPORTER_GTK_COMMON_H__
-#define CRASHREPORTER_GTK_COMMON_H__
-
-#include <glib.h>
-#include <gtk/gtk.h>
-
-#include <string>
-#include <vector>
-
-#include "json/json.h"
-
-const char kIniFile[] = "crashreporter.ini";
-
-extern GtkWidget* gWindow;
-extern GtkWidget* gSubmitReportCheck;
-extern GtkWidget* gIncludeURLCheck;
-extern GtkWidget* gThrobber;
-extern GtkWidget* gProgressLabel;
-extern GtkWidget* gCloseButton;
-extern GtkWidget* gRestartButton;
-
-extern std::vector<std::string> gRestartArgs;
-extern GThread* gSendThreadID;
-
-extern bool gInitialized;
-extern bool gDidTrySend;
-extern StringTable gFiles;
-extern Json::Value gQueryParameters;
-extern std::string gHttpProxy;
-extern std::string gAuth;
-extern std::string gCACertificateFile;
-extern std::string gSendURL;
-extern std::string gURLParameter;
-
-void LoadProxyinfo();
-gboolean CloseApp(gpointer data);
-gpointer SendThread(gpointer args);
-gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData);
-gboolean check_escape(GtkWidget* window, GdkEventKey* event, gpointer data);
-void SubmitReportChecked(GtkButton* sender, gpointer userData);
-void IncludeURLClicked(GtkButton* sender, gpointer userData);
-void CloseClicked(GtkButton* button, gpointer userData);
-void RestartClicked(GtkButton* button, gpointer userData);
-void SaveSettings(void);
-
-#endif // CRASHREPORTER_GTK_COMMON_H__
diff --git a/toolkit/crashreporter/client/crashreporter_linux.cpp b/toolkit/crashreporter/client/crashreporter_linux.cpp
deleted file mode 100644
index 644b34654e..0000000000
--- a/toolkit/crashreporter/client/crashreporter_linux.cpp
+++ /dev/null
@@ -1,525 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#include <dlfcn.h>
-#include <fcntl.h>
-#include <glib.h>
-#include <gtk/gtk.h>
-#include <string.h>
-
-#include <cctype>
-
-#include "crashreporter.h"
-#include "crashreporter_gtk_common.h"
-
-#define LABEL_MAX_CHAR_WIDTH 48
-
-using std::ios;
-using std::string;
-using std::vector;
-
-using namespace CrashReporter;
-
-static GtkWidget* gViewReportButton = 0;
-static GtkWidget* gCommentTextLabel = 0;
-static GtkWidget* gCommentText = 0;
-
-static bool gCommentFieldHint = true;
-
-// handle from dlopen'ing libgnome
-static void* gnomeLib = nullptr;
-// handle from dlopen'ing libgnomeui
-static void* gnomeuiLib = nullptr;
-
-static void LoadSettings() {
- /*
- * NOTE! This code needs to stay in sync with the preference checking
- * code in in nsExceptionHandler.cpp.
- */
-
- bool includeURL = true;
- bool submitReport = true;
- StringTable settings;
- if (ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true)) {
- if (settings.find("IncludeURL") != settings.end()) {
- includeURL = settings["IncludeURL"][0] != '0';
- }
- if (settings.find("SubmitReport") != settings.end()) {
- submitReport = settings["SubmitReport"][0] != '0';
- }
- }
-
- if (gIncludeURLCheck) {
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck),
- includeURL);
- }
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck),
- submitReport);
-}
-
-static string Escape(const string& str) {
- string ret;
- for (auto c : str) {
- if (c == '\\') {
- ret += "\\\\";
- } else if (c == '\n') {
- ret += "\\n";
- } else if (c == '\t') {
- ret += "\\t";
- } else {
- ret.push_back(c);
- }
- }
-
- return ret;
-}
-
-static bool WriteStrings(std::ostream& out, const string& header,
- StringTable& strings, bool escape) {
- out << "[" << header << "]\n";
- for (const auto& iter : strings) {
- out << iter.first << "=";
- if (escape) {
- out << Escape(iter.second);
- } else {
- out << iter.second;
- }
-
- out << '\n';
- }
-
- return true;
-}
-
-static bool WriteStringsToFile(const string& path, const string& header,
- StringTable& strings, bool escape) {
- std::ofstream* f = UIOpenWrite(path, ios::trunc);
- bool success = false;
- if (f->is_open()) {
- success = WriteStrings(*f, header, strings, escape);
- f->close();
- }
-
- delete f;
- return success;
-}
-
-void SaveSettings() {
- /*
- * NOTE! This code needs to stay in sync with the preference setting
- * code in in nsExceptionHandler.cpp.
- */
-
- StringTable settings;
-
- ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true);
- if (gIncludeURLCheck != 0)
- settings["IncludeURL"] =
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck)) ? "1"
- : "0";
- settings["SubmitReport"] =
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck)) ? "1"
- : "0";
-
- WriteStringsToFile(gSettingsPath + "/" + kIniFile, "Crash Reporter", settings,
- true);
-}
-
-void SendReport() {
- LoadProxyinfo();
-
- // spawn a thread to do the sending
- gSendThreadID = g_thread_create(SendThread, nullptr, TRUE, nullptr);
-}
-
-void DisableGUIAndSendReport() {
- // disable all our gui controls, show the throbber + change the progress text
- gtk_widget_set_sensitive(gSubmitReportCheck, FALSE);
- gtk_widget_set_sensitive(gViewReportButton, FALSE);
- gtk_widget_set_sensitive(gCommentText, FALSE);
- if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE);
- gtk_widget_set_sensitive(gCloseButton, FALSE);
- if (gRestartButton) gtk_widget_set_sensitive(gRestartButton, FALSE);
- gtk_widget_show_all(gThrobber);
- gtk_label_set_text(GTK_LABEL(gProgressLabel),
- gStrings[ST_REPORTDURINGSUBMIT].c_str());
-
- SendReport();
-}
-
-static void ShowReportInfo(GtkTextView* viewReportTextView) {
- GtkTextBuffer* buffer = gtk_text_view_get_buffer(viewReportTextView);
-
- GtkTextIter start, end;
- gtk_text_buffer_get_start_iter(buffer, &start);
- gtk_text_buffer_get_end_iter(buffer, &end);
-
- gtk_text_buffer_delete(buffer, &start, &end);
-
- for (Json::ValueConstIterator iter = gQueryParameters.begin();
- iter != gQueryParameters.end(); ++iter) {
- gtk_text_buffer_insert(buffer, &end, iter.name().c_str(),
- iter.name().length());
- gtk_text_buffer_insert(buffer, &end, ": ", -1);
- string value;
- if (iter->isString()) {
- value = iter->asString();
- } else {
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- value = writeString(builder, *iter);
- }
- gtk_text_buffer_insert(buffer, &end, value.c_str(), value.length());
- gtk_text_buffer_insert(buffer, &end, "\n", -1);
- }
-
- gtk_text_buffer_insert(buffer, &end, "\n", -1);
- gtk_text_buffer_insert(buffer, &end, gStrings[ST_EXTRAREPORTINFO].c_str(),
- -1);
-}
-
-void UpdateSubmit() {
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) {
- gtk_widget_set_sensitive(gViewReportButton, TRUE);
- gtk_widget_set_sensitive(gCommentText, TRUE);
- if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, TRUE);
- gtk_label_set_text(GTK_LABEL(gProgressLabel),
- gStrings[ST_REPORTPRESUBMIT].c_str());
- } else {
- gtk_widget_set_sensitive(gViewReportButton, FALSE);
- gtk_widget_set_sensitive(gCommentText, FALSE);
- if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE);
- gtk_label_set_text(GTK_LABEL(gProgressLabel), "");
- }
-}
-
-static void ViewReportClicked(GtkButton* button, gpointer userData) {
- GtkDialog* dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
- gStrings[ST_VIEWREPORTTITLE].c_str(), GTK_WINDOW(gWindow),
- GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_OK, nullptr));
-
- GtkWidget* scrolled = gtk_scrolled_window_new(0, 0);
- gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(dialog)),
- scrolled);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
- gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled),
- GTK_SHADOW_IN);
- gtk_widget_set_vexpand(scrolled, TRUE);
-
- GtkWidget* viewReportTextView = gtk_text_view_new();
- gtk_container_add(GTK_CONTAINER(scrolled), viewReportTextView);
- gtk_text_view_set_editable(GTK_TEXT_VIEW(viewReportTextView), FALSE);
- gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(viewReportTextView), GTK_WRAP_WORD);
- gtk_widget_set_size_request(GTK_WIDGET(viewReportTextView), -1, 100);
-
- ShowReportInfo(GTK_TEXT_VIEW(viewReportTextView));
-
- gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK);
- gtk_widget_set_size_request(GTK_WIDGET(dialog), 400, 200);
- gtk_widget_show_all(GTK_WIDGET(dialog));
- gtk_dialog_run(dialog);
- gtk_widget_destroy(GTK_WIDGET(dialog));
-}
-
-static void CommentChanged(GtkTextBuffer* buffer, gpointer userData) {
- GtkTextIter start, end;
- gtk_text_buffer_get_start_iter(buffer, &start);
- gtk_text_buffer_get_end_iter(buffer, &end);
- const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
- if (comment[0] == '\0' || gCommentFieldHint) {
- gQueryParameters.removeMember("Comments");
- } else {
- gQueryParameters["Comments"] = comment;
- }
-}
-
-static void CommentInsert(GtkTextBuffer* buffer, GtkTextIter* location,
- gchar* text, gint len, gpointer userData) {
- GtkTextIter start, end;
- gtk_text_buffer_get_start_iter(buffer, &start);
- gtk_text_buffer_get_end_iter(buffer, &end);
- const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
-
- // limit to 500 bytes in utf-8
- if (strlen(comment) + len > MAX_COMMENT_LENGTH) {
- g_signal_stop_emission_by_name(buffer, "insert-text");
- }
-}
-
-static void UpdateHintText(GtkWidget* widget, gboolean gainedFocus,
- bool* hintShowing, const char* hintText) {
- GtkTextBuffer* buffer = nullptr;
- if (GTK_IS_TEXT_VIEW(widget))
- buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
-
- if (gainedFocus) {
- if (*hintShowing) {
- if (buffer == nullptr) { // sort of cheating
- gtk_entry_set_text(GTK_ENTRY(widget), "");
- } else { // GtkTextView
- gtk_text_buffer_set_text(buffer, "", 0);
- }
- gtk_widget_modify_text(widget, GTK_STATE_NORMAL, nullptr);
- *hintShowing = false;
- }
- } else {
- // lost focus
- const char* text = nullptr;
- if (buffer == nullptr) {
- text = gtk_entry_get_text(GTK_ENTRY(widget));
- } else {
- GtkTextIter start, end;
- gtk_text_buffer_get_start_iter(buffer, &start);
- gtk_text_buffer_get_end_iter(buffer, &end);
- text = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
- }
-
- if (text == nullptr || text[0] == '\0') {
- *hintShowing = true;
-
- if (buffer == nullptr) {
- gtk_entry_set_text(GTK_ENTRY(widget), hintText);
- } else {
- gtk_text_buffer_set_text(buffer, hintText, -1);
- }
-
- gtk_widget_modify_text(
- widget, GTK_STATE_NORMAL,
- &gtk_widget_get_style(widget)->text[GTK_STATE_INSENSITIVE]);
- }
- }
-}
-
-static gboolean CommentFocusChange(GtkWidget* widget, GdkEventFocus* event,
- gpointer userData) {
- UpdateHintText(widget, event->in, &gCommentFieldHint,
- gStrings[ST_COMMENTGRAYTEXT].c_str());
-
- return FALSE;
-}
-
-typedef struct _GnomeProgram GnomeProgram;
-typedef struct _GnomeModuleInfo GnomeModuleInfo;
-typedef GnomeProgram* (*_gnome_program_init_fn)(const char*, const char*,
- const GnomeModuleInfo*, int,
- char**, const char*, ...);
-typedef const GnomeModuleInfo* (*_libgnomeui_module_info_get_fn)();
-
-void TryInitGnome() {
- gnomeLib = dlopen("libgnome-2.so.0", RTLD_LAZY);
- if (!gnomeLib) return;
-
- gnomeuiLib = dlopen("libgnomeui-2.so.0", RTLD_LAZY);
- if (!gnomeuiLib) return;
-
- _gnome_program_init_fn gnome_program_init =
- (_gnome_program_init_fn)(dlsym(gnomeLib, "gnome_program_init"));
- _libgnomeui_module_info_get_fn libgnomeui_module_info_get =
- (_libgnomeui_module_info_get_fn)(dlsym(gnomeuiLib,
- "libgnomeui_module_info_get"));
-
- if (gnome_program_init && libgnomeui_module_info_get) {
- gnome_program_init("crashreporter", "1.0", libgnomeui_module_info_get(),
- gArgc, gArgv, nullptr);
- }
-}
-
-/* === Crashreporter UI Functions === */
-
-/*
- * Anything not listed here is in crashreporter_gtk_common.cpp:
- * UIInit
- * UIShowDefaultUI
- * UIError_impl
- * UIGetIniPath
- * UIGetSettingsPath
- * UIEnsurePathExists
- * UIFileExists
- * UIMoveFile
- * UIDeleteFile
- * UIOpenRead
- * UIOpenWrite
- */
-
-void UIShutdown() {
- if (gnomeuiLib) dlclose(gnomeuiLib);
- // Don't dlclose gnomeLib as libgnomevfs and libORBit-2 use atexit().
-}
-
-bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
- const string& sendURL, const vector<string>& restartArgs) {
- gFiles = files;
- gQueryParameters = queryParameters;
- gSendURL = sendURL;
- gRestartArgs = restartArgs;
- if (gQueryParameters.isMember("URL")) {
- gURLParameter = gQueryParameters["URL"].asString();
- }
-
- if (gAutoSubmit) {
- SendReport();
- CloseApp(nullptr);
- return true;
- }
-
- gWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- gtk_window_set_title(GTK_WINDOW(gWindow),
- gStrings[ST_CRASHREPORTERTITLE].c_str());
- gtk_window_set_resizable(GTK_WINDOW(gWindow), FALSE);
- gtk_window_set_position(GTK_WINDOW(gWindow), GTK_WIN_POS_CENTER);
- gtk_container_set_border_width(GTK_CONTAINER(gWindow), 12);
- g_signal_connect(gWindow, "delete-event", G_CALLBACK(WindowDeleted), 0);
- g_signal_connect(gWindow, "key_press_event", G_CALLBACK(check_escape),
- nullptr);
-
- GtkWidget* vbox = gtk_vbox_new(FALSE, 6);
- gtk_container_add(GTK_CONTAINER(gWindow), vbox);
-
- GtkWidget* titleLabel = gtk_label_new("");
- gtk_box_pack_start(GTK_BOX(vbox), titleLabel, FALSE, FALSE, 0);
- gtk_misc_set_alignment(GTK_MISC(titleLabel), 0, 0.5);
- char* markup =
- g_strdup_printf("<b>%s</b>", gStrings[ST_CRASHREPORTERHEADER].c_str());
- gtk_label_set_markup(GTK_LABEL(titleLabel), markup);
- g_free(markup);
-
- GtkWidget* descriptionLabel =
- gtk_label_new(gStrings[ST_CRASHREPORTERDESCRIPTION].c_str());
- gtk_box_pack_start(GTK_BOX(vbox), descriptionLabel, TRUE, TRUE, 0);
- // force the label to line wrap
- gtk_label_set_max_width_chars(GTK_LABEL(descriptionLabel),
- LABEL_MAX_CHAR_WIDTH);
- gtk_label_set_line_wrap(GTK_LABEL(descriptionLabel), TRUE);
- gtk_label_set_selectable(GTK_LABEL(descriptionLabel), TRUE);
- gtk_misc_set_alignment(GTK_MISC(descriptionLabel), 0, 0.5);
-
- // this is honestly how they suggest you indent a section
- GtkWidget* indentBox = gtk_hbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(vbox), indentBox, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(indentBox), gtk_label_new(""), FALSE, FALSE, 6);
-
- GtkWidget* innerVBox1 = gtk_vbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(indentBox), innerVBox1, TRUE, TRUE, 0);
-
- gSubmitReportCheck =
- gtk_check_button_new_with_label(gStrings[ST_CHECKSUBMIT].c_str());
- gtk_box_pack_start(GTK_BOX(innerVBox1), gSubmitReportCheck, FALSE, FALSE, 0);
- g_signal_connect(gSubmitReportCheck, "clicked",
- G_CALLBACK(SubmitReportChecked), 0);
-
- // indent again, below the "submit report" checkbox
- GtkWidget* indentBox2 = gtk_hbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(innerVBox1), indentBox2, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(indentBox2), gtk_label_new(""), FALSE, FALSE, 6);
-
- GtkWidget* innerVBox = gtk_vbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(indentBox2), innerVBox, TRUE, TRUE, 0);
- gtk_box_set_spacing(GTK_BOX(innerVBox), 6);
-
- GtkWidget* viewReportButtonBox = gtk_hbutton_box_new();
- gtk_box_pack_start(GTK_BOX(innerVBox), viewReportButtonBox, FALSE, FALSE, 0);
- gtk_box_set_spacing(GTK_BOX(viewReportButtonBox), 6);
- gtk_button_box_set_layout(GTK_BUTTON_BOX(viewReportButtonBox),
- GTK_BUTTONBOX_START);
-
- gViewReportButton =
- gtk_button_new_with_label(gStrings[ST_VIEWREPORT].c_str());
- gtk_box_pack_start(GTK_BOX(viewReportButtonBox), gViewReportButton, FALSE,
- FALSE, 0);
- g_signal_connect(gViewReportButton, "clicked", G_CALLBACK(ViewReportClicked),
- 0);
-
- GtkWidget* scrolled = gtk_scrolled_window_new(0, 0);
- gtk_container_add(GTK_CONTAINER(innerVBox), scrolled);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
- GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
- gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled),
- GTK_SHADOW_IN);
- gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolled),
- 100);
-
- gCommentTextLabel = gtk_label_new(gStrings[ST_COMMENTGRAYTEXT].c_str());
- gCommentText = gtk_text_view_new();
- gtk_label_set_mnemonic_widget(GTK_LABEL(gCommentTextLabel), gCommentText);
- gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(gCommentText), FALSE);
- g_signal_connect(gCommentText, "focus-in-event",
- G_CALLBACK(CommentFocusChange), 0);
- g_signal_connect(gCommentText, "focus-out-event",
- G_CALLBACK(CommentFocusChange), 0);
-
- GtkTextBuffer* commentBuffer =
- gtk_text_view_get_buffer(GTK_TEXT_VIEW(gCommentText));
- g_signal_connect(commentBuffer, "changed", G_CALLBACK(CommentChanged), 0);
- g_signal_connect(commentBuffer, "insert-text", G_CALLBACK(CommentInsert), 0);
-
- gtk_container_add(GTK_CONTAINER(scrolled), gCommentText);
- gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gCommentText), GTK_WRAP_WORD_CHAR);
- gtk_widget_set_size_request(GTK_WIDGET(gCommentText), -1, 100);
-
- if (gQueryParameters.isMember("URL")) {
- gIncludeURLCheck =
- gtk_check_button_new_with_label(gStrings[ST_CHECKURL].c_str());
- gtk_box_pack_start(GTK_BOX(innerVBox), gIncludeURLCheck, FALSE, FALSE, 0);
- g_signal_connect(gIncludeURLCheck, "clicked", G_CALLBACK(IncludeURLClicked),
- 0);
- // on by default
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck), TRUE);
- }
-
- GtkWidget* progressBox = gtk_hbox_new(FALSE, 6);
- gtk_box_pack_start(GTK_BOX(vbox), progressBox, TRUE, TRUE, 0);
-
- // Get the throbber image from alongside the executable
- char* dir = g_path_get_dirname(gArgv[0]);
- char* path = g_build_filename(dir, "Throbber-small.gif", nullptr);
- g_free(dir);
- gThrobber = gtk_image_new_from_file(path);
- gtk_box_pack_start(GTK_BOX(progressBox), gThrobber, FALSE, FALSE, 0);
-
- gProgressLabel = gtk_label_new(gStrings[ST_REPORTPRESUBMIT].c_str());
- gtk_box_pack_start(GTK_BOX(progressBox), gProgressLabel, TRUE, TRUE, 0);
- // force the label to line wrap
- gtk_label_set_max_width_chars(GTK_LABEL(gProgressLabel),
- LABEL_MAX_CHAR_WIDTH);
- gtk_label_set_line_wrap(GTK_LABEL(gProgressLabel), TRUE);
-
- GtkWidget* buttonBox = gtk_hbutton_box_new();
- gtk_box_pack_end(GTK_BOX(vbox), buttonBox, FALSE, FALSE, 0);
- gtk_box_set_spacing(GTK_BOX(buttonBox), 6);
- gtk_button_box_set_layout(GTK_BUTTON_BOX(buttonBox), GTK_BUTTONBOX_END);
-
- gCloseButton = gtk_button_new_with_label(gStrings[ST_QUIT].c_str());
- gtk_box_pack_start(GTK_BOX(buttonBox), gCloseButton, FALSE, FALSE, 0);
- gtk_widget_set_can_default(gCloseButton, TRUE);
- g_signal_connect(gCloseButton, "clicked", G_CALLBACK(CloseClicked), 0);
-
- gRestartButton = 0;
- if (!restartArgs.empty()) {
- gRestartButton = gtk_button_new_with_label(gStrings[ST_RESTART].c_str());
- gtk_box_pack_start(GTK_BOX(buttonBox), gRestartButton, FALSE, FALSE, 0);
- gtk_widget_set_can_default(gRestartButton, TRUE);
- g_signal_connect(gRestartButton, "clicked", G_CALLBACK(RestartClicked), 0);
- }
-
- gtk_widget_grab_focus(gSubmitReportCheck);
-
- gtk_widget_grab_default(gRestartButton ? gRestartButton : gCloseButton);
-
- LoadSettings();
-
- UpdateSubmit();
-
- UpdateHintText(gCommentText, FALSE, &gCommentFieldHint,
- gStrings[ST_COMMENTGRAYTEXT].c_str());
-
- gtk_widget_show_all(gWindow);
- // stick this here to avoid the show_all above...
- gtk_widget_hide(gThrobber);
-
- gtk_main();
-
- return gDidTrySend;
-}
diff --git a/toolkit/crashreporter/client/crashreporter_osx.h b/toolkit/crashreporter/client/crashreporter_osx.h
deleted file mode 100644
index d9aeddb4b0..0000000000
--- a/toolkit/crashreporter/client/crashreporter_osx.h
+++ /dev/null
@@ -1,108 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#ifndef CRASHREPORTER_OSX_H__
-#define CRASHREPORTER_OSX_H__
-
-#include <Cocoa/Cocoa.h>
-#include "HTTPMultipartUpload.h"
-#include "crashreporter.h"
-#include "json/json.h"
-
-// Defined below
-@class TextViewWithPlaceHolder;
-
-@interface CrashReporterUI : NSObject {
- IBOutlet NSWindow* mWindow;
-
- /* Crash reporter view */
- IBOutlet NSTextField* mHeaderLabel;
- IBOutlet NSTextField* mDescriptionLabel;
- IBOutlet NSButton* mViewReportButton;
- IBOutlet NSScrollView* mCommentScrollView;
- IBOutlet TextViewWithPlaceHolder* mCommentText;
- IBOutlet NSButton* mSubmitReportButton;
- IBOutlet NSButton* mIncludeURLButton;
- IBOutlet NSButton* mEmailMeButton;
- IBOutlet NSTextField* mEmailText;
- IBOutlet NSButton* mCloseButton;
- IBOutlet NSButton* mRestartButton;
- IBOutlet NSProgressIndicator* mProgressIndicator;
- IBOutlet NSTextField* mProgressText;
-
- /* Error view */
- IBOutlet NSView* mErrorView;
- IBOutlet NSTextField* mErrorHeaderLabel;
- IBOutlet NSTextField* mErrorLabel;
- IBOutlet NSButton* mErrorCloseButton;
-
- /* For "show info" alert */
- IBOutlet NSWindow* mViewReportWindow;
- IBOutlet NSTextView* mViewReportTextView;
- IBOutlet NSButton* mViewReportOkButton;
-
- HTTPMultipartUpload* mPost;
-}
-
-- (void)showCrashUI:(const StringTable&)files
- queryParameters:(const Json::Value&)queryParameters
- sendURL:(const std::string&)sendURL;
-- (void)showErrorUI:(const std::string&)message;
-- (void)showReportInfo;
-- (void)maybeSubmitReport;
-- (void)closeMeDown:(id)unused;
-
-- (IBAction)submitReportClicked:(id)sender;
-- (IBAction)viewReportClicked:(id)sender;
-- (IBAction)viewReportOkClicked:(id)sender;
-- (IBAction)closeClicked:(id)sender;
-- (IBAction)restartClicked:(id)sender;
-- (IBAction)includeURLClicked:(id)sender;
-
-- (void)textDidChange:(NSNotification*)aNotification;
-- (BOOL)textView:(NSTextView*)aTextView
- shouldChangeTextInRange:(NSRange)affectedCharRange
- replacementString:(NSString*)replacementString;
-
-- (void)doInitialResizing;
-- (float)setStringFitVertically:(NSControl*)control
- string:(NSString*)str
- resizeWindow:(BOOL)resizeWindow;
-- (void)setView:(NSView*)v animate:(BOOL)animate;
-- (void)enableControls:(BOOL)enabled;
-- (void)updateSubmit;
-- (void)updateURL;
-- (void)updateEmail;
-- (void)sendReport;
-- (bool)setupPost;
-- (void)uploadThread:(HTTPMultipartUpload*)post;
-- (void)uploadComplete:(NSData*)data;
-
-- (BOOL)applicationShouldTerminateAfterLastWindowClosed:
- (NSApplication*)theApplication;
-- (void)applicationWillTerminate:(NSNotification*)aNotification;
-
-@end
-
-/*
- * Subclass NSTextView to provide a text view with placeholder text.
- * Also provide a setEnabled implementation.
- */
-@interface TextViewWithPlaceHolder : NSTextView {
- NSMutableAttributedString* mPlaceHolderString;
-}
-
-- (BOOL)becomeFirstResponder;
-- (void)drawRect:(NSRect)rect;
-- (BOOL)resignFirstResponder;
-- (void)setPlaceholder:(NSString*)placeholder;
-- (void)insertTab:(id)sender;
-- (void)insertBacktab:(id)sender;
-- (void)setEnabled:(BOOL)enabled;
-- (void)dealloc;
-
-@end
-
-#endif
diff --git a/toolkit/crashreporter/client/crashreporter_osx.mm b/toolkit/crashreporter/client/crashreporter_osx.mm
deleted file mode 100644
index b6d5d8ac6f..0000000000
--- a/toolkit/crashreporter/client/crashreporter_osx.mm
+++ /dev/null
@@ -1,805 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#import <Cocoa/Cocoa.h>
-#import <CoreFoundation/CoreFoundation.h>
-#include "crashreporter.h"
-#include "crashreporter_osx.h"
-#include <crt_externs.h>
-#include <spawn.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <fcntl.h>
-#include <sstream>
-
-using std::ostringstream;
-using std::string;
-using std::vector;
-
-using namespace CrashReporter;
-
-static NSAutoreleasePool* gMainPool;
-static CrashReporterUI* gUI = 0;
-static StringTable gFiles;
-static Json::Value gQueryParameters;
-static string gURLParameter;
-static string gSendURL;
-static vector<string> gRestartArgs;
-static bool gDidTrySend = false;
-static bool gRTLlayout = false;
-
-static cpu_type_t pref_cpu_types[2] = {
-#if defined(__i386__)
- CPU_TYPE_X86,
-#elif defined(__x86_64__)
- CPU_TYPE_X86_64,
-#elif defined(__ppc__)
- CPU_TYPE_POWERPC,
-#elif defined(__aarch64__)
- CPU_TYPE_ARM64,
-#endif
- CPU_TYPE_ANY};
-
-#define NSSTR(s) [NSString stringWithUTF8String:(s).c_str()]
-
-static NSString* Str(const char* aName) {
- string str = gStrings[aName];
- if (str.empty()) str = "?";
- return NSSTR(str);
-}
-
-static bool RestartApplication() {
- vector<char*> argv(gRestartArgs.size() + 1);
-
- posix_spawnattr_t spawnattr;
- if (posix_spawnattr_init(&spawnattr) != 0) {
- return false;
- }
-
- // Set spawn attributes.
- size_t attr_count = sizeof(pref_cpu_types) / sizeof(pref_cpu_types[0]);
- size_t attr_ocount = 0;
- if (posix_spawnattr_setbinpref_np(&spawnattr, attr_count, pref_cpu_types,
- &attr_ocount) != 0 ||
- attr_ocount != attr_count) {
- posix_spawnattr_destroy(&spawnattr);
- return false;
- }
-
- unsigned int i;
- for (i = 0; i < gRestartArgs.size(); i++) {
- argv[i] = (char*)gRestartArgs[i].c_str();
- }
- argv[i] = 0;
-
- char** env = NULL;
- char*** nsEnv = _NSGetEnviron();
- if (nsEnv) env = *nsEnv;
- int result = posix_spawnp(NULL, argv[0], NULL, &spawnattr, &argv[0], env);
-
- posix_spawnattr_destroy(&spawnattr);
-
- return result == 0;
-}
-
-@implementation CrashReporterUI
-
-- (void)awakeFromNib {
- gUI = self;
- [mWindow center];
-
- [mWindow setTitle:[[NSBundle mainBundle]
- objectForInfoDictionaryKey:@"CFBundleName"]];
- [NSApp activateIgnoringOtherApps:YES];
-}
-
-- (void)showCrashUI:(const StringTable&)files
- queryParameters:(const Json::Value&)queryParameters
- sendURL:(const string&)sendURL {
- gFiles = files;
- gQueryParameters = queryParameters;
- gSendURL = sendURL;
-
- if (gAutoSubmit) {
- gDidTrySend = true;
- [self sendReport];
- return;
- }
-
- [mWindow setTitle:Str(ST_CRASHREPORTERTITLE)];
- [mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
-
- NSRect viewReportFrame = [mViewReportButton frame];
- [mViewReportButton setTitle:Str(ST_VIEWREPORT)];
- [mViewReportButton sizeToFit];
- if (gRTLlayout) {
- // sizeToFit will keep the left side fixed, so realign
- float oldWidth = viewReportFrame.size.width;
- viewReportFrame = [mViewReportButton frame];
- viewReportFrame.origin.x += oldWidth - viewReportFrame.size.width;
- [mViewReportButton setFrame:viewReportFrame];
- }
-
- [mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)];
- [mIncludeURLButton setTitle:Str(ST_CHECKURL)];
- [mViewReportOkButton setTitle:Str(ST_OK)];
-
- [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)];
- if (gRTLlayout) [mCommentText toggleBaseWritingDirection:self];
-
- if (gQueryParameters.isMember("URL")) {
- // save the URL value in case the checkbox gets unchecked
- gURLParameter = gQueryParameters["URL"].asString();
- } else {
- // no URL specified, hide checkbox
- [mIncludeURLButton removeFromSuperview];
- // shrink window to fit
- NSRect frame = [mWindow frame];
- NSRect includeURLFrame = [mIncludeURLButton frame];
- NSRect emailFrame = [mEmailMeButton frame];
- int buttonMask = [mViewReportButton autoresizingMask];
- int checkMask = [mSubmitReportButton autoresizingMask];
- int commentScrollMask = [mCommentScrollView autoresizingMask];
-
- [mViewReportButton setAutoresizingMask:NSViewMinYMargin];
- [mSubmitReportButton setAutoresizingMask:NSViewMinYMargin];
- [mCommentScrollView setAutoresizingMask:NSViewMinYMargin];
-
- // remove all the space in between
- frame.size.height -= includeURLFrame.origin.y - emailFrame.origin.y;
- [mWindow setFrame:frame display:true animate:NO];
-
- [mViewReportButton setAutoresizingMask:buttonMask];
- [mSubmitReportButton setAutoresizingMask:checkMask];
- [mCommentScrollView setAutoresizingMask:commentScrollMask];
- }
-
- // resize some buttons horizontally and possibly some controls vertically
- [self doInitialResizing];
-
- // load default state of submit checkbox
- // we don't just do this via IB because we want the default to be
- // off a certain percentage of the time
- BOOL submitChecked = YES;
- NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
- if (nil != [userDefaults objectForKey:@"submitReport"]) {
- submitChecked = [userDefaults boolForKey:@"submitReport"];
- } else {
- [userDefaults setBool:submitChecked forKey:@"submitReport"];
- }
- [mSubmitReportButton setState:(submitChecked ? NSOnState : NSOffState)];
-
- // load default state of include URL checkbox
- BOOL includeChecked = YES;
- if (nil != [userDefaults objectForKey:@"IncludeURL"]) {
- includeChecked = [userDefaults boolForKey:@"IncludeURL"];
- } else {
- [userDefaults setBool:includeChecked forKey:@"IncludeURL"];
- }
- [mIncludeURLButton setState:(includeChecked ? NSOnState : NSOffState)];
-
- [self updateSubmit];
- [self updateURL];
- [self updateEmail];
-
- [mWindow makeKeyAndOrderFront:nil];
-}
-
-- (void)showErrorUI:(const string&)message {
- [self setView:mErrorView animate:NO];
-
- [mErrorHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
- [self setStringFitVertically:mErrorLabel
- string:NSSTR(message)
- resizeWindow:YES];
- [mErrorCloseButton setTitle:Str(ST_OK)];
-
- [mErrorCloseButton setKeyEquivalent:@"\r"];
- [mWindow makeFirstResponder:mErrorCloseButton];
- [mWindow makeKeyAndOrderFront:nil];
-}
-
-- (void)showReportInfo {
- NSDictionary* boldAttr = @{
- NSFontAttributeName :
- [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]],
- NSForegroundColorAttributeName : NSColor.textColor,
- };
- NSDictionary* normalAttr = @{
- NSFontAttributeName :
- [NSFont systemFontOfSize:[NSFont smallSystemFontSize]],
- NSForegroundColorAttributeName : NSColor.textColor,
- };
-
- [mViewReportTextView setString:@""];
- for (Json::ValueConstIterator iter = gQueryParameters.begin();
- iter != gQueryParameters.end(); ++iter) {
- NSAttributedString* key =
- [[NSAttributedString alloc] initWithString:NSSTR(iter.name() + ": ")
- attributes:boldAttr];
- string str;
- if (iter->isString()) {
- str = iter->asString();
- } else {
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- str = writeString(builder, *iter);
- }
- NSAttributedString* value =
- [[NSAttributedString alloc] initWithString:NSSTR(str + "\n")
- attributes:normalAttr];
- [[mViewReportTextView textStorage] appendAttributedString:key];
- [[mViewReportTextView textStorage] appendAttributedString:value];
- [key release];
- [value release];
- }
-
- NSAttributedString* extra = [[NSAttributedString alloc]
- initWithString:NSSTR("\n" + gStrings[ST_EXTRAREPORTINFO])
- attributes:normalAttr];
- [[mViewReportTextView textStorage] appendAttributedString:extra];
- [extra release];
-}
-
-- (void)maybeSubmitReport {
- if ([mSubmitReportButton state] == NSOnState) {
- [self setStringFitVertically:mProgressText
- string:Str(ST_REPORTDURINGSUBMIT)
- resizeWindow:YES];
- // disable all the controls
- [self enableControls:NO];
- [mSubmitReportButton setEnabled:NO];
- [mRestartButton setEnabled:NO];
- [mCloseButton setEnabled:NO];
- [mProgressIndicator startAnimation:self];
- gDidTrySend = true;
- [self sendReport];
- } else {
- [NSApp terminate:self];
- }
-}
-
-- (void)closeMeDown:(id)unused {
- [NSApp terminate:self];
-}
-
-- (IBAction)submitReportClicked:(id)sender {
- [self updateSubmit];
- NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
- [userDefaults setBool:([mSubmitReportButton state] == NSOnState)
- forKey:@"submitReport"];
- [userDefaults synchronize];
-}
-
-- (IBAction)viewReportClicked:(id)sender {
- [self showReportInfo];
- [NSApp beginSheet:mViewReportWindow
- modalForWindow:mWindow
- modalDelegate:nil
- didEndSelector:nil
- contextInfo:nil];
-}
-
-- (IBAction)viewReportOkClicked:(id)sender {
- [mViewReportWindow orderOut:nil];
- [NSApp endSheet:mViewReportWindow];
-}
-
-- (IBAction)closeClicked:(id)sender {
- [self maybeSubmitReport];
-}
-
-- (IBAction)restartClicked:(id)sender {
- RestartApplication();
- [self maybeSubmitReport];
-}
-
-- (IBAction)includeURLClicked:(id)sender {
- [self updateURL];
- NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
- [userDefaults setBool:([mIncludeURLButton state] == NSOnState)
- forKey:@"IncludeURL"];
- [userDefaults synchronize];
-}
-
-- (void)textDidChange:(NSNotification*)aNotification {
- // update comment parameter
- if ([[[mCommentText textStorage] mutableString] length] > 0)
- gQueryParameters["Comments"] =
- [[[mCommentText textStorage] mutableString] UTF8String];
- else
- gQueryParameters.removeMember("Comments");
-}
-
-// Limit the comment field to 500 bytes in UTF-8
-- (BOOL)textView:(NSTextView*)aTextView
- shouldChangeTextInRange:(NSRange)affectedCharRange
- replacementString:(NSString*)replacementString {
- // current string length + replacement text length - replaced range length
- if (([[aTextView string] lengthOfBytesUsingEncoding:NSUTF8StringEncoding] +
- [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] -
- [[[aTextView string] substringWithRange:affectedCharRange]
- lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) >
- MAX_COMMENT_LENGTH) {
- return NO;
- }
- return YES;
-}
-
-- (void)doInitialResizing {
- NSRect windowFrame = [mWindow frame];
- NSRect restartFrame = [mRestartButton frame];
- NSRect closeFrame = [mCloseButton frame];
- // resize close button to fit text
- float oldCloseWidth = closeFrame.size.width;
- [mCloseButton setTitle:Str(ST_QUIT)];
- [mCloseButton sizeToFit];
- closeFrame = [mCloseButton frame];
- // move close button left if it grew
- if (!gRTLlayout) {
- closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth;
- }
-
- if (gRestartArgs.size() == 0) {
- [mRestartButton removeFromSuperview];
- if (!gRTLlayout) {
- closeFrame.origin.x = restartFrame.origin.x +
- (restartFrame.size.width - closeFrame.size.width);
- } else {
- closeFrame.origin.x = restartFrame.origin.x;
- }
- [mCloseButton setFrame:closeFrame];
- [mCloseButton setKeyEquivalent:@"\r"];
- } else {
- [mRestartButton setTitle:Str(ST_RESTART)];
- // resize "restart" button
- float oldRestartWidth = restartFrame.size.width;
- [mRestartButton sizeToFit];
- restartFrame = [mRestartButton frame];
- if (!gRTLlayout) {
- // move left by the amount that the button grew
- restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
- closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
- } else {
- // shift the close button right in RTL
- closeFrame.origin.x += restartFrame.size.width - oldRestartWidth;
- }
- [mRestartButton setFrame:restartFrame];
- [mCloseButton setFrame:closeFrame];
- // possibly resize window if both buttons no longer fit
- // leave 20 px from either side of the window, and 12 px
- // between the buttons
- float neededWidth =
- closeFrame.size.width + restartFrame.size.width + 2 * 20 + 12;
-
- if (neededWidth > windowFrame.size.width) {
- windowFrame.size.width = neededWidth;
- [mWindow setFrame:windowFrame display:true animate:NO];
- }
- [mRestartButton setKeyEquivalent:@"\r"];
- }
-
- NSButton* checkboxes[] = {mSubmitReportButton, mIncludeURLButton};
-
- for (auto checkbox : checkboxes) {
- NSRect frame = [checkbox frame];
- [checkbox sizeToFit];
- if (gRTLlayout) {
- // sizeToFit will keep the left side fixed, so realign
- float oldWidth = frame.size.width;
- frame = [checkbox frame];
- frame.origin.x += oldWidth - frame.size.width;
- [checkbox setFrame:frame];
- }
- // keep existing spacing on left side, + 20 px spare on right
- float neededWidth =
- frame.origin.x + checkbox.intrinsicContentSize.width + 20;
- if (neededWidth > windowFrame.size.width) {
- windowFrame.size.width = neededWidth;
- [mWindow setFrame:windowFrame display:true animate:NO];
- }
- }
-
- // do this down here because we may have made the window wider
- // up above
- [self setStringFitVertically:mDescriptionLabel
- string:Str(ST_CRASHREPORTERDESCRIPTION)
- resizeWindow:YES];
-
- // now pin all the controls (except quit/submit) in place,
- // if we lengthen the window after this, it's just to lengthen
- // the progress text, so nothing above that text should move.
- NSView* views[] = {mSubmitReportButton, mViewReportButton,
- mCommentScrollView, mIncludeURLButton,
- mProgressIndicator, mProgressText};
- for (auto view : views) {
- [view setAutoresizingMask:NSViewMinYMargin];
- }
-}
-
-- (float)setStringFitVertically:(NSControl*)control
- string:(NSString*)str
- resizeWindow:(BOOL)resizeWindow {
- // hack to make the text field grow vertically
- NSRect frame = [control frame];
- float oldHeight = frame.size.height;
-
- frame.size.height = 10000;
- NSSize oldCellSize = [[control cell] cellSizeForBounds:frame];
- [control setStringValue:str];
- NSSize newCellSize = [[control cell] cellSizeForBounds:frame];
-
- float delta = newCellSize.height - oldCellSize.height;
- frame.origin.y -= delta;
- frame.size.height = oldHeight + delta;
- [control setFrame:frame];
-
- if (resizeWindow) {
- NSRect frame = [mWindow frame];
- frame.origin.y -= delta;
- frame.size.height += delta;
- [mWindow setFrame:frame display:true animate:NO];
- }
-
- return delta;
-}
-
-- (void)setView:(NSView*)v animate:(BOOL)animate {
- NSRect frame = [mWindow frame];
-
- NSRect oldViewFrame = [[mWindow contentView] frame];
- NSRect newViewFrame = [v frame];
-
- frame.origin.y += oldViewFrame.size.height - newViewFrame.size.height;
- frame.size.height -= oldViewFrame.size.height - newViewFrame.size.height;
-
- frame.origin.x += oldViewFrame.size.width - newViewFrame.size.width;
- frame.size.width -= oldViewFrame.size.width - newViewFrame.size.width;
-
- [mWindow setContentView:v];
- [mWindow setFrame:frame display:true animate:animate];
-}
-
-- (void)enableControls:(BOOL)enabled {
- [mViewReportButton setEnabled:enabled];
- [mIncludeURLButton setEnabled:enabled];
- [mCommentText setEnabled:enabled];
- [mCommentScrollView setHasVerticalScroller:enabled];
-}
-
-- (void)updateSubmit {
- if ([mSubmitReportButton state] == NSOnState) {
- [self setStringFitVertically:mProgressText
- string:Str(ST_REPORTPRESUBMIT)
- resizeWindow:YES];
- [mProgressText setHidden:NO];
- // enable all the controls
- [self enableControls:YES];
- } else {
- // not submitting, disable all the controls under
- // the submit checkbox, and hide the status text
- [mProgressText setHidden:YES];
- [self enableControls:NO];
- }
-}
-
-- (void)updateURL {
- if ([mIncludeURLButton state] == NSOnState && !gURLParameter.empty()) {
- gQueryParameters["URL"] = gURLParameter;
- } else {
- gQueryParameters.removeMember("URL");
- }
-}
-
-- (void)updateEmail {
- // In order to remove the email fields, we have to edit the .nib files which
- // we can't do with current xcode so we make them hidden; updating the
- // crashreporter interface for mac is covered in bug #1696164
- [mEmailMeButton setHidden:YES];
- [mEmailText setHidden:YES];
-}
-
-- (void)sendReport {
- if (![self setupPost]) {
- LogMessage("Crash report submission failed: could not set up POST data");
-
- if (gAutoSubmit) {
- [NSApp terminate:self];
- }
-
- [self setStringFitVertically:mProgressText
- string:Str(ST_SUBMITFAILED)
- resizeWindow:YES];
- // quit after 5 seconds
- [self performSelector:@selector(closeMeDown:)
- withObject:nil
- afterDelay:5.0];
- }
-
- [NSThread detachNewThreadSelector:@selector(uploadThread:)
- toTarget:self
- withObject:mPost];
-}
-
-- (bool)setupPost {
- NSURL* url = [NSURL
- URLWithString:[NSSTR(gSendURL) stringByAddingPercentEscapesUsingEncoding:
- NSUTF8StringEncoding]];
- if (!url) return false;
-
- mPost = [[HTTPMultipartUpload alloc] initWithURL:url];
- if (!mPost) return false;
-
- for (StringTable::const_iterator i = gFiles.begin(); i != gFiles.end(); i++) {
- [mPost addFileAtPath:NSSTR(i->second) name:NSSTR(i->first)];
- }
-
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- string output = writeString(builder, gQueryParameters).append("\r\n");
- NSMutableString* parameters =
- [[NSMutableString alloc] initWithUTF8String:output.c_str()];
-
- [mPost setParameters:parameters];
- [parameters release];
-
- return true;
-}
-
-- (void)uploadComplete:(NSData*)data {
- NSHTTPURLResponse* response = [mPost response];
- [mPost release];
-
- bool success;
- string reply;
- if (!data || !response || [response statusCode] != 200) {
- success = false;
- reply = "";
-
- // if data is nil, we probably logged an error in uploadThread
- if (data != nil && response != nil) {
- ostringstream message;
- message << "Crash report submission failed: server returned status "
- << [response statusCode];
- LogMessage(message.str());
- }
- } else {
- success = true;
- LogMessage("Crash report submitted successfully");
-
- NSString* encodingName = [response textEncodingName];
- NSStringEncoding encoding;
- if (encodingName) {
- encoding = CFStringConvertEncodingToNSStringEncoding(
- CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName));
- } else {
- encoding = NSISOLatin1StringEncoding;
- }
- NSString* r = [[NSString alloc] initWithData:data encoding:encoding];
- reply = [r UTF8String];
- [r release];
- }
-
- SendCompleted(success, reply);
-
- if (gAutoSubmit) {
- [NSApp terminate:self];
- }
-
- [mProgressIndicator stopAnimation:self];
- if (success) {
- [self setStringFitVertically:mProgressText
- string:Str(ST_REPORTSUBMITSUCCESS)
- resizeWindow:YES];
- } else {
- [self setStringFitVertically:mProgressText
- string:Str(ST_SUBMITFAILED)
- resizeWindow:YES];
- }
- // quit after 5 seconds
- [self performSelector:@selector(closeMeDown:) withObject:nil afterDelay:5.0];
-}
-
-- (void)uploadThread:(HTTPMultipartUpload*)post {
- NSAutoreleasePool* autoreleasepool = [[NSAutoreleasePool alloc] init];
- NSError* error = nil;
- NSData* data = [post send:&error];
- if (error) {
- data = nil;
- NSString* errorDesc = [error localizedDescription];
- string message = [errorDesc UTF8String];
- LogMessage("Crash report submission failed: " + message);
- }
-
- [self performSelectorOnMainThread:@selector(uploadComplete:)
- withObject:data
- waitUntilDone:YES];
-
- [autoreleasepool release];
-}
-
-// to get auto-quit when we close the window
-- (BOOL)applicationShouldTerminateAfterLastWindowClosed:
- (NSApplication*)theApplication {
- return YES;
-}
-
-- (void)applicationWillTerminate:(NSNotification*)aNotification {
- // since we use [NSApp terminate:] we never return to main,
- // so do our cleanup here
- if (!gDidTrySend) DeleteDump();
-}
-
-@end
-
-@implementation TextViewWithPlaceHolder
-
-- (BOOL)becomeFirstResponder {
- [self setNeedsDisplay:YES];
- return [super becomeFirstResponder];
-}
-
-- (void)drawRect:(NSRect)rect {
- [super drawRect:rect];
- if (mPlaceHolderString && [[self string] isEqualToString:@""] &&
- self != [[self window] firstResponder])
- [mPlaceHolderString drawInRect:[self frame]];
-}
-
-- (BOOL)resignFirstResponder {
- [self setNeedsDisplay:YES];
- return [super resignFirstResponder];
-}
-
-- (void)setPlaceholder:(NSString*)placeholder {
- NSColor* txtColor = [NSColor disabledControlTextColor];
- NSDictionary* txtDict = [NSDictionary
- dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName,
- nil];
- mPlaceHolderString =
- [[NSMutableAttributedString alloc] initWithString:placeholder
- attributes:txtDict];
- if (gRTLlayout)
- [mPlaceHolderString setAlignment:NSTextAlignmentRight
- range:NSMakeRange(0, [placeholder length])];
-}
-
-- (void)insertTab:(id)sender {
- // don't actually want to insert tabs, just tab to next control
- [[self window] selectNextKeyView:sender];
-}
-
-- (void)insertBacktab:(id)sender {
- [[self window] selectPreviousKeyView:sender];
-}
-
-- (void)setEnabled:(BOOL)enabled {
- [self setSelectable:enabled];
- [self setEditable:enabled];
- if (![[self string] isEqualToString:@""]) {
- NSAttributedString* colorString;
- NSColor* txtColor;
- if (enabled)
- txtColor = [NSColor textColor];
- else
- txtColor = [NSColor disabledControlTextColor];
- NSDictionary* txtDict = [NSDictionary
- dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName,
- nil];
- colorString = [[NSAttributedString alloc] initWithString:[self string]
- attributes:txtDict];
- [[self textStorage] setAttributedString:colorString];
- [self setInsertionPointColor:txtColor];
- [colorString release];
- }
-}
-
-- (void)dealloc {
- [mPlaceHolderString release];
- [super dealloc];
-}
-
-@end
-
-/* === Crashreporter UI Functions === */
-
-bool UIInit() {
- gMainPool = [[NSAutoreleasePool alloc] init];
- [NSApplication sharedApplication];
-
- if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
- gRTLlayout = true;
-
- if (gAutoSubmit) {
- gUI = [[CrashReporterUI alloc] init];
- } else {
- [[NSBundle mainBundle]
- loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu")
- owner:NSApp
- topLevelObjects:nil];
- }
-
- return true;
-}
-
-void UIShutdown() { [gMainPool release]; }
-
-void UIShowDefaultUI() {
- [gUI showErrorUI:gStrings[ST_CRASHREPORTERDEFAULT]];
- [NSApp run];
-}
-
-bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
- const string& sendURL, const vector<string>& restartArgs) {
- gRestartArgs = restartArgs;
-
- [gUI showCrashUI:files queryParameters:queryParameters sendURL:sendURL];
- [NSApp run];
-
- return gDidTrySend;
-}
-
-void UIError_impl(const string& message) {
- if (!gUI) {
- // UI failed to initialize, printing is the best we can do
- printf("Error: %s\n", message.c_str());
- return;
- }
-
- [gUI showErrorUI:message];
- [NSApp run];
-}
-
-bool UIGetIniPath(string& path) {
- NSString* tmpPath = [NSString stringWithUTF8String:gArgv[0]];
- NSString* iniName = [tmpPath lastPathComponent];
- iniName = [iniName stringByAppendingPathExtension:@"ini"];
- tmpPath = [tmpPath stringByDeletingLastPathComponent];
- tmpPath = [tmpPath stringByDeletingLastPathComponent];
- tmpPath = [tmpPath stringByAppendingPathComponent:@"Resources"];
- tmpPath = [tmpPath stringByAppendingPathComponent:iniName];
- path = [tmpPath UTF8String];
- return true;
-}
-
-bool UIGetSettingsPath(const string& vendor, const string& product,
- string& settingsPath) {
- NSArray* paths = NSSearchPathForDirectoriesInDomains(
- NSApplicationSupportDirectory, NSUserDomainMask, YES);
- NSString* destPath = [paths firstObject];
-
- // Note that MacOS ignores the vendor when creating the profile hierarchy -
- // all application preferences directories live alongside one another in
- // ~/Library/Application Support/
- destPath = [destPath stringByAppendingPathComponent:NSSTR(product)];
- // Thunderbird stores its profile in ~/Library/Thunderbird,
- // but we're going to put stuff in ~/Library/Application Support/Thunderbird
- // anyway, so we have to ensure that path exists.
- string tempPath = [destPath UTF8String];
- if (!UIEnsurePathExists(tempPath)) return false;
-
- destPath = [destPath stringByAppendingPathComponent:@"Crash Reports"];
-
- settingsPath = [destPath UTF8String];
-
- return true;
-}
-
-bool UIMoveFile(const string& file, const string& newfile) {
- if (!rename(file.c_str(), newfile.c_str())) return true;
- if (errno != EXDEV) return false;
-
- NSFileManager* fileManager = [NSFileManager defaultManager];
- NSString* source =
- [fileManager stringWithFileSystemRepresentation:file.c_str()
- length:file.length()];
- NSString* dest =
- [fileManager stringWithFileSystemRepresentation:newfile.c_str()
- length:newfile.length()];
- if (!source || !dest) return false;
-
- [fileManager moveItemAtPath:source toPath:dest error:NULL];
- return UIFileExists(newfile);
-}
diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp
deleted file mode 100644
index e6514d4423..0000000000
--- a/toolkit/crashreporter/client/crashreporter_unix_common.cpp
+++ /dev/null
@@ -1,139 +0,0 @@
-/* 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/. */
-
-#include "crashreporter.h"
-
-#include <algorithm>
-#include <sys/wait.h>
-
-#include <dirent.h>
-#include <errno.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-using namespace CrashReporter;
-using std::ios_base;
-using std::sort;
-using std::string;
-using std::vector;
-
-struct FileData {
- time_t timestamp;
- string path;
-};
-
-static bool CompareFDTime(const FileData& fd1, const FileData& fd2) {
- return fd1.timestamp > fd2.timestamp;
-}
-
-void UIPruneSavedDumps(const string& directory) {
- DIR* dirfd = opendir(directory.c_str());
- if (!dirfd) return;
-
- vector<FileData> dumpfiles;
-
- while (dirent* dir = readdir(dirfd)) {
- FileData fd;
- fd.path = directory + '/' + dir->d_name;
- if (fd.path.size() < 5) continue;
-
- if (fd.path.compare(fd.path.size() - 4, 4, ".dmp") != 0) continue;
-
- struct stat st;
- if (stat(fd.path.c_str(), &st)) {
- closedir(dirfd);
- return;
- }
-
- fd.timestamp = st.st_mtime;
-
- dumpfiles.push_back(fd);
- }
-
- closedir(dirfd);
-
- sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime);
-
- while (dumpfiles.size() > kSaveCount) {
- // get the path of the oldest file
- string path = dumpfiles[dumpfiles.size() - 1].path;
- UIDeleteFile(path);
-
- // s/.dmp/.extra/
- path.replace(path.size() - 4, 4, ".extra");
- UIDeleteFile(path);
-
- dumpfiles.pop_back();
- }
-}
-
-bool UIRunProgram(const string& exename, const vector<string>& args,
- bool wait) {
- pid_t pid = fork();
-
- if (pid == -1) {
- return false;
- } else if (pid == 0) {
- // Child
- size_t argvLen = args.size() + 2;
- vector<char*> argv(argvLen);
-
- argv[0] = const_cast<char*>(exename.c_str());
-
- for (size_t i = 0; i < args.size(); i++) {
- argv[i + 1] = const_cast<char*>(args[i].c_str());
- }
-
- argv[argvLen - 1] = nullptr;
-
- // Run the program
- int rv = execv(exename.c_str(), argv.data());
-
- if (rv == -1) {
- exit(EXIT_FAILURE);
- }
- } else {
- // Parent
- if (wait) {
- waitpid(pid, nullptr, 0);
- }
- }
-
- return true;
-}
-
-bool UIEnsurePathExists(const string& path) {
- int ret = mkdir(path.c_str(), S_IRWXU);
- int e = errno;
- if (ret == -1 && e != EEXIST) return false;
-
- return true;
-}
-
-bool UIFileExists(const string& path) {
- struct stat sb;
- int ret = stat(path.c_str(), &sb);
- if (ret == -1 || !(sb.st_mode & S_IFREG)) return false;
-
- return true;
-}
-
-bool UIDeleteFile(const string& file) { return (unlink(file.c_str()) != -1); }
-
-std::ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) {
- return new std::ifstream(filename.c_str(), mode);
-}
-
-std::ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) {
- return new std::ofstream(filename.c_str(), mode);
-}
-
-string UIGetEnv(const string& name) {
- const char* var = getenv(name.c_str());
- if (var && *var) {
- return var;
- }
-
- return "";
-}
diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp
deleted file mode 100644
index 35018bda4a..0000000000
--- a/toolkit/crashreporter/client/crashreporter_win.cpp
+++ /dev/null
@@ -1,1266 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#ifdef WIN32_LEAN_AND_MEAN
-# undef WIN32_LEAN_AND_MEAN
-#endif
-
-#include "crashreporter.h"
-
-#include <windows.h>
-#include <commctrl.h>
-#include <richedit.h>
-#include <shellapi.h>
-#include <shlobj.h>
-#include <shlwapi.h>
-#include <math.h>
-#include <set>
-#include <algorithm>
-#include "resource.h"
-#include "windows/sender/crash_report_sender.h"
-#include "common/windows/string_utils-inl.h"
-
-#define SUBMIT_REPORT_VALUE L"SubmitCrashReport"
-#define INCLUDE_URL_VALUE L"IncludeURL"
-
-#define WM_UPLOADCOMPLETE WM_APP
-
-// Thanks, Windows.h :(
-#undef min
-#undef max
-
-using std::ifstream;
-using std::ios;
-using std::ios_base;
-using std::map;
-using std::ofstream;
-using std::set;
-using std::string;
-using std::vector;
-using std::wstring;
-
-using namespace CrashReporter;
-
-typedef struct {
- HWND hDlg;
- Json::Value queryParameters;
- map<wstring, wstring> files;
- wstring sendURL;
-
- wstring serverResponse;
-} SendThreadData;
-
-/*
- * Per http://msdn2.microsoft.com/en-us/library/ms645398(VS.85).aspx
- * "The DLGTEMPLATEEX structure is not defined in any standard header file.
- * The structure definition is provided here to explain the format of an
- * extended template for a dialog box.
- */
-typedef struct {
- WORD dlgVer;
- WORD signature;
- DWORD helpID;
- DWORD exStyle;
- // There's more to this struct, but it has weird variable-length
- // members, and I only actually need to touch exStyle on an existing
- // instance, so I've omitted the rest.
-} DLGTEMPLATEEX;
-
-static HANDLE gThreadHandle;
-static SendThreadData gSendData = {
- 0,
-};
-static vector<string> gRestartArgs;
-static Json::Value gQueryParameters;
-static wstring gCrashReporterKey(L"Software\\Mozilla\\Crash Reporter");
-static string gURLParameter;
-static int gCheckboxPadding = 6;
-static bool gRTLlayout = false;
-
-// When vertically resizing the dialog, these items should move down
-static set<UINT> gAttachedBottom;
-
-// Default set of items for gAttachedBottom
-static const UINT kDefaultAttachedBottom[] = {
- IDC_SUBMITREPORTCHECK, IDC_VIEWREPORTBUTTON, IDC_COMMENTTEXT,
- IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT, IDC_THROBBER,
- IDC_CLOSEBUTTON, IDC_RESTARTBUTTON,
-};
-
-static wstring UTF8ToWide(const string& utf8, bool* success = 0);
-static DWORD WINAPI SendThreadProc(LPVOID param);
-
-static wstring Str(const char* key) { return UTF8ToWide(gStrings[key]); }
-
-/* === win32 helper functions === */
-
-static void DoInitCommonControls() {
- INITCOMMONCONTROLSEX ic;
- ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
- ic.dwICC = ICC_PROGRESS_CLASS;
- InitCommonControlsEx(&ic);
- // also get the rich edit control
- LoadLibrary(L"Msftedit.dll");
-}
-
-static bool GetBoolValue(HKEY hRegKey, LPCTSTR valueName, DWORD* value) {
- DWORD type, dataSize;
- dataSize = sizeof(DWORD);
- if (RegQueryValueEx(hRegKey, valueName, nullptr, &type, (LPBYTE)value,
- &dataSize) == ERROR_SUCCESS &&
- type == REG_DWORD)
- return true;
-
- return false;
-}
-
-static bool CheckBoolKey(const wchar_t* key, const wchar_t* valueName,
- bool* enabled) {
- /*
- * NOTE! This code needs to stay in sync with the preference checking
- * code in in nsExceptionHandler.cpp.
- */
- *enabled = false;
- bool found = false;
- HKEY hRegKey;
- DWORD val;
- // see if our reg key is set globally
- if (RegOpenKey(HKEY_LOCAL_MACHINE, key, &hRegKey) == ERROR_SUCCESS) {
- if (GetBoolValue(hRegKey, valueName, &val)) {
- *enabled = (val == 1);
- found = true;
- }
- RegCloseKey(hRegKey);
- } else {
- // look for it in user settings
- if (RegOpenKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) {
- if (GetBoolValue(hRegKey, valueName, &val)) {
- *enabled = (val == 1);
- found = true;
- }
- RegCloseKey(hRegKey);
- }
- }
-
- return found;
-}
-
-static void SetBoolKey(const wchar_t* key, const wchar_t* value, bool enabled) {
- /*
- * NOTE! This code needs to stay in sync with the preference setting
- * code in in nsExceptionHandler.cpp.
- */
- HKEY hRegKey;
-
- if (RegCreateKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) {
- DWORD data = (enabled ? 1 : 0);
- RegSetValueEx(hRegKey, value, 0, REG_DWORD, (LPBYTE)&data, sizeof(data));
- RegCloseKey(hRegKey);
- }
-}
-
-static string FormatLastError() {
- DWORD err = GetLastError();
- LPWSTR s;
- string message = "Crash report submission failed: ";
- // odds are it's a WinInet error
- HANDLE hInetModule = GetModuleHandle(L"WinInet.dll");
- if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
- FORMAT_MESSAGE_FROM_SYSTEM |
- FORMAT_MESSAGE_FROM_HMODULE,
- hInetModule, err, 0, (LPWSTR)&s, 0, nullptr) != 0) {
- message += WideToUTF8(s, nullptr);
- LocalFree(s);
- // strip off any trailing newlines
- string::size_type n = message.find_last_not_of("\r\n");
- if (n < message.size() - 1) {
- message.erase(n + 1);
- }
- } else {
- char buf[64];
- sprintf(buf, "Unknown error, error code: 0x%08x",
- static_cast<unsigned int>(err));
- message += buf;
- }
- return message;
-}
-
-#define TS_DRAW 2
-#define BP_CHECKBOX 3
-
-typedef HANDLE(WINAPI* OpenThemeDataPtr)(HWND hwnd, LPCWSTR pszClassList);
-typedef HRESULT(WINAPI* CloseThemeDataPtr)(HANDLE hTheme);
-typedef HRESULT(WINAPI* GetThemePartSizePtr)(HANDLE hTheme, HDC hdc,
- int iPartId, int iStateId,
- RECT* prc, int ts, SIZE* psz);
-typedef HRESULT(WINAPI* GetThemeContentRectPtr)(HANDLE hTheme, HDC hdc,
- int iPartId, int iStateId,
- const RECT* pRect,
- RECT* pContentRect);
-
-static void GetThemeSizes(HWND hwnd) {
- HMODULE themeDLL = LoadLibrary(L"uxtheme.dll");
-
- if (!themeDLL) return;
-
- OpenThemeDataPtr openTheme =
- (OpenThemeDataPtr)GetProcAddress(themeDLL, "OpenThemeData");
- CloseThemeDataPtr closeTheme =
- (CloseThemeDataPtr)GetProcAddress(themeDLL, "CloseThemeData");
- GetThemePartSizePtr getThemePartSize =
- (GetThemePartSizePtr)GetProcAddress(themeDLL, "GetThemePartSize");
-
- if (!openTheme || !closeTheme || !getThemePartSize) {
- FreeLibrary(themeDLL);
- return;
- }
-
- HANDLE buttonTheme = openTheme(hwnd, L"Button");
- if (!buttonTheme) {
- FreeLibrary(themeDLL);
- return;
- }
- HDC hdc = GetDC(hwnd);
- SIZE s;
- getThemePartSize(buttonTheme, hdc, BP_CHECKBOX, 0, nullptr, TS_DRAW, &s);
- gCheckboxPadding = s.cx;
- closeTheme(buttonTheme);
- FreeLibrary(themeDLL);
-}
-
-// Gets the position of a window relative to another window's client area
-static void GetRelativeRect(HWND hwnd, HWND hwndParent, RECT* r) {
- GetWindowRect(hwnd, r);
- MapWindowPoints(nullptr, hwndParent, (POINT*)r, 2);
-}
-
-static void SetDlgItemVisible(HWND hwndDlg, UINT item, bool visible) {
- HWND hwnd = GetDlgItem(hwndDlg, item);
-
- ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE);
-}
-
-/* === Crash Reporting Dialog === */
-
-static void StretchDialog(HWND hwndDlg, int ydiff) {
- RECT r;
- GetWindowRect(hwndDlg, &r);
- r.bottom += ydiff;
- MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
-}
-
-static void ReflowDialog(HWND hwndDlg, int ydiff) {
- // Move items attached to the bottom down/up by as much as
- // the window resize
- for (set<UINT>::const_iterator item = gAttachedBottom.begin();
- item != gAttachedBottom.end(); item++) {
- RECT r;
- HWND hwnd = GetDlgItem(hwndDlg, *item);
- GetRelativeRect(hwnd, hwndDlg, &r);
- r.top += ydiff;
- r.bottom += ydiff;
- MoveWindow(hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
- }
-}
-
-static DWORD WINAPI SendThreadProc(LPVOID param) {
- bool finishedOk;
- SendThreadData* td = (SendThreadData*)param;
-
- if (td->sendURL.empty()) {
- finishedOk = false;
- LogMessage("No server URL, not sending report");
- } else {
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- string parameters(Json::writeString(builder, td->queryParameters));
- google_breakpad::CrashReportSender sender(L"");
- finishedOk = (sender.SendCrashReport(td->sendURL, parameters, td->files,
- &td->serverResponse) ==
- google_breakpad::RESULT_SUCCEEDED);
- if (finishedOk) {
- LogMessage("Crash report submitted successfully");
- } else {
- // get an error string and print it to the log
- // XXX: would be nice to get the HTTP status code here, filed:
- // http://code.google.com/p/google-breakpad/issues/detail?id=220
- LogMessage(FormatLastError());
- }
- }
-
- if (gAutoSubmit) {
- // Ordinarily this is done on the main thread in CrashReporterDialogProc,
- // for auto submit we don't run that and it should be safe to finish up
- // here as is done on other platforms.
- SendCompleted(finishedOk, WideToUTF8(gSendData.serverResponse));
- } else {
- PostMessage(td->hDlg, WM_UPLOADCOMPLETE, finishedOk ? 1 : 0, 0);
- }
-
- return 0;
-}
-
-static void EndCrashReporterDialog(HWND hwndDlg, int code) {
- // Save the current values to the registry
- SetBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE,
- IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK) != 0);
- SetBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE,
- IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0);
-
- EndDialog(hwndDlg, code);
-}
-
-static void MaybeResizeProgressText(HWND hwndDlg) {
- HWND hwndProgress = GetDlgItem(hwndDlg, IDC_PROGRESSTEXT);
- HDC hdc = GetDC(hwndProgress);
- HFONT hfont = (HFONT)SendMessage(hwndProgress, WM_GETFONT, 0, 0);
- if (hfont) SelectObject(hdc, hfont);
- SIZE size;
- RECT rect;
- GetRelativeRect(hwndProgress, hwndDlg, &rect);
-
- wchar_t text[1024];
- GetWindowText(hwndProgress, text, 1024);
-
- if (!GetTextExtentPoint32(hdc, text, wcslen(text), &size)) return;
-
- if (size.cx < (rect.right - rect.left)) return;
-
- // Figure out how much we need to resize things vertically
- // This is sort of a fudge, but it should be good enough.
- int wantedHeight =
- size.cy * (int)ceil((float)size.cx / (float)(rect.right - rect.left));
- int diff = wantedHeight - (rect.bottom - rect.top);
- if (diff <= 0) return;
-
- MoveWindow(hwndProgress, rect.left, rect.top, rect.right - rect.left,
- wantedHeight, TRUE);
-
- gAttachedBottom.clear();
- gAttachedBottom.insert(IDC_CLOSEBUTTON);
- gAttachedBottom.insert(IDC_RESTARTBUTTON);
-
- StretchDialog(hwndDlg, diff);
-
- for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) {
- gAttachedBottom.insert(kDefaultAttachedBottom[i]);
- }
-}
-
-static void MaybeSendReport(HWND hwndDlg) {
- if (!IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK)) {
- EndCrashReporterDialog(hwndDlg, 0);
- return;
- }
-
- // disable all the form controls
- EnableWindow(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK), false);
- EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), false);
- EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), false);
- EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), false);
- EnableWindow(GetDlgItem(hwndDlg, IDC_CLOSEBUTTON), false);
- EnableWindow(GetDlgItem(hwndDlg, IDC_RESTARTBUTTON), false);
-
- SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT, Str(ST_REPORTDURINGSUBMIT).c_str());
- MaybeResizeProgressText(hwndDlg);
- // start throbber
- // play entire AVI, and loop
- Animate_Play(GetDlgItem(hwndDlg, IDC_THROBBER), 0, -1, -1);
- SetDlgItemVisible(hwndDlg, IDC_THROBBER, true);
- gThreadHandle = nullptr;
- gSendData.hDlg = hwndDlg;
- gSendData.queryParameters = gQueryParameters;
-
- gThreadHandle =
- CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr);
-}
-
-static void RestartApplication() {
- wstring cmdLine;
-
- for (unsigned int i = 0; i < gRestartArgs.size(); i++) {
- cmdLine += L"\"" + UTF8ToWide(gRestartArgs[i]) + L"\" ";
- }
-
- STARTUPINFO si;
- PROCESS_INFORMATION pi;
-
- ZeroMemory(&si, sizeof(si));
- si.cb = sizeof(si);
- si.dwFlags = STARTF_USESHOWWINDOW;
- si.wShowWindow = SW_SHOWNORMAL;
- ZeroMemory(&pi, sizeof(pi));
-
- if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE,
- 0, nullptr, nullptr, &si, &pi)) {
- CloseHandle(pi.hProcess);
- CloseHandle(pi.hThread);
- }
-}
-
-static void ShowReportInfo(HWND hwndDlg) {
- wstring description;
-
- for (Json::ValueConstIterator iter = gQueryParameters.begin();
- iter != gQueryParameters.end(); ++iter) {
- description += UTF8ToWide(iter.name());
- description += L": ";
- string value;
- if (iter->isString()) {
- value = iter->asString();
- } else {
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- value = Json::writeString(builder, *iter);
- }
- description += UTF8ToWide(value);
- description += L"\n";
- }
-
- description += L"\n";
- description += Str(ST_EXTRAREPORTINFO);
-
- SetDlgItemText(hwndDlg, IDC_VIEWREPORTTEXT, description.c_str());
-}
-
-static void UpdateURL(HWND hwndDlg) {
- if (IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK)) {
- gQueryParameters["URL"] = gURLParameter;
- } else {
- gQueryParameters.removeMember("URL");
- }
-}
-
-static void UpdateComment(HWND hwndDlg) {
- wchar_t comment[MAX_COMMENT_LENGTH + 1];
- GetDlgItemTextW(hwndDlg, IDC_COMMENTTEXT, comment,
- sizeof(comment) / sizeof(comment[0]));
- if (wcslen(comment) > 0)
- gQueryParameters["Comments"] = WideToUTF8(comment);
- else
- gQueryParameters.removeMember("Comments");
-}
-
-/*
- * Dialog procedure for the "view report" dialog.
- */
-static BOOL CALLBACK ViewReportDialogProc(HWND hwndDlg, UINT message,
- WPARAM wParam, LPARAM lParam) {
- switch (message) {
- case WM_INITDIALOG: {
- SetWindowText(hwndDlg, Str(ST_VIEWREPORTTITLE).c_str());
- SetDlgItemText(hwndDlg, IDOK, Str(ST_OK).c_str());
- SendDlgItemMessage(hwndDlg, IDC_VIEWREPORTTEXT, EM_SETTARGETDEVICE,
- (WPARAM) nullptr, 0);
- ShowReportInfo(hwndDlg);
- SetFocus(GetDlgItem(hwndDlg, IDOK));
- return FALSE;
- }
-
- case WM_COMMAND: {
- if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDOK)
- EndDialog(hwndDlg, 0);
- return FALSE;
- }
- }
- return FALSE;
-}
-
-// Return the number of bytes this string will take encoded
-// in UTF-8
-static inline int BytesInUTF8(wchar_t* str) {
- // Just count size of buffer for UTF-8, minus one
- // (we don't need to count the null terminator)
- return WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, nullptr,
- nullptr) -
- 1;
-}
-
-// Calculate the length of the text in this edit control (in bytes,
-// in the UTF-8 encoding) after replacing the current selection
-// with |insert|.
-static int NewTextLength(HWND hwndEdit, wchar_t* insert) {
- wchar_t current[MAX_COMMENT_LENGTH + 1];
-
- GetWindowText(hwndEdit, current, MAX_COMMENT_LENGTH + 1);
- DWORD selStart, selEnd;
- SendMessage(hwndEdit, EM_GETSEL, (WPARAM)&selStart, (LPARAM)&selEnd);
-
- int selectionLength = 0;
- if (selEnd - selStart > 0) {
- wchar_t selection[MAX_COMMENT_LENGTH + 1];
- google_breakpad::WindowsStringUtils::safe_wcsncpy(
- selection, MAX_COMMENT_LENGTH + 1, current + selStart,
- selEnd - selStart);
- selection[selEnd - selStart] = '\0';
- selectionLength = BytesInUTF8(selection);
- }
-
- // current string length + replacement text length
- // - replaced selection length
- return BytesInUTF8(current) + BytesInUTF8(insert) - selectionLength;
-}
-
-// Window procedure for subclassing edit controls
-static LRESULT CALLBACK EditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam,
- LPARAM lParam) {
- static WNDPROC super = nullptr;
-
- if (super == nullptr) super = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_USERDATA);
-
- switch (uMsg) {
- case WM_PAINT: {
- HDC hdc;
- PAINTSTRUCT ps;
- RECT r;
- wchar_t windowText[1024];
-
- GetWindowText(hwnd, windowText, 1024);
- // if the control contains text or is focused, draw it normally
- if (GetFocus() == hwnd || windowText[0] != '\0')
- return CallWindowProc(super, hwnd, uMsg, wParam, lParam);
-
- GetClientRect(hwnd, &r);
- hdc = BeginPaint(hwnd, &ps);
- FillRect(hdc, &r,
- GetSysColorBrush(IsWindowEnabled(hwnd) ? COLOR_WINDOW
- : COLOR_BTNFACE));
- SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT));
- SelectObject(hdc, (HFONT)GetStockObject(DEFAULT_GUI_FONT));
- SetBkMode(hdc, TRANSPARENT);
- wchar_t* txt = (wchar_t*)GetProp(hwnd, L"PROP_GRAYTEXT");
- // Get the actual edit control rect
- CallWindowProc(super, hwnd, EM_GETRECT, 0, (LPARAM)&r);
- UINT format = DT_EDITCONTROL | DT_NOPREFIX | DT_WORDBREAK | DT_INTERNAL;
- if (gRTLlayout) format |= DT_RIGHT;
- if (txt) DrawText(hdc, txt, wcslen(txt), &r, format);
- EndPaint(hwnd, &ps);
- return 0;
- }
-
- // We handle WM_CHAR and WM_PASTE to limit the comment box to 500
- // bytes in UTF-8.
- case WM_CHAR: {
- // Leave accelerator keys and non-printing chars (except LF) alone
- if (wParam & (1 << 24) || wParam & (1 << 29) ||
- (wParam < ' ' && wParam != '\n'))
- break;
-
- wchar_t ch[2] = {(wchar_t)wParam, 0};
- if (NewTextLength(hwnd, ch) > MAX_COMMENT_LENGTH) return 0;
-
- break;
- }
-
- case WM_PASTE: {
- if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(hwnd)) {
- HGLOBAL hg = GetClipboardData(CF_UNICODETEXT);
- wchar_t* pastedText = (wchar_t*)GlobalLock(hg);
- int newSize = 0;
-
- if (pastedText) newSize = NewTextLength(hwnd, pastedText);
-
- GlobalUnlock(hg);
- CloseClipboard();
-
- if (newSize > MAX_COMMENT_LENGTH) return 0;
- }
- break;
- }
-
- case WM_SETFOCUS:
- case WM_KILLFOCUS: {
- RECT r;
- GetClientRect(hwnd, &r);
- InvalidateRect(hwnd, &r, TRUE);
- break;
- }
-
- case WM_DESTROY: {
- // cleanup our property
- HGLOBAL hData = RemoveProp(hwnd, L"PROP_GRAYTEXT");
- if (hData) GlobalFree(hData);
- }
- }
-
- return CallWindowProc(super, hwnd, uMsg, wParam, lParam);
-}
-
-// Resize a control to fit this text
-static int ResizeControl(HWND hwndButton, RECT& rect, wstring text,
- bool shiftLeft, int userDefinedPadding) {
- HDC hdc = GetDC(hwndButton);
- HFONT hfont = (HFONT)SendMessage(hwndButton, WM_GETFONT, 0, 0);
- if (hfont) SelectObject(hdc, hfont);
- SIZE size, oldSize;
- int sizeDiff = 0;
-
- wchar_t oldText[1024];
- GetWindowText(hwndButton, oldText, 1024);
-
- if (GetTextExtentPoint32(hdc, text.c_str(), text.length(), &size)
- // default text on the button
- && GetTextExtentPoint32(hdc, oldText, wcslen(oldText), &oldSize)) {
- /*
- Expand control widths to accomidate wider text strings. For most
- controls (including buttons) the text padding is defined by the
- dialog's rc file. Some controls (such as checkboxes) have padding
- that extends to the end of the dialog, in which case we ignore the
- rc padding and rely on a user defined value passed in through
- userDefinedPadding.
- */
- int textIncrease = size.cx - oldSize.cx;
- if (textIncrease < 0) return 0;
- int existingTextPadding;
- if (userDefinedPadding == 0)
- existingTextPadding = (rect.right - rect.left) - oldSize.cx;
- else
- existingTextPadding = userDefinedPadding;
- sizeDiff = textIncrease + existingTextPadding;
-
- if (shiftLeft) {
- // shift left by the amount the button should grow
- rect.left -= sizeDiff;
- } else {
- // grow right instead
- rect.right += sizeDiff;
- }
- MoveWindow(hwndButton, rect.left, rect.top, rect.right - rect.left,
- rect.bottom - rect.top, TRUE);
- }
- return sizeDiff;
-}
-
-// The window was resized horizontally, so widen some of our
-// controls to make use of the space
-static void StretchControlsToFit(HWND hwndDlg) {
- int controls[] = {IDC_DESCRIPTIONTEXT, IDC_SUBMITREPORTCHECK, IDC_COMMENTTEXT,
- IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT};
-
- RECT dlgRect;
- GetClientRect(hwndDlg, &dlgRect);
-
- for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); i++) {
- RECT r;
- HWND hwndControl = GetDlgItem(hwndDlg, controls[i]);
- GetRelativeRect(hwndControl, hwndDlg, &r);
- // 6 pixel spacing on the right
- if (r.right + 6 != dlgRect.right) {
- r.right = dlgRect.right - 6;
- MoveWindow(hwndControl, r.left, r.top, r.right - r.left, r.bottom - r.top,
- TRUE);
- }
- }
-}
-
-static void SubmitReportChecked(HWND hwndDlg) {
- bool enabled = (IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0);
- EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), enabled);
- EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), enabled);
- EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), enabled);
- SetDlgItemVisible(hwndDlg, IDC_PROGRESSTEXT, enabled);
-}
-
-static INT_PTR DialogBoxParamMaybeRTL(UINT idd, HWND hwndParent,
- DLGPROC dlgProc, LPARAM param) {
- INT_PTR rv = 0;
- if (gRTLlayout) {
- // We need to toggle the WS_EX_LAYOUTRTL style flag on the dialog
- // template.
- HRSRC hDialogRC = FindResource(nullptr, MAKEINTRESOURCE(idd), RT_DIALOG);
- HGLOBAL hDlgTemplate = LoadResource(nullptr, hDialogRC);
- DLGTEMPLATEEX* pDlgTemplate = (DLGTEMPLATEEX*)LockResource(hDlgTemplate);
- unsigned long sizeDlg = SizeofResource(nullptr, hDialogRC);
- HGLOBAL hMyDlgTemplate = GlobalAlloc(GPTR, sizeDlg);
- DLGTEMPLATEEX* pMyDlgTemplate = (DLGTEMPLATEEX*)GlobalLock(hMyDlgTemplate);
- memcpy(pMyDlgTemplate, pDlgTemplate, sizeDlg);
-
- pMyDlgTemplate->exStyle |= WS_EX_LAYOUTRTL;
-
- rv = DialogBoxIndirectParam(nullptr, (LPCDLGTEMPLATE)pMyDlgTemplate,
- hwndParent, dlgProc, param);
- GlobalUnlock(hMyDlgTemplate);
- GlobalFree(hMyDlgTemplate);
- } else {
- rv = DialogBoxParam(nullptr, MAKEINTRESOURCE(idd), hwndParent, dlgProc,
- param);
- }
-
- return rv;
-}
-
-static BOOL CALLBACK CrashReporterDialogProc(HWND hwndDlg, UINT message,
- WPARAM wParam, LPARAM lParam) {
- static int sHeight = 0;
-
- bool success;
- bool enabled;
-
- switch (message) {
- case WM_INITDIALOG: {
- GetThemeSizes(hwndDlg);
- RECT r;
- GetClientRect(hwndDlg, &r);
- sHeight = r.bottom - r.top;
-
- SetWindowText(hwndDlg, Str(ST_CRASHREPORTERTITLE).c_str());
- HICON hIcon =
- LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDI_MAINICON));
- SendMessage(hwndDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
- SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
-
- // resize the "View Report" button based on the string length
- RECT rect;
- HWND hwnd = GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON);
- GetRelativeRect(hwnd, hwndDlg, &rect);
- ResizeControl(hwnd, rect, Str(ST_VIEWREPORT), false, 0);
- SetDlgItemText(hwndDlg, IDC_VIEWREPORTBUTTON, Str(ST_VIEWREPORT).c_str());
-
- hwnd = GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK);
- GetRelativeRect(hwnd, hwndDlg, &rect);
- long maxdiff = ResizeControl(hwnd, rect, Str(ST_CHECKSUBMIT), false,
- gCheckboxPadding);
- SetDlgItemText(hwndDlg, IDC_SUBMITREPORTCHECK,
- Str(ST_CHECKSUBMIT).c_str());
-
- if (!CheckBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE,
- &enabled))
- enabled = true;
-
- CheckDlgButton(hwndDlg, IDC_SUBMITREPORTCHECK,
- enabled ? BST_CHECKED : BST_UNCHECKED);
- SubmitReportChecked(hwndDlg);
-
- HWND hwndComment = GetDlgItem(hwndDlg, IDC_COMMENTTEXT);
- WNDPROC OldWndProc = (WNDPROC)SetWindowLongPtr(
- hwndComment, GWLP_WNDPROC, (LONG_PTR)EditSubclassProc);
-
- // Subclass comment edit control to get placeholder text
- SetWindowLongPtr(hwndComment, GWLP_USERDATA, (LONG_PTR)OldWndProc);
- wstring commentGrayText = Str(ST_COMMENTGRAYTEXT);
- wchar_t* hMem = (wchar_t*)GlobalAlloc(
- GPTR, (commentGrayText.length() + 1) * sizeof(wchar_t));
- wcscpy(hMem, commentGrayText.c_str());
- SetProp(hwndComment, L"PROP_GRAYTEXT", hMem);
-
- hwnd = GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK);
- GetRelativeRect(hwnd, hwndDlg, &rect);
- long diff =
- ResizeControl(hwnd, rect, Str(ST_CHECKURL), false, gCheckboxPadding);
- maxdiff = std::max(diff, maxdiff);
- SetDlgItemText(hwndDlg, IDC_INCLUDEURLCHECK, Str(ST_CHECKURL).c_str());
-
- // want this on by default
- if (CheckBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE,
- &enabled) &&
- !enabled) {
- CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_UNCHECKED);
- } else {
- CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_CHECKED);
- }
-
- SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT,
- Str(ST_REPORTPRESUBMIT).c_str());
-
- RECT closeRect;
- HWND hwndClose = GetDlgItem(hwndDlg, IDC_CLOSEBUTTON);
- GetRelativeRect(hwndClose, hwndDlg, &closeRect);
-
- RECT restartRect;
- HWND hwndRestart = GetDlgItem(hwndDlg, IDC_RESTARTBUTTON);
- GetRelativeRect(hwndRestart, hwndDlg, &restartRect);
-
- // set the close button text and shift the buttons around
- // since the size may need to change
- int sizeDiff = ResizeControl(hwndClose, closeRect, Str(ST_QUIT), true, 0);
- restartRect.left -= sizeDiff;
- restartRect.right -= sizeDiff;
- SetDlgItemText(hwndDlg, IDC_CLOSEBUTTON, Str(ST_QUIT).c_str());
-
- if (gRestartArgs.size() > 0) {
- // Resize restart button to fit text
- ResizeControl(hwndRestart, restartRect, Str(ST_RESTART), true, 0);
- SetDlgItemText(hwndDlg, IDC_RESTARTBUTTON, Str(ST_RESTART).c_str());
- } else {
- // No restart arguments, so just hide the restart button
- SetDlgItemVisible(hwndDlg, IDC_RESTARTBUTTON, false);
- }
- // See if we need to widen the window
- // Leave 6 pixels on either side + 6 pixels between the buttons
- int neededSize = closeRect.right - closeRect.left + restartRect.right -
- restartRect.left + 6 * 3;
- GetClientRect(hwndDlg, &r);
- // We may already have resized one of the checkboxes above
- maxdiff = std::max(maxdiff, neededSize - (r.right - r.left));
-
- if (maxdiff > 0) {
- // widen window
- GetWindowRect(hwndDlg, &r);
- r.right += maxdiff;
- MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top,
- TRUE);
- // shift both buttons right
- if (restartRect.left + maxdiff < 6) maxdiff += 6;
- closeRect.left += maxdiff;
- closeRect.right += maxdiff;
- restartRect.left += maxdiff;
- restartRect.right += maxdiff;
- MoveWindow(hwndClose, closeRect.left, closeRect.top,
- closeRect.right - closeRect.left,
- closeRect.bottom - closeRect.top, TRUE);
- StretchControlsToFit(hwndDlg);
- }
- // need to move the restart button regardless
- MoveWindow(hwndRestart, restartRect.left, restartRect.top,
- restartRect.right - restartRect.left,
- restartRect.bottom - restartRect.top, TRUE);
-
- // Resize the description text last, in case the window was resized
- // before this.
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETEVENTMASK,
- (WPARAM) nullptr, ENM_REQUESTRESIZE);
-
- wstring description = Str(ST_CRASHREPORTERHEADER);
- description += L"\n\n";
- description += Str(ST_CRASHREPORTERDESCRIPTION);
- SetDlgItemText(hwndDlg, IDC_DESCRIPTIONTEXT, description.c_str());
-
- // Make the title bold.
- CHARFORMAT fmt = {
- 0,
- };
- fmt.cbSize = sizeof(fmt);
- fmt.dwMask = CFM_BOLD;
- fmt.dwEffects = CFE_BOLD;
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0,
- Str(ST_CRASHREPORTERHEADER).length());
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETCHARFORMAT,
- SCF_SELECTION, (LPARAM)&fmt);
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0, 0);
- // Force redraw.
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETTARGETDEVICE,
- (WPARAM) nullptr, 0);
- // Force resize.
- SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_REQUESTRESIZE, 0, 0);
-
- // if no URL was given, hide the URL checkbox
- if (!gQueryParameters.isMember("URL")) {
- RECT urlCheckRect;
- GetWindowRect(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), &urlCheckRect);
-
- SetDlgItemVisible(hwndDlg, IDC_INCLUDEURLCHECK, false);
-
- gAttachedBottom.erase(IDC_VIEWREPORTBUTTON);
- gAttachedBottom.erase(IDC_SUBMITREPORTCHECK);
- gAttachedBottom.erase(IDC_COMMENTTEXT);
-
- StretchDialog(hwndDlg, urlCheckRect.top - urlCheckRect.bottom);
-
- gAttachedBottom.insert(IDC_VIEWREPORTBUTTON);
- gAttachedBottom.insert(IDC_SUBMITREPORTCHECK);
- gAttachedBottom.insert(IDC_COMMENTTEXT);
- }
-
- MaybeResizeProgressText(hwndDlg);
-
- // Open the AVI resource for the throbber
- Animate_Open(GetDlgItem(hwndDlg, IDC_THROBBER),
- MAKEINTRESOURCE(IDR_THROBBER));
-
- UpdateURL(hwndDlg);
-
- SetFocus(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK));
- return FALSE;
- }
- case WM_SIZE: {
- ReflowDialog(hwndDlg, HIWORD(lParam) - sHeight);
- sHeight = HIWORD(lParam);
- InvalidateRect(hwndDlg, nullptr, TRUE);
- return FALSE;
- }
- case WM_NOTIFY: {
- NMHDR* notification = reinterpret_cast<NMHDR*>(lParam);
- if (notification->code == EN_REQUESTRESIZE) {
- // Resizing the rich edit control to fit the description text.
- REQRESIZE* reqresize = reinterpret_cast<REQRESIZE*>(lParam);
- RECT newSize = reqresize->rc;
- RECT oldSize;
- GetRelativeRect(notification->hwndFrom, hwndDlg, &oldSize);
-
- // resize the text box as requested
- MoveWindow(notification->hwndFrom, newSize.left, newSize.top,
- newSize.right - newSize.left, newSize.bottom - newSize.top,
- TRUE);
-
- // Resize the dialog to fit (the WM_SIZE handler will move the controls)
- StretchDialog(hwndDlg, newSize.bottom - oldSize.bottom);
- }
- return FALSE;
- }
- case WM_COMMAND: {
- if (HIWORD(wParam) == BN_CLICKED) {
- switch (LOWORD(wParam)) {
- case IDC_VIEWREPORTBUTTON:
- DialogBoxParamMaybeRTL(IDD_VIEWREPORTDIALOG, hwndDlg,
- (DLGPROC)ViewReportDialogProc, 0);
- break;
- case IDC_SUBMITREPORTCHECK:
- SubmitReportChecked(hwndDlg);
- break;
- case IDC_INCLUDEURLCHECK:
- UpdateURL(hwndDlg);
- break;
- case IDC_CLOSEBUTTON:
- MaybeSendReport(hwndDlg);
- break;
- case IDC_RESTARTBUTTON:
- RestartApplication();
- MaybeSendReport(hwndDlg);
- break;
- }
- } else if (HIWORD(wParam) == EN_CHANGE) {
- switch (LOWORD(wParam)) {
- case IDC_COMMENTTEXT:
- UpdateComment(hwndDlg);
- }
- }
-
- return FALSE;
- }
- case WM_UPLOADCOMPLETE: {
- WaitForSingleObject(gThreadHandle, INFINITE);
- success = (wParam == 1);
- SendCompleted(success, WideToUTF8(gSendData.serverResponse));
- // hide throbber
- Animate_Stop(GetDlgItem(hwndDlg, IDC_THROBBER));
- SetDlgItemVisible(hwndDlg, IDC_THROBBER, false);
-
- SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT,
- success ? Str(ST_REPORTSUBMITSUCCESS).c_str()
- : Str(ST_SUBMITFAILED).c_str());
- MaybeResizeProgressText(hwndDlg);
- // close dialog after 5 seconds
- SetTimer(hwndDlg, 0, 5000, nullptr);
- //
- return TRUE;
- }
-
- case WM_TIMER: {
- // The "1" gets used down in UIShowCrashUI to indicate that we at least
- // tried to send the report.
- EndCrashReporterDialog(hwndDlg, 1);
- return FALSE;
- }
-
- case WM_CLOSE: {
- EndCrashReporterDialog(hwndDlg, 0);
- return FALSE;
- }
- }
- return FALSE;
-}
-
-static wstring UTF8ToWide(const string& utf8, bool* success) {
- wchar_t* buffer = nullptr;
- int buffer_size =
- MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
- if (buffer_size == 0) {
- if (success) *success = false;
- return L"";
- }
-
- buffer = new wchar_t[buffer_size];
- if (buffer == nullptr) {
- if (success) *success = false;
- return L"";
- }
-
- MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer, buffer_size);
- wstring str = buffer;
- delete[] buffer;
-
- if (success) *success = true;
-
- return str;
-}
-
-static string WideToMBCP(const wstring& wide, unsigned int cp,
- bool* success = nullptr) {
- char* buffer = nullptr;
- int buffer_size = WideCharToMultiByte(cp, 0, wide.c_str(), -1, nullptr, 0,
- nullptr, nullptr);
- if (buffer_size == 0) {
- if (success) *success = false;
- return "";
- }
-
- buffer = new char[buffer_size];
- if (buffer == nullptr) {
- if (success) *success = false;
- return "";
- }
-
- WideCharToMultiByte(cp, 0, wide.c_str(), -1, buffer, buffer_size, nullptr,
- nullptr);
- string mb = buffer;
- delete[] buffer;
-
- if (success) *success = true;
-
- return mb;
-}
-
-string WideToUTF8(const wstring& wide, bool* success) {
- return WideToMBCP(wide, CP_UTF8, success);
-}
-
-/* === Crashreporter UI Functions === */
-
-bool UIInit() {
- for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) {
- gAttachedBottom.insert(kDefaultAttachedBottom[i]);
- }
-
- DoInitCommonControls();
-
- return true;
-}
-
-void UIShutdown() {}
-
-void UIShowDefaultUI() {
- MessageBox(nullptr, Str(ST_CRASHREPORTERDEFAULT).c_str(), L"Crash Reporter",
- MB_OK | MB_ICONSTOP);
-}
-
-bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
- const string& sendURL, const vector<string>& restartArgs) {
- gSendData.hDlg = nullptr;
- gSendData.sendURL = UTF8ToWide(sendURL);
-
- for (StringTable::const_iterator i = files.begin(); i != files.end(); i++) {
- gSendData.files[UTF8ToWide(i->first)] = UTF8ToWide(i->second);
- }
-
- gQueryParameters = queryParameters;
-
- if (gQueryParameters.isMember("Vendor")) {
- gCrashReporterKey = L"Software\\";
- string vendor = gQueryParameters["Vendor"].asString();
- if (!vendor.empty()) {
- gCrashReporterKey += UTF8ToWide(vendor) + L"\\";
- }
- string productName = gQueryParameters["ProductName"].asString();
- gCrashReporterKey += UTF8ToWide(productName) + L"\\Crash Reporter";
- }
-
- if (gQueryParameters.isMember("URL")) {
- gURLParameter = gQueryParameters["URL"].asString();
- }
-
- gRestartArgs = restartArgs;
-
- if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
- gRTLlayout = true;
-
- if (gAutoSubmit) {
- gSendData.queryParameters = gQueryParameters;
-
- gThreadHandle =
- CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr);
- WaitForSingleObject(gThreadHandle, INFINITE);
- // SendCompleted was called from SendThreadProc
- return true;
- }
-
- return 1 == DialogBoxParamMaybeRTL(IDD_SENDDIALOG, nullptr,
- (DLGPROC)CrashReporterDialogProc, 0);
-}
-
-void UIError_impl(const string& message) {
- wstring title = Str(ST_CRASHREPORTERTITLE);
- if (title.empty()) title = L"Crash Reporter Error";
-
- MessageBox(nullptr, UTF8ToWide(message).c_str(), title.c_str(),
- MB_OK | MB_ICONSTOP);
-}
-
-bool UIGetIniPath(string& path) {
- wchar_t fileName[MAX_PATH];
- if (GetModuleFileName(nullptr, fileName, MAX_PATH)) {
- // get crashreporter ini
- wchar_t* s = wcsrchr(fileName, '.');
- if (s) {
- wcscpy(s, L".ini");
- path = WideToUTF8(fileName);
- return true;
- }
- }
-
- return false;
-}
-
-bool UIGetSettingsPath(const string& vendor, const string& product,
- string& settings_path) {
- wchar_t path[MAX_PATH] = {};
- HRESULT hRes = SHGetFolderPath(nullptr, CSIDL_APPDATA, nullptr, 0, path);
- if (FAILED(hRes)) {
- // This provides a fallback for getting the path to APPDATA by querying the
- // registry when the call to SHGetFolderPath is unable to provide this path
- // (Bug 513958).
- HKEY key;
- DWORD type, dwRes;
- DWORD size = sizeof(path) - 1;
- dwRes = ::RegOpenKeyExW(HKEY_CURRENT_USER,
- L"Software\\Microsoft\\Windows\\CurrentVersion\\Exp"
- L"lorer\\Shell Folders",
- 0, KEY_READ, &key);
- if (dwRes != ERROR_SUCCESS) return false;
-
- dwRes =
- RegQueryValueExW(key, L"AppData", nullptr, &type, (LPBYTE)&path, &size);
- ::RegCloseKey(key);
- // The call to RegQueryValueExW must succeed, the type must be REG_SZ, the
- // buffer size must not equal 0, and the buffer size be a multiple of 2.
- if (dwRes != ERROR_SUCCESS || type != REG_SZ || size == 0 || size % 2 != 0)
- return false;
- }
-
- if (!vendor.empty()) {
- PathAppend(path, UTF8ToWide(vendor).c_str());
- }
- PathAppend(path, UTF8ToWide(product).c_str());
- PathAppend(path, L"Crash Reports");
- settings_path = WideToUTF8(path);
- return true;
-}
-
-bool UIEnsurePathExists(const string& path) {
- if (CreateDirectory(UTF8ToWide(path).c_str(), nullptr) == 0) {
- if (GetLastError() != ERROR_ALREADY_EXISTS) return false;
- }
-
- return true;
-}
-
-bool UIFileExists(const string& path) {
- DWORD attrs = GetFileAttributes(UTF8ToWide(path).c_str());
- return (attrs != INVALID_FILE_ATTRIBUTES);
-}
-
-bool UIMoveFile(const string& oldfile, const string& newfile) {
- if (oldfile == newfile) return true;
-
- return MoveFile(UTF8ToWide(oldfile).c_str(), UTF8ToWide(newfile).c_str()) ==
- TRUE;
-}
-
-bool UIDeleteFile(const string& oldfile) {
- return DeleteFile(UTF8ToWide(oldfile).c_str()) == TRUE;
-}
-
-ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) {
-#if defined(_MSC_VER)
- ifstream* file = new ifstream();
- file->open(UTF8ToWide(filename).c_str(), mode);
-#else // GCC
- ifstream* file =
- new ifstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode);
-#endif // _MSC_VER
-
- return file;
-}
-
-ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) {
-#if defined(_MSC_VER)
- ofstream* file = new ofstream();
- file->open(UTF8ToWide(filename).c_str(), mode);
-#else // GCC
- ofstream* file =
- new ofstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode);
-#endif // _MSC_VER
-
- return file;
-}
-
-struct FileData {
- FILETIME timestamp;
- wstring path;
-};
-
-static bool CompareFDTime(const FileData& fd1, const FileData& fd2) {
- return CompareFileTime(&fd1.timestamp, &fd2.timestamp) > 0;
-}
-
-void UIPruneSavedDumps(const std::string& directory) {
- wstring wdirectory = UTF8ToWide(directory);
-
- WIN32_FIND_DATA fdata;
- wstring findpath = wdirectory + L"\\*.dmp";
- HANDLE dirlist = FindFirstFile(findpath.c_str(), &fdata);
- if (dirlist == INVALID_HANDLE_VALUE) return;
-
- vector<FileData> dumpfiles;
-
- for (BOOL ok = true; ok; ok = FindNextFile(dirlist, &fdata)) {
- FileData fd = {fdata.ftLastWriteTime, wdirectory + L"\\" + fdata.cFileName};
- dumpfiles.push_back(fd);
- }
-
- sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime);
-
- while (dumpfiles.size() > kSaveCount) {
- // get the path of the oldest file
- wstring path = (--dumpfiles.end())->path;
- DeleteFile(path.c_str());
-
- // s/.dmp/.extra/
- path.replace(path.size() - 4, 4, L".extra");
- DeleteFile(path.c_str());
-
- dumpfiles.pop_back();
- }
- FindClose(dirlist);
-}
-
-bool UIRunProgram(const string& exename, const std::vector<std::string>& args,
- bool wait) {
- wstring cmdLine = L"\"" + UTF8ToWide(exename) + L"\" ";
-
- for (auto arg : args) {
- cmdLine += L"\"" + UTF8ToWide(arg) + L"\" ";
- }
-
- STARTUPINFO si = {};
- si.cb = sizeof(si);
- PROCESS_INFORMATION pi = {};
-
- if (!CreateProcess(/* lpApplicationName */ nullptr, (LPWSTR)cmdLine.c_str(),
- /* lpProcessAttributes */ nullptr,
- /* lpThreadAttributes */ nullptr,
- /* bInheritHandles */ false,
- NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,
- /* lpEnvironment */ nullptr,
- /* lpCurrentDirectory */ nullptr, &si, &pi)) {
- return false;
- }
-
- if (wait) {
- WaitForSingleObject(pi.hProcess, INFINITE);
- }
-
- CloseHandle(pi.hProcess);
- CloseHandle(pi.hThread);
- return true;
-}
-
-string UIGetEnv(const string& name) {
- const wchar_t* var = _wgetenv(UTF8ToWide(name).c_str());
- if (var && *var) {
- return WideToUTF8(var);
- }
-
- return "";
-}
diff --git a/toolkit/crashreporter/client/gtkbind/Cargo.toml b/toolkit/crashreporter/client/gtkbind/Cargo.toml
new file mode 100644
index 0000000000..813d43dbe3
--- /dev/null
+++ b/toolkit/crashreporter/client/gtkbind/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "gtkbind"
+version = "0.1.0"
+edition = "2021"
+
+[build-dependencies]
+bindgen = { version = "0.69.0", default-features = false, features = ["runtime"] }
+mozbuild = "0.1.0"
diff --git a/toolkit/crashreporter/client/gtkbind/build.rs b/toolkit/crashreporter/client/gtkbind/build.rs
new file mode 100644
index 0000000000..fa3402fdf2
--- /dev/null
+++ b/toolkit/crashreporter/client/gtkbind/build.rs
@@ -0,0 +1,43 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+use mozbuild::config::{
+ CC_BASE_FLAGS as CFLAGS, MOZ_GTK3_CFLAGS as GTK_CFLAGS, MOZ_GTK3_LIBS as GTK_LIBS,
+};
+
+const HEADER: &str = r#"
+#include "gtk/gtk.h"
+#include "pango/pango.h"
+#include "gdk-pixbuf/gdk-pixbuf.h"
+"#;
+
+fn main() {
+ let bindings = bindgen::Builder::default()
+ .header_contents("gtk_bindings.h", HEADER)
+ .clang_args(CFLAGS)
+ .clang_args(GTK_CFLAGS)
+ .allowlist_function("gtk_.*")
+ .allowlist_function(
+ "g_(application|main_context|memory_input_stream|object|signal|timeout)_.*",
+ )
+ .allowlist_function("gdk_pixbuf_new_from_stream")
+ .allowlist_function("pango_attr_.*")
+ // The gtk/glib valist functions generate FFI-unsafe signatures on aarch64 which cause
+ // compile errors. We don't use them anyway.
+ .blocklist_function(".*_valist")
+ .derive_default(true)
+ .generate()
+ .expect("unable to generate gtk bindings");
+ for flag in GTK_LIBS {
+ if let Some(lib) = flag.strip_prefix("-l") {
+ println!("cargo:rustc-link-lib={lib}");
+ } else if let Some(path) = flag.strip_prefix("-L") {
+ println!("cargo:rustc-link-search={path}");
+ }
+ }
+ let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("gtk_bindings.rs"))
+ .expect("failed to write gtk bindings");
+}
diff --git a/toolkit/crashreporter/process_reader/src/process_reader.rs b/toolkit/crashreporter/client/gtkbind/src/lib.rs
index 1473aafa09..714f3ed047 100644
--- a/toolkit/crashreporter/process_reader/src/process_reader.rs
+++ b/toolkit/crashreporter/client/gtkbind/src/lib.rs
@@ -2,5 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-#[cfg(target_os = "windows")]
-mod windows;
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(concat!(env!("OUT_DIR"), "/gtk_bindings.rs"));
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
deleted file mode 100644
index 254131e431..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>IBClasses</key>
- <array>
- <dict>
- <key>ACTIONS</key>
- <dict>
- <key>closeClicked</key>
- <string>id</string>
- <key>includeURLClicked</key>
- <string>id</string>
- <key>restartClicked</key>
- <string>id</string>
- <key>submitReportClicked</key>
- <string>id</string>
- <key>viewReportClicked</key>
- <string>id</string>
- <key>viewReportOkClicked</key>
- <string>id</string>
- </dict>
- <key>CLASS</key>
- <string>CrashReporterUI</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>OUTLETS</key>
- <dict>
- <key>mCloseButton</key>
- <string>NSButton</string>
- <key>mCommentScrollView</key>
- <string>NSScrollView</string>
- <key>mCommentText</key>
- <string>TextViewWithPlaceHolder</string>
- <key>mDescriptionLabel</key>
- <string>NSTextField</string>
- <key>mEmailMeButton</key>
- <string>NSButton</string>
- <key>mEmailText</key>
- <string>NSTextField</string>
- <key>mErrorCloseButton</key>
- <string>NSButton</string>
- <key>mErrorHeaderLabel</key>
- <string>NSTextField</string>
- <key>mErrorLabel</key>
- <string>NSTextField</string>
- <key>mErrorView</key>
- <string>NSView</string>
- <key>mHeaderLabel</key>
- <string>NSTextField</string>
- <key>mIncludeURLButton</key>
- <string>NSButton</string>
- <key>mProgressIndicator</key>
- <string>NSProgressIndicator</string>
- <key>mProgressText</key>
- <string>NSTextField</string>
- <key>mRestartButton</key>
- <string>NSButton</string>
- <key>mSubmitReportButton</key>
- <string>NSButton</string>
- <key>mViewReportButton</key>
- <string>NSButton</string>
- <key>mViewReportOkButton</key>
- <string>NSButton</string>
- <key>mViewReportTextView</key>
- <string>NSTextView</string>
- <key>mViewReportWindow</key>
- <string>NSWindow</string>
- <key>mWindow</key>
- <string>NSWindow</string>
- </dict>
- <key>SUPERCLASS</key>
- <string>NSObject</string>
- </dict>
- <dict>
- <key>ACTIONS</key>
- <dict>
- <key>insertTab</key>
- <string>id</string>
- </dict>
- <key>CLASS</key>
- <string>TextViewWithPlaceHolder</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>SUPERCLASS</key>
- <string>NSTextView</string>
- </dict>
- <dict>
- <key>CLASS</key>
- <string>FirstResponder</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>SUPERCLASS</key>
- <string>NSObject</string>
- </dict>
- </array>
- <key>IBVersion</key>
- <string>1</string>
-</dict>
-</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
deleted file mode 100644
index 517349ffce..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>IBFramework Version</key>
- <string>629</string>
- <key>IBOldestOS</key>
- <integer>5</integer>
- <key>IBOpenObjects</key>
- <array>
- <integer>2</integer>
- </array>
- <key>IBSystem Version</key>
- <string>9C7010</string>
- <key>targetFramework</key>
- <string>IBCocoaFramework</string>
-</dict>
-</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
deleted file mode 100644
index bfdcccb74c..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
+++ /dev/null
Binary files differ
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib
deleted file mode 100644
index 254131e431..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>IBClasses</key>
- <array>
- <dict>
- <key>ACTIONS</key>
- <dict>
- <key>closeClicked</key>
- <string>id</string>
- <key>includeURLClicked</key>
- <string>id</string>
- <key>restartClicked</key>
- <string>id</string>
- <key>submitReportClicked</key>
- <string>id</string>
- <key>viewReportClicked</key>
- <string>id</string>
- <key>viewReportOkClicked</key>
- <string>id</string>
- </dict>
- <key>CLASS</key>
- <string>CrashReporterUI</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>OUTLETS</key>
- <dict>
- <key>mCloseButton</key>
- <string>NSButton</string>
- <key>mCommentScrollView</key>
- <string>NSScrollView</string>
- <key>mCommentText</key>
- <string>TextViewWithPlaceHolder</string>
- <key>mDescriptionLabel</key>
- <string>NSTextField</string>
- <key>mEmailMeButton</key>
- <string>NSButton</string>
- <key>mEmailText</key>
- <string>NSTextField</string>
- <key>mErrorCloseButton</key>
- <string>NSButton</string>
- <key>mErrorHeaderLabel</key>
- <string>NSTextField</string>
- <key>mErrorLabel</key>
- <string>NSTextField</string>
- <key>mErrorView</key>
- <string>NSView</string>
- <key>mHeaderLabel</key>
- <string>NSTextField</string>
- <key>mIncludeURLButton</key>
- <string>NSButton</string>
- <key>mProgressIndicator</key>
- <string>NSProgressIndicator</string>
- <key>mProgressText</key>
- <string>NSTextField</string>
- <key>mRestartButton</key>
- <string>NSButton</string>
- <key>mSubmitReportButton</key>
- <string>NSButton</string>
- <key>mViewReportButton</key>
- <string>NSButton</string>
- <key>mViewReportOkButton</key>
- <string>NSButton</string>
- <key>mViewReportTextView</key>
- <string>NSTextView</string>
- <key>mViewReportWindow</key>
- <string>NSWindow</string>
- <key>mWindow</key>
- <string>NSWindow</string>
- </dict>
- <key>SUPERCLASS</key>
- <string>NSObject</string>
- </dict>
- <dict>
- <key>ACTIONS</key>
- <dict>
- <key>insertTab</key>
- <string>id</string>
- </dict>
- <key>CLASS</key>
- <string>TextViewWithPlaceHolder</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>SUPERCLASS</key>
- <string>NSTextView</string>
- </dict>
- <dict>
- <key>CLASS</key>
- <string>FirstResponder</string>
- <key>LANGUAGE</key>
- <string>ObjC</string>
- <key>SUPERCLASS</key>
- <string>NSObject</string>
- </dict>
- </array>
- <key>IBVersion</key>
- <string>1</string>
-</dict>
-</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib
deleted file mode 100644
index 4a2251aaf5..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>IBFramework Version</key>
- <string>629</string>
- <key>IBOldestOS</key>
- <integer>5</integer>
- <key>IBOpenObjects</key>
- <array>
- <integer>2</integer>
- </array>
- <key>IBSystem Version</key>
- <string>9D34</string>
- <key>targetFramework</key>
- <string>IBCocoaFramework</string>
-</dict>
-</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib
deleted file mode 100644
index 6c93849b94..0000000000
--- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib
+++ /dev/null
Binary files differ
diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build
deleted file mode 100644
index 82e19b8637..0000000000
--- a/toolkit/crashreporter/client/moz.build
+++ /dev/null
@@ -1,97 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-if CONFIG["OS_TARGET"] != "Android":
- Program("crashreporter")
-
- UNIFIED_SOURCES += [
- "../CrashAnnotations.cpp",
- "crashreporter.cpp",
- "ping.cpp",
- ]
-
- LOCAL_INCLUDES += [
- "/toolkit/components/jsoncpp/include",
- ]
-
- USE_LIBS += [
- "jsoncpp",
- ]
-
-if CONFIG["OS_ARCH"] == "WINNT":
- UNIFIED_SOURCES += [
- "crashreporter_win.cpp",
- ]
- include("/toolkit/crashreporter/breakpad-client/windows/sender/objs.mozbuild")
- SOURCES += objs_sender
- SOURCES += [
- "../google-breakpad/src/common/windows/http_upload.cc",
- ]
- DEFINES["UNICODE"] = True
- DEFINES["_UNICODE"] = True
- USE_LIBS += [
- "nss",
- ]
- OS_LIBS += [
- "advapi32",
- "comctl32",
- "gdi32",
- "ole32",
- "shell32",
- "wininet",
- "shlwapi",
- "user32",
- ]
-elif CONFIG["OS_ARCH"] == "Darwin":
- UNIFIED_SOURCES += [
- "../google-breakpad/src/common/mac/HTTPMultipartUpload.m",
- "crashreporter_osx.mm",
- "crashreporter_unix_common.cpp",
- ]
- LOCAL_INCLUDES += [
- "../google-breakpad/src/common/mac",
- ]
- OS_LIBS += ["-framework Cocoa"]
- USE_LIBS += [
- "nss",
- ]
- LDFLAGS += ["-Wl,-rpath,@executable_path/../../../"]
-elif CONFIG["OS_ARCH"] == "SunOS":
- SOURCES += [
- "crashreporter_linux.cpp",
- "crashreporter_unix.cpp",
- ]
- USE_LIBS += [
- "breakpad_solaris_common_s",
- ]
-
-if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
- UNIFIED_SOURCES += [
- "../google-breakpad/src/common/linux/http_upload.cc",
- "crashreporter_gtk_common.cpp",
- "crashreporter_linux.cpp",
- "crashreporter_unix_common.cpp",
- ]
- OS_LIBS += CONFIG["MOZ_GTK3_LIBS"]
- OS_LIBS += CONFIG["MOZ_GTHREAD_LIBS"]
- CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
- CXXFLAGS += CONFIG["MOZ_GTHREAD_CFLAGS"]
-
-if CONFIG["OS_ARCH"] == "Linux" or CONFIG["OS_ARCH"] == "SunOS":
- FINAL_TARGET_FILES += [
- "Throbber-small.gif",
- ]
-
-DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"]
-DEFINES["BIN_SUFFIX"] = '"%s"' % CONFIG["BIN_SUFFIX"]
-
-RCINCLUDE = "crashreporter.rc"
-
-# Don't use the STL wrappers in the crashreporter clients; they don't
-# link with -lmozalloc, and it really doesn't matter here anyway.
-DisableStlWrapping()
-
-include("/toolkit/crashreporter/crashreporter.mozbuild")
diff --git a/toolkit/crashreporter/client/ping.cpp b/toolkit/crashreporter/client/ping.cpp
deleted file mode 100644
index b49211c9c1..0000000000
--- a/toolkit/crashreporter/client/ping.cpp
+++ /dev/null
@@ -1,324 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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/. */
-
-#include "crashreporter.h"
-
-#include <cstring>
-#include <ctime>
-#include <string>
-
-#if defined(XP_LINUX)
-# include <fcntl.h>
-# include <unistd.h>
-# include <sys/stat.h>
-#elif defined(XP_MACOSX)
-# include <CoreFoundation/CoreFoundation.h>
-#elif defined(XP_WIN)
-# include <objbase.h>
-#endif
-
-#include "json/json.h"
-
-#include "CrashAnnotations.h"
-
-using std::string;
-
-namespace CrashReporter {
-
-struct UUID {
- uint32_t m0;
- uint16_t m1;
- uint16_t m2;
- uint8_t m3[8];
-};
-
-// Generates an UUID; the code here is mostly copied from nsUUIDGenerator.cpp
-static string GenerateUUID() {
- UUID id = {};
-
-#if defined(XP_WIN) // Windows
- HRESULT hr = CoCreateGuid((GUID*)&id);
- if (FAILED(hr)) {
- return "";
- }
-#elif defined(XP_MACOSX) // MacOS X
- CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
- if (!uuid) {
- return "";
- }
-
- CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuid);
- memcpy(&id, &bytes, sizeof(UUID));
-
- CFRelease(uuid);
-#elif defined(HAVE_ARC4RANDOM_BUF) // Android, BSD, ...
- arc4random_buf(&id, sizeof(UUID));
-#else // Linux
- int fd = open("/dev/urandom", O_RDONLY);
-
- if (fd == -1) {
- return "";
- }
-
- if (read(fd, &id, sizeof(UUID)) != sizeof(UUID)) {
- close(fd);
- return "";
- }
-
- close(fd);
-#endif
-
- /* Put in the version */
- id.m2 &= 0x0fff;
- id.m2 |= 0x4000;
-
- /* Put in the variant */
- id.m3[0] &= 0x3f;
- id.m3[0] |= 0x80;
-
- const char* kUUIDFormatString =
- "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x";
- const size_t kUUIDFormatStringLength = 36;
- char str[kUUIDFormatStringLength + 1] = {'\0'};
-
- int num = snprintf(str, kUUIDFormatStringLength + 1, kUUIDFormatString, id.m0,
- id.m1, id.m2, id.m3[0], id.m3[1], id.m3[2], id.m3[3],
- id.m3[4], id.m3[5], id.m3[6], id.m3[7]);
-
- if (num != kUUIDFormatStringLength) {
- return "";
- }
-
- return str;
-}
-
-const char kISO8601Date[] = "%F";
-const char kISO8601DateHours[] = "%FT%H:00:00.000Z";
-
-// Return the current date as a string in the specified format, the following
-// constants are provided:
-// - kISO8601Date, the ISO 8601 date format, YYYY-MM-DD
-// - kISO8601DateHours, the ISO 8601 full date format, YYYY-MM-DDTHH:00:00.000Z
-static string CurrentDate(string format) {
- time_t now;
- time(&now);
- char buf[64]; // This should be plenty
- strftime(buf, sizeof buf, format.c_str(), gmtime(&now));
- return buf;
-}
-
-const char kTelemetryClientId[] = "TelemetryClientId";
-const char kTelemetryUrl[] = "TelemetryServerURL";
-const char kTelemetrySessionId[] = "TelemetrySessionId";
-const int kTelemetryVersion = 4;
-
-// Create the payload.metadata node of the crash ping using fields extracted
-// from the .extra file
-static Json::Value CreateMetadataNode(const Json::Value& aExtra) {
- Json::Value node;
-
- for (Json::ValueConstIterator iter = aExtra.begin(); iter != aExtra.end();
- ++iter) {
- Annotation annotation;
-
- if (AnnotationFromString(annotation, iter.memberName())) {
- if (IsAnnotationAllowedForPing(annotation)) {
- node[iter.memberName()] = *iter;
- }
- }
- }
-
- return node;
-}
-
-// Create the payload node of the crash ping
-static Json::Value CreatePayloadNode(const Json::Value& aExtra,
- const string& aHash,
- const string& aSessionId) {
- Json::Value payload;
-
- payload["sessionId"] = aSessionId;
- payload["version"] = 1;
- payload["crashDate"] = CurrentDate(kISO8601Date);
- payload["crashTime"] = CurrentDate(kISO8601DateHours);
- payload["hasCrashEnvironment"] = true;
- payload["crashId"] = CrashReporter::GetDumpLocalID();
- payload["minidumpSha256Hash"] = aHash;
- payload["processType"] = "main"; // This is always a main crash
- if (aExtra.isMember("StackTraces")) {
- payload["stackTraces"] = aExtra["StackTraces"];
- }
-
- // Assemble the payload metadata
- payload["metadata"] = CreateMetadataNode(aExtra);
-
- return payload;
-}
-
-// Create the application node of the crash ping
-static Json::Value CreateApplicationNode(
- const string& aVendor, const string& aName, const string& aVersion,
- const string& aDisplayVersion, const string& aPlatformVersion,
- const string& aChannel, const string& aBuildId, const string& aArchitecture,
- const string& aXpcomAbi) {
- Json::Value application;
-
- application["vendor"] = aVendor;
- application["name"] = aName;
- application["buildId"] = aBuildId;
- application["displayVersion"] = aDisplayVersion;
- application["platformVersion"] = aPlatformVersion;
- application["version"] = aVersion;
- application["channel"] = aChannel;
- if (!aArchitecture.empty()) {
- application["architecture"] = aArchitecture;
- }
- if (!aXpcomAbi.empty()) {
- application["xpcomAbi"] = aXpcomAbi;
- }
-
- return application;
-}
-
-// Create the root node of the crash ping
-static Json::Value CreateRootNode(
- const Json::Value& aExtra, const string& aUuid, const string& aHash,
- const string& aClientId, const string& aSessionId, const string& aName,
- const string& aVersion, const string& aChannel, const string& aBuildId) {
- Json::Value root;
- root["type"] = "crash"; // This is a crash ping
- root["id"] = aUuid;
- root["version"] = kTelemetryVersion;
- root["creationDate"] = CurrentDate(kISO8601DateHours);
- root["clientId"] = aClientId;
-
- // Parse the telemetry environment
- Json::Value environment;
- Json::Reader reader;
- string architecture;
- string xpcomAbi;
- string displayVersion;
- string platformVersion;
-
- if (reader.parse(aExtra["TelemetryEnvironment"].asString(), environment,
- /* collectComments */ false)) {
- if (environment.isMember("build") && environment["build"].isObject()) {
- Json::Value build = environment["build"];
- if (build.isMember("architecture") && build["architecture"].isString()) {
- architecture = build["architecture"].asString();
- }
- if (build.isMember("xpcomAbi") && build["xpcomAbi"].isString()) {
- xpcomAbi = build["xpcomAbi"].asString();
- }
- if (build.isMember("displayVersion") &&
- build["displayVersion"].isString()) {
- displayVersion = build["displayVersion"].asString();
- }
- if (build.isMember("platformVersion") &&
- build["platformVersion"].isString()) {
- platformVersion = build["platformVersion"].asString();
- }
- }
-
- root["environment"] = environment;
- }
-
- root["payload"] = CreatePayloadNode(aExtra, aHash, aSessionId);
- root["application"] = CreateApplicationNode(
- aExtra["Vendor"].asString(), aName, aVersion, displayVersion,
- platformVersion, aChannel, aBuildId, architecture, xpcomAbi);
-
- return root;
-}
-
-// Generates the URL used to submit the crash ping, see TelemetrySend.sys.mjs
-string GenerateSubmissionUrl(const string& aUrl, const string& aId,
- const string& aName, const string& aVersion,
- const string& aChannel, const string& aBuildId) {
- return aUrl + "/submit/telemetry/" + aId + "/crash/" + aName + "/" +
- aVersion + "/" + aChannel + "/" + aBuildId +
- "?v=" + std::to_string(kTelemetryVersion);
-}
-
-// Write out the ping into the specified file.
-//
-// Returns true if the ping was written out successfully, false otherwise.
-static bool WritePing(const string& aPath, const string& aPing) {
- std::ofstream* f = UIOpenWrite(aPath, std::ios::trunc);
- bool success = false;
-
- if (f->is_open()) {
- *f << aPing;
- f->close();
- success = f->good();
- }
-
- delete f;
- return success;
-}
-
-// Assembles the crash ping using the JSON data extracted from the .extra file
-// and sends it using the crash sender. All the telemetry specific data but the
-// environment will be stripped from the annotations so that it won't be sent
-// together with the crash report.
-//
-// Note that the crash ping sender is invoked in a fire-and-forget way so this
-// won't block waiting for the ping to be delivered.
-//
-// Returns true if the ping was assembled and handed over to the pingsender
-// correctly, also populates the aPingUuid parameter with the ping UUID. Returns
-// false otherwise and leaves the aPingUuid parameter unmodified.
-bool SendCrashPing(Json::Value& aExtra, const string& aHash, string& aPingUuid,
- const string& pingDir) {
- // Remove the telemetry-related data from the crash annotations
- Json::Value value;
- aExtra.removeMember(kTelemetryClientId, &value);
- string clientId = value.asString();
- aExtra.removeMember(kTelemetryUrl, &value);
- string serverUrl = value.asString();
- aExtra.removeMember(kTelemetrySessionId, &value);
- string sessionId = value.asString();
-
- if (clientId.empty() || serverUrl.empty() || sessionId.empty()) {
- return false;
- }
-
- string buildId = aExtra["BuildID"].asString();
- string channel = aExtra["ReleaseChannel"].asString();
- string name = aExtra["ProductName"].asString();
- string version = aExtra["Version"].asString();
- string uuid = GenerateUUID();
- string url =
- GenerateSubmissionUrl(serverUrl, uuid, name, version, channel, buildId);
-
- if (serverUrl.empty() || uuid.empty()) {
- return false;
- }
-
- Json::Value root = CreateRootNode(aExtra, uuid, aHash, clientId, sessionId,
- name, version, channel, buildId);
-
- // Write out the result to the pending pings directory
- Json::StreamWriterBuilder builder;
- builder["indentation"] = "";
- string ping = Json::writeString(builder, root);
- string pingPath = pingDir + UI_DIR_SEPARATOR + uuid + ".json";
-
- if (!WritePing(pingPath, ping)) {
- return false;
- }
-
- // Hand over the ping to the sender
- std::vector<string> args = {url, pingPath};
- if (UIRunProgram(CrashReporter::GetProgramPath(UI_PING_SENDER_FILENAME),
- args)) {
- aPingUuid = uuid;
- return true;
- } else {
- return false;
- }
-}
-
-} // namespace CrashReporter
diff --git a/toolkit/crashreporter/client/resource.h b/toolkit/crashreporter/client/resource.h
deleted file mode 100644
index 2e7917daa4..0000000000
--- a/toolkit/crashreporter/client/resource.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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/. */
-
-//{{NO_DEPENDENCIES}}
-// Microsoft Visual C++ generated include file.
-// Used by crashreporter.rc
-//
-#define IDD_SENDDIALOG 102
-#define IDR_THROBBER 103
-#define IDD_VIEWREPORTDIALOG 104
-#define IDI_MAINICON 105
-#define IDC_PROGRESS 1003
-#define IDC_DESCRIPTIONTEXT 1004
-#define IDC_CLOSEBUTTON 1005
-#define IDC_VIEWREPORTBUTTON 1006
-#define IDC_SUBMITREPORTCHECK 1007
-#define IDC_INCLUDEURLCHECK 1010
-#define IDC_COMMENTTEXT 1011
-#define IDC_RESTARTBUTTON 1012
-#define IDC_DESCRIPTIONLABEL 1013
-#define IDC_PROGRESSTEXT 1014
-#define IDC_THROBBER 1015
-#define IDC_VIEWREPORTTEXT 1016
-
-// Next default values for new objects
-//
-#ifdef APSTUDIO_INVOKED
-# ifndef APSTUDIO_READONLY_SYMBOLS
-# define _APS_NEXT_RESOURCE_VALUE 106
-# define _APS_NEXT_COMMAND_VALUE 40001
-# define _APS_NEXT_CONTROL_VALUE 1017
-# define _APS_NEXT_SYMED_VALUE 101
-# endif
-#endif
diff --git a/toolkit/crashreporter/docs/index.rst b/toolkit/crashreporter/docs/index.rst
index fe1af45d8f..d08c28fb92 100644
--- a/toolkit/crashreporter/docs/index.rst
+++ b/toolkit/crashreporter/docs/index.rst
@@ -258,6 +258,15 @@ Environment variables used internally
- ``MOZ_CRASHREPORTER_STRINGS_OVERRIDE`` - Overrides the path used to load the
.ini file holding the strings used in the crash reporter client UI.
+Environment variables used for development
+------------------------------------------
+
+Set these at build time (e.g. ``ac_add_options`` in ``.mozconfig``).
+
+- ``MOZ_CRASHREPORTER_MOCK`` - When set, causes the crash reporter client to
+ mock its interfaces to the system so that you can test the GUI behavior. The
+ GUI will not interact with the host system at all when this is set.
+
Other topics
============
diff --git a/toolkit/crashreporter/moz.build b/toolkit/crashreporter/moz.build
index 952ca405bb..2fb1071de9 100644
--- a/toolkit/crashreporter/moz.build
+++ b/toolkit/crashreporter/moz.build
@@ -67,10 +67,12 @@ if CONFIG["MOZ_CRASHREPORTER"]:
DIRS += ["rust_minidump_writer_linux"]
if CONFIG["OS_TARGET"] != "Android":
- DIRS += ["minidump-analyzer"]
+ DIRS += [
+ "client/app",
+ "minidump-analyzer",
+ ]
DIRS += [
- "client",
"mozannotation_client",
"mozannotation_server",
]
@@ -118,9 +120,12 @@ if CONFIG["MOZ_CRASHREPORTER"]:
DEFINES["MOZ_PHC"] = True
LOCAL_INCLUDES += [
+ "/toolkit/components/jsoncpp/include",
"google-breakpad/src",
]
+ USE_LIBS += ["jsoncpp"]
+
PYTHON_UNITTEST_MANIFESTS += [
"tools/python.toml",
]
diff --git a/toolkit/crashreporter/mozannotation_client/src/lib.rs b/toolkit/crashreporter/mozannotation_client/src/lib.rs
index df2bd5bf94..2df133c4b9 100644
--- a/toolkit/crashreporter/mozannotation_client/src/lib.rs
+++ b/toolkit/crashreporter/mozannotation_client/src/lib.rs
@@ -101,6 +101,9 @@ extern "C" {
#[cfg(target_os = "windows")]
pub const ANNOTATION_SECTION: &'static [u8; 8] = b"mozannot";
+#[cfg(target_os = "macos")]
+pub const ANNOTATION_SECTION: &'static [u8; 16] = b"mozannotation\0\0\0";
+
// TODO: Use the following constants in the assembly below when constant
// expressions are stabilized: https://github.com/rust-lang/rust/issues/93332
#[cfg(any(target_os = "linux", target_os = "android"))]
diff --git a/toolkit/crashreporter/mozannotation_server/Cargo.toml b/toolkit/crashreporter/mozannotation_server/Cargo.toml
index 3a5834a645..bd3fb283bf 100644
--- a/toolkit/crashreporter/mozannotation_server/Cargo.toml
+++ b/toolkit/crashreporter/mozannotation_server/Cargo.toml
@@ -8,17 +8,10 @@ license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-goblin = { version = "0.7", features = ["elf32", "elf64", "pe32", "pe64"] }
-memoffset = "0.8"
mozannotation_client = { path = "../mozannotation_client/" }
+process_reader = { path = "../process_reader/" }
thin-vec = { version = "0.2.7", features = ["gecko-ffi"] }
thiserror = "1.0.38"
[target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies]
-libc = "0.2"
-
-[target."cfg(target_os = \"windows\")".dependencies]
-winapi = { version = "0.3", features = ["minwindef", "memoryapi", "psapi"] }
-
-[target."cfg(target_os = \"macos\")".dependencies]
-mach2 = { version = "0.4" }
+memoffset = "0.8"
diff --git a/toolkit/crashreporter/mozannotation_server/src/errors.rs b/toolkit/crashreporter/mozannotation_server/src/errors.rs
index 037d432e5e..b8103ad4e4 100644
--- a/toolkit/crashreporter/mozannotation_server/src/errors.rs
+++ b/toolkit/crashreporter/mozannotation_server/src/errors.rs
@@ -5,77 +5,13 @@
use thiserror::Error;
#[derive(Debug, Error)]
-pub enum RetrievalError {
- #[error("The process handle/PID was invalid")]
- InvalidProcessHandle,
- #[error("Could not find the address of the annotations vector")]
- AnnotationTableNotFound(#[from] FindAnnotationsAddressError),
+pub enum AnnotationsRetrievalError {
+ #[error("Address was out of bounds")]
+ InvalidAddress,
#[error("Corrupt or wrong annotation table")]
InvalidAnnotationTable,
#[error("The data read from the target process is invalid")]
InvalidData,
- #[cfg(any(target_os = "linux", target_os = "android"))]
- #[error("Could not attach to the target process")]
- AttachError(#[from] PtraceError),
- #[error("Could not read from the target process address space")]
- ReadFromProcessError(#[from] ReadError),
- #[error("waitpid() failed when attaching to the process")]
- WaitPidError,
-}
-
-#[derive(Debug, Error)]
-pub enum FindAnnotationsAddressError {
- #[error("Could not convert address {0}")]
- ConvertAddressError(#[from] std::num::TryFromIntError),
- #[error("goblin failed to parse a module")]
- GoblinError(#[from] goblin::error::Error),
- #[error("Address was out of bounds")]
- InvalidAddress,
- #[error("IO error for file {0}")]
- IOError(#[from] std::io::Error),
- #[error("Could not find the address of the annotations vector")]
- NotFound,
- #[error("Could not parse address {0}")]
- ParseAddressError(#[from] std::num::ParseIntError),
- #[error("Could not parse a line in /proc/<pid>/maps")]
- ProcMapsParseError,
- #[cfg(any(target_os = "linux", target_os = "android"))]
- #[error("Program header was not found")]
- ProgramHeaderNotFound,
- #[cfg(target_os = "windows")]
- #[error("Section was not found")]
- SectionNotFound,
- #[cfg(target_os = "windows")]
- #[error("Cannot enumerate the target process's modules")]
- EnumProcessModulesError,
- #[error("Could not read memory from the target process")]
- ReadError(#[from] ReadError),
- #[cfg(target_os = "macos")]
- #[error("Failure when requesting the task information")]
- TaskInfoError,
- #[cfg(target_os = "macos")]
- #[error("The task dyld information format is unknown or invalid")]
- ImageFormatError,
-}
-
-#[derive(Debug, Error)]
-pub enum ReadError {
- #[cfg(any(target_os = "linux", target_os = "android"))]
- #[error("ptrace-specific error")]
- PtraceError(#[from] PtraceError),
- #[cfg(target_os = "windows")]
- #[error("ReadProcessMemory failed")]
- ReadProcessMemoryError,
- #[cfg(target_os = "macos")]
- #[error("mach call failed")]
- MachError,
-}
-
-#[cfg(any(target_os = "linux", target_os = "android"))]
-#[derive(Debug, Error)]
-pub enum PtraceError {
- #[error("Could not read from the target process address space")]
- ReadError(#[source] std::io::Error),
- #[error("Could not trace the process")]
- TraceError(#[source] std::io::Error),
+ #[error("Could not execute operation on the target process")]
+ ProcessReaderError(#[from] process_reader::error::ProcessReaderError),
}
diff --git a/toolkit/crashreporter/mozannotation_server/src/lib.rs b/toolkit/crashreporter/mozannotation_server/src/lib.rs
index 6ed5b3cc41..27682247f8 100644
--- a/toolkit/crashreporter/mozannotation_server/src/lib.rs
+++ b/toolkit/crashreporter/mozannotation_server/src/lib.rs
@@ -3,12 +3,18 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
mod errors;
-mod process_reader;
use crate::errors::*;
+#[cfg(any(target_os = "linux", target_os = "android"))]
+use memoffset::offset_of;
+use process_reader::error::ProcessReaderError;
use process_reader::ProcessReader;
+#[cfg(any(target_os = "windows", target_os = "macos"))]
+use mozannotation_client::ANNOTATION_SECTION;
use mozannotation_client::{Annotation, AnnotationContents, AnnotationMutex};
+#[cfg(any(target_os = "linux", target_os = "android"))]
+use mozannotation_client::{MozAnnotationNote, ANNOTATION_TYPE};
use std::cmp::min;
use std::iter::FromIterator;
use std::mem::{size_of, ManuallyDrop};
@@ -29,12 +35,7 @@ pub struct CAnnotation {
data: AnnotationData,
}
-#[cfg(target_os = "windows")]
-type ProcessHandle = winapi::shared::ntdef::HANDLE;
-#[cfg(any(target_os = "linux", target_os = "android"))]
-type ProcessHandle = libc::pid_t;
-#[cfg(any(target_os = "macos"))]
-type ProcessHandle = mach2::mach_types::task_t;
+pub type ProcessHandle = process_reader::ProcessHandle;
/// Return the annotations of a given process.
///
@@ -68,19 +69,23 @@ pub unsafe extern "C" fn mozannotation_free(ptr: *mut ThinVec<CAnnotation>) {
pub fn retrieve_annotations(
process: ProcessHandle,
max_annotations: usize,
-) -> Result<Box<ThinVec<CAnnotation>>, RetrievalError> {
+) -> Result<Box<ThinVec<CAnnotation>>, AnnotationsRetrievalError> {
let reader = ProcessReader::new(process)?;
- let address = reader.find_annotations()?;
+ let address = find_annotations(&reader)?;
- let mut mutex = reader.copy_object_shallow::<AnnotationMutex>(address)?;
+ let mut mutex = reader
+ .copy_object_shallow::<AnnotationMutex>(address)
+ .map_err(ProcessReaderError::from)?;
let mutex = unsafe { mutex.assume_init_mut() };
// TODO: we should clear the poison value here before getting the mutex
// contents. Right now we have to fail if the mutex was poisoned.
- let annotation_table = mutex.get_mut().map_err(|_e| RetrievalError::InvalidData)?;
+ let annotation_table = mutex
+ .get_mut()
+ .map_err(|_e| AnnotationsRetrievalError::InvalidData)?;
if !annotation_table.verify() {
- return Err(RetrievalError::InvalidAnnotationTable);
+ return Err(AnnotationsRetrievalError::InvalidAnnotationTable);
}
let vec_pointer = annotation_table.get_ptr();
@@ -97,8 +102,47 @@ pub fn retrieve_annotations(
Ok(Box::new(annotations))
}
+fn find_annotations(reader: &ProcessReader) -> Result<usize, AnnotationsRetrievalError> {
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ {
+ let libxul_address = reader.find_module("libxul.so")?;
+ let note_address = reader.find_program_note(
+ libxul_address,
+ ANNOTATION_TYPE,
+ size_of::<MozAnnotationNote>(),
+ )?;
+
+ let note = reader
+ .copy_object::<MozAnnotationNote>(note_address)
+ .map_err(ProcessReaderError::from)?;
+ let desc = note.desc;
+ let ehdr = (-note.ehdr) as usize;
+ let offset = desc + ehdr
+ - (offset_of!(MozAnnotationNote, ehdr) - offset_of!(MozAnnotationNote, desc));
+
+ usize::checked_add(libxul_address, offset).ok_or(AnnotationsRetrievalError::InvalidAddress)
+ }
+ #[cfg(any(target_os = "macos"))]
+ {
+ let libxul_address = reader.find_module("XUL")?;
+ reader
+ .find_section(libxul_address, ANNOTATION_SECTION)
+ .map_err(AnnotationsRetrievalError::from)
+ }
+ #[cfg(any(target_os = "windows"))]
+ {
+ let libxul_address = reader.find_module("xul.dll")?;
+ reader
+ .find_section(libxul_address, ANNOTATION_SECTION)
+ .map_err(AnnotationsRetrievalError::from)
+ }
+}
+
// Read an annotation from the given address
-fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation, ReadError> {
+fn read_annotation(
+ reader: &ProcessReader,
+ address: usize,
+) -> Result<CAnnotation, process_reader::error::ReadError> {
let raw_annotation = ManuallyDrop::new(reader.copy_object::<Annotation>(address)?);
let mut annotation = CAnnotation {
id: raw_annotation.id,
@@ -116,16 +160,16 @@ fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation
annotation.data = AnnotationData::ByteBuffer(string);
}
AnnotationContents::CStringPointer => {
- let buffer = copy_null_terminated_string_pointer(reader, raw_annotation.address)?;
- annotation.data = AnnotationData::ByteBuffer(buffer);
+ let string = copy_null_terminated_string_pointer(reader, raw_annotation.address)?;
+ annotation.data = AnnotationData::ByteBuffer(string);
}
AnnotationContents::CString => {
- let buffer = copy_null_terminated_string(reader, raw_annotation.address)?;
- annotation.data = AnnotationData::ByteBuffer(buffer);
+ let string = copy_null_terminated_string(reader, raw_annotation.address)?;
+ annotation.data = AnnotationData::ByteBuffer(string);
}
AnnotationContents::ByteBuffer(size) | AnnotationContents::OwnedByteBuffer(size) => {
- let buffer = copy_bytebuffer(reader, raw_annotation.address, size)?;
- annotation.data = AnnotationData::ByteBuffer(buffer);
+ let string = copy_bytebuffer(reader, raw_annotation.address, size)?;
+ annotation.data = AnnotationData::ByteBuffer(string);
}
};
@@ -135,7 +179,7 @@ fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation
fn copy_null_terminated_string_pointer(
reader: &ProcessReader,
address: usize,
-) -> Result<ThinVec<u8>, ReadError> {
+) -> Result<ThinVec<u8>, process_reader::error::ReadError> {
let buffer_address = reader.copy_object::<usize>(address)?;
copy_null_terminated_string(reader, buffer_address)
}
@@ -143,56 +187,15 @@ fn copy_null_terminated_string_pointer(
fn copy_null_terminated_string(
reader: &ProcessReader,
address: usize,
-) -> Result<ThinVec<u8>, ReadError> {
- // Try copying the string word-by-word first, this is considerably faster
- // than one byte at a time.
- if let Ok(string) = copy_null_terminated_string_word_by_word(reader, address) {
- return Ok(string);
- }
-
- // Reading the string one word at a time failed, let's try again one byte
- // at a time. It's slow but it might work in situations where the string
- // alignment causes word-by-word access to straddle page boundaries.
- let mut length = 0;
- let mut string = ThinVec::<u8>::new();
-
- loop {
- let char = reader.copy_object::<u8>(address + length)?;
- length += 1;
- string.push(char);
-
- if char == 0 {
- break;
- }
- }
-
- Ok(string)
+) -> Result<ThinVec<u8>, process_reader::error::ReadError> {
+ let string = reader.copy_null_terminated_string(address)?;
+ Ok(ThinVec::<u8>::from(string.as_bytes()))
}
-fn copy_null_terminated_string_word_by_word(
+fn copy_nscstring(
reader: &ProcessReader,
address: usize,
-) -> Result<ThinVec<u8>, ReadError> {
- const WORD_SIZE: usize = size_of::<usize>();
- let mut length = 0;
- let mut string = ThinVec::<u8>::new();
-
- loop {
- let array = reader.copy_array::<u8>(address + length, WORD_SIZE)?;
- let null_terminator = array.iter().position(|&e| e == 0);
- length += null_terminator.unwrap_or(WORD_SIZE);
- string.extend(array.into_iter());
-
- if null_terminator.is_some() {
- string.truncate(length);
- break;
- }
- }
-
- Ok(string)
-}
-
-fn copy_nscstring(reader: &ProcessReader, address: usize) -> Result<ThinVec<u8>, ReadError> {
+) -> Result<ThinVec<u8>, process_reader::error::ReadError> {
// HACK: This assumes the layout of the nsCString object
let length_address = address + size_of::<usize>();
let length = reader.copy_object::<u32>(length_address)?;
@@ -211,7 +214,7 @@ fn copy_bytebuffer(
reader: &ProcessReader,
address: usize,
size: u32,
-) -> Result<ThinVec<u8>, ReadError> {
+) -> Result<ThinVec<u8>, process_reader::error::ReadError> {
let value = reader.copy_array::<u8>(address, size as _)?;
Ok(ThinVec::<u8>::from_iter(value.into_iter()))
}
diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs b/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs
deleted file mode 100644
index ffdcd95937..0000000000
--- a/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs
+++ /dev/null
@@ -1,193 +0,0 @@
-/* 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 https://mozilla.org/MPL/2.0/. */
-
-use std::{
- convert::TryInto,
- mem::{size_of, MaybeUninit},
- ptr::null_mut,
-};
-
-use winapi::{
- shared::{
- basetsd::SIZE_T,
- minwindef::{DWORD, FALSE, HMODULE},
- },
- um::{
- memoryapi::ReadProcessMemory,
- psapi::{K32EnumProcessModules, K32GetModuleInformation, MODULEINFO},
- },
-};
-
-use crate::{
- errors::{FindAnnotationsAddressError, ReadError, RetrievalError},
- ProcessHandle,
-};
-
-use super::ProcessReader;
-
-impl ProcessReader {
- pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> {
- Ok(ProcessReader { process })
- }
-
- pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> {
- let modules = self.get_module_list()?;
-
- modules
- .iter()
- .find_map(|&module| {
- self.get_module_info(module).and_then(|info| {
- self.find_annotations_in_module(
- info.lpBaseOfDll as usize,
- info.SizeOfImage as usize,
- )
- .ok()
- })
- })
- .ok_or(FindAnnotationsAddressError::InvalidAddress)
- }
-
- fn get_module_list(&self) -> Result<Vec<HMODULE>, FindAnnotationsAddressError> {
- let mut module_num: usize = 100;
- let mut required_buffer_size: DWORD = 0;
- let mut module_array = Vec::<MaybeUninit<HMODULE>>::with_capacity(module_num);
-
- loop {
- let buffer_size: DWORD = (module_num * size_of::<HMODULE>()).try_into()?;
- let res = unsafe {
- K32EnumProcessModules(
- self.process,
- module_array.as_mut_ptr() as *mut _,
- buffer_size,
- &mut required_buffer_size as *mut _,
- )
- };
-
- module_num = required_buffer_size as usize / size_of::<HMODULE>();
-
- if res == 0 {
- if required_buffer_size > buffer_size {
- module_array = Vec::<MaybeUninit<HMODULE>>::with_capacity(module_num);
- } else {
- return Err(FindAnnotationsAddressError::EnumProcessModulesError);
- }
- } else {
- break;
- }
- }
-
- // SAFETY: module_array has been filled by K32EnumProcessModules()
- let module_array: Vec<HMODULE> = unsafe {
- module_array.set_len(module_num);
- std::mem::transmute(module_array)
- };
-
- Ok(module_array)
- }
-
- fn get_module_info(&self, module: HMODULE) -> Option<MODULEINFO> {
- let mut info: MaybeUninit<MODULEINFO> = MaybeUninit::uninit();
- let res = unsafe {
- K32GetModuleInformation(
- self.process,
- module,
- info.as_mut_ptr(),
- size_of::<MODULEINFO>() as DWORD,
- )
- };
-
- if res == 0 {
- None
- } else {
- let info = unsafe { info.assume_init() };
- Some(info)
- }
- }
-
- fn find_annotations_in_module(
- &self,
- module_address: usize,
- size: usize,
- ) -> Result<usize, FindAnnotationsAddressError> {
- // We read only the first page from the module, this should be more than
- // enough to read the header and section list. In the future we might do
- // this incrementally but for now goblin requires an array to parse
- // so we can't do it just yet.
- let page_size = 4096;
- if size < page_size {
- // Don't try to read from the target module if it's too small
- return Err(FindAnnotationsAddressError::ReadError(
- ReadError::ReadProcessMemoryError,
- ));
- }
-
- let bytes = self.copy_array(module_address as _, 4096)?;
- let header = goblin::pe::header::Header::parse(&bytes)?;
-
- // Skip the PE header so we can parse the sections
- let optional_header_offset = header.dos_header.pe_pointer as usize
- + goblin::pe::header::SIZEOF_PE_MAGIC
- + goblin::pe::header::SIZEOF_COFF_HEADER;
- let offset =
- &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize);
-
- let sections = header.coff_header.sections(&bytes, offset)?;
-
- for section in sections {
- if section.name.eq(mozannotation_client::ANNOTATION_SECTION) {
- let address = module_address.checked_add(section.virtual_address as usize);
- return address.ok_or(FindAnnotationsAddressError::InvalidAddress);
- }
- }
-
- Err(FindAnnotationsAddressError::SectionNotFound)
- }
-
- pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> {
- let mut object = MaybeUninit::<T>::uninit();
- let res = unsafe {
- ReadProcessMemory(
- self.process,
- src as _,
- object.as_mut_ptr() as _,
- size_of::<T>() as SIZE_T,
- null_mut(),
- )
- };
-
- if res != FALSE {
- Ok(object)
- } else {
- Err(ReadError::ReadProcessMemoryError)
- }
- }
-
- pub fn copy_object<T>(&self, src: usize) -> Result<T, ReadError> {
- let object = self.copy_object_shallow(src)?;
- Ok(unsafe { object.assume_init() })
- }
-
- pub fn copy_array<T>(&self, src: usize, num: usize) -> Result<Vec<T>, ReadError> {
- let num_of_bytes = num * size_of::<T>();
- let mut array: Vec<MaybeUninit<T>> = Vec::with_capacity(num);
- let res = unsafe {
- ReadProcessMemory(
- self.process,
- src as _,
- array.as_mut_ptr() as _,
- num_of_bytes as SIZE_T,
- null_mut(),
- )
- };
-
- if res != FALSE {
- unsafe {
- array.set_len(num);
- Ok(std::mem::transmute(array))
- }
- } else {
- Err(ReadError::ReadProcessMemoryError)
- }
- }
-}
diff --git a/toolkit/crashreporter/mozwer-rust/Cargo.toml b/toolkit/crashreporter/mozwer-rust/Cargo.toml
index 561a10b015..89cf3d4356 100644
--- a/toolkit/crashreporter/mozwer-rust/Cargo.toml
+++ b/toolkit/crashreporter/mozwer-rust/Cargo.toml
@@ -7,7 +7,9 @@ license = "MPL-2.0"
[dependencies]
libc = "0.2.0"
-mozilla-central-workspace-hack = { version = "0.1", features = ["mozwer_s"], optional = true }
+mozilla-central-workspace-hack = { version = "0.1", features = [
+ "mozwer_s",
+], optional = true }
process_reader = { path = "../process_reader/" }
rust-ini = "0.10"
serde = { version = "1.0", features = ["derive"] }
@@ -23,6 +25,7 @@ features = [
"Win32_System_Com",
"Win32_System_Diagnostics_Debug",
"Win32_System_ErrorReporting",
+ "Win32_System_Memory",
"Win32_System_ProcessStatus",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
diff --git a/toolkit/crashreporter/mozwer-rust/lib.rs b/toolkit/crashreporter/mozwer-rust/lib.rs
index 198a14a34b..61fc894462 100644
--- a/toolkit/crashreporter/mozwer-rust/lib.rs
+++ b/toolkit/crashreporter/mozwer-rust/lib.rs
@@ -9,7 +9,7 @@ use serde::Serialize;
use serde_json::ser::to_writer;
use std::convert::TryInto;
use std::ffi::{c_void, OsString};
-use std::fs::{read_to_string, DirBuilder, File};
+use std::fs::{read_to_string, DirBuilder, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::mem::{size_of, transmute, zeroed};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
@@ -248,8 +248,9 @@ fn handle_main_process_crash(
fn handle_child_process_crash(crash_report: CrashReport, child_process: HANDLE) -> Result<()> {
let parent_process = get_parent_process(child_process)?;
let process_reader = ProcessReader::new(parent_process).map_err(|_e| ())?;
+ let libxul_address = process_reader.find_module("xul.dll").map_err(|_e| ())?;
let wer_notify_proc = process_reader
- .find_section("xul.dll", "mozwerpt")
+ .find_section(libxul_address, b"mozwerpt")
.map_err(|_e| ())?;
let wer_notify_proc = unsafe { transmute::<_, LPTHREAD_START_ROUTINE>(wer_notify_proc) };
@@ -524,8 +525,7 @@ impl ApplicationInformation {
let install_time = ApplicationInformation::get_install_time(
&crash_reports_dir,
&application_data.build_id,
- )
- .unwrap_or("0".to_string());
+ );
Ok(ApplicationInformation {
install_path,
@@ -596,10 +596,29 @@ impl ApplicationInformation {
}
}
- fn get_install_time(crash_reports_path: &Path, build_id: &str) -> Result<String> {
+ fn get_install_time(crash_reports_path: &Path, build_id: &str) -> String {
let file_name = "InstallTime".to_owned() + build_id;
let file_path = crash_reports_path.join(file_name);
- read_to_string(file_path).map_err(|_e| ())
+
+ // If the file isn't present we'll attempt to atomically create it and
+ // populate it. This code essentially matches the corresponding code in
+ // nsExceptionHandler.cpp SetupExtraData().
+ if let Ok(mut file) = OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open(&file_path)
+ {
+ // SAFETY: No risks in calling `time()` with a null pointer.
+ let _ = write!(&mut file, "{}", unsafe { time(null_mut()) }.to_string());
+ }
+
+ // As a last resort, if we can't read the file we fall back to the
+ // current time. This might cause us to overstate the number of users
+ // affected by a crash, but given it's very unlikely to hit this particular
+ // path it won't be a problem.
+ //
+ // SAFETY: No risks in calling `time()` with a null pointer.
+ read_to_string(&file_path).unwrap_or(unsafe { time(null_mut()) }.to_string())
}
}
diff --git a/toolkit/crashreporter/mozwer/moz.build b/toolkit/crashreporter/mozwer/moz.build
index 1f6418fd48..41aed9b59b 100644
--- a/toolkit/crashreporter/mozwer/moz.build
+++ b/toolkit/crashreporter/mozwer/moz.build
@@ -10,6 +10,13 @@ OS_LIBS += [
"advapi32",
"bcrypt",
]
+# Version string comparison is generally wrong, but by the time it would
+# actually matter, either bug 1489995 would be fixed, or the build would
+# require version >= 1.78.
+if CONFIG["RUSTC_VERSION"] and CONFIG["RUSTC_VERSION"] >= "1.78.0":
+ OS_LIBS += [
+ "synchronization",
+ ]
DEFFILE = "mozwer.def"
USE_STATIC_LIBS = True
diff --git a/toolkit/crashreporter/nsExceptionHandler.cpp b/toolkit/crashreporter/nsExceptionHandler.cpp
index aca266749a..868005a5c7 100644
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -7,6 +7,7 @@
#include "nsExceptionHandler.h"
#include "nsExceptionHandlerUtils.h"
+#include "json/json.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsComponentManagerUtils.h"
#include "nsDirectoryServiceDefs.h"
@@ -95,6 +96,9 @@
using mozilla::InjectCrashRunnable;
#endif
+#include <fstream>
+#include <optional>
+
#include <stdlib.h>
#include <time.h>
#include <prenv.h>
@@ -195,7 +199,7 @@ static const XP_CHAR dumpFileExtension[] = XP_TEXT(".dmp");
static const XP_CHAR extraFileExtension[] = XP_TEXT(".extra");
static const XP_CHAR memoryReportExtension[] = XP_TEXT(".memory.json.gz");
-static xpstring* defaultMemoryReportPath = nullptr;
+static std::optional<xpstring> defaultMemoryReportPath = {};
static const char kCrashMainID[] = "crash.main.3\n";
@@ -433,26 +437,26 @@ static void CreateFileFromPath(const xpstring& path, nsIFile** file) {
NS_NewLocalFile(nsDependentString(path.c_str()), false, file);
}
-static xpstring* CreatePathFromFile(nsIFile* file) {
+static std::optional<xpstring> CreatePathFromFile(nsIFile* file) {
nsAutoString path;
nsresult rv = file->GetPath(path);
if (NS_FAILED(rv)) {
- return nullptr;
+ return {};
}
- return new xpstring(static_cast<wchar_t*>(path.get()), path.Length());
+ return xpstring(static_cast<wchar_t*>(path.get()), path.Length());
}
#else
static void CreateFileFromPath(const xpstring& path, nsIFile** file) {
NS_NewNativeLocalFile(nsDependentCString(path.c_str()), false, file);
}
-MAYBE_UNUSED static xpstring* CreatePathFromFile(nsIFile* file) {
+MAYBE_UNUSED static std::optional<xpstring> CreatePathFromFile(nsIFile* file) {
nsAutoCString path;
nsresult rv = file->GetNativePath(path);
if (NS_FAILED(rv)) {
- return nullptr;
+ return {};
}
- return new xpstring(path.get(), path.Length());
+ return xpstring(path.get(), path.Length());
}
#endif
@@ -2252,7 +2256,7 @@ static nsresult SetupCrashReporterDirectory(nsIFile* aAppDataDirectory,
NS_ENSURE_SUCCESS(rv, rv);
EnsureDirectoryExists(directory);
- xpstring* directoryPath = CreatePathFromFile(directory);
+ std::optional<xpstring> directoryPath = CreatePathFromFile(directory);
if (!directoryPath) {
return NS_ERROR_FAILURE;
@@ -2264,8 +2268,6 @@ static nsresult SetupCrashReporterDirectory(nsIFile* aAppDataDirectory,
setenv(aEnvVarName, directoryPath->c_str(), /* overwrite */ 1);
#endif
- delete directoryPath;
-
if (aDirectory) {
directory.forget(aDirectory);
}
@@ -2760,177 +2762,54 @@ nsresult AppendObjCExceptionInfoToAppNotes(void* inException) {
*/
static nsresult PrefSubmitReports(bool* aSubmitReports, bool writePref) {
nsresult rv;
-#if defined(XP_WIN)
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_UNIX)
/*
- * NOTE! This needs to stay in sync with the preference checking code
- * in toolkit/crashreporter/client/crashreporter_win.cpp
+ * NOTE! This needs to stay in sync with the code in
+ * toolkit/crashreporter/client/app/src/{logic,settings}.rs
*/
- nsCOMPtr<nsIXULAppInfo> appinfo =
- do_GetService("@mozilla.org/xre/app-info;1", &rv);
- NS_ENSURE_SUCCESS(rv, rv);
-
- nsAutoCString appVendor, appName;
- rv = appinfo->GetVendor(appVendor);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = appinfo->GetName(appName);
+ nsCOMPtr<nsIFile> reporterSettings;
+ rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(reporterSettings));
NS_ENSURE_SUCCESS(rv, rv);
+ reporterSettings->AppendNative("Crash Reports"_ns);
+ reporterSettings->AppendNative("crashreporter_settings.json"_ns);
- nsCOMPtr<nsIWindowsRegKey> regKey(
- do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
- NS_ENSURE_SUCCESS(rv, rv);
-
- nsAutoCString regPath;
-
- regPath.AppendLiteral("Software\\");
-
- // We need to ensure the registry keys are created so we can properly
- // write values to it
-
- // Create appVendor key
- if (!appVendor.IsEmpty()) {
- regPath.Append(appVendor);
- regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_SET_VALUE);
- regPath.Append('\\');
- }
-
- // Create appName key
- regPath.Append(appName);
- regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_SET_VALUE);
- regPath.Append('\\');
-
- // Create Crash Reporter key
- regPath.AppendLiteral("Crash Reporter");
- regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_SET_VALUE);
-
- // If we're saving the pref value, just write it to ROOT_KEY_CURRENT_USER
- // and we're done.
- if (writePref) {
- rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_SET_VALUE);
- NS_ENSURE_SUCCESS(rv, rv);
-
- uint32_t value = *aSubmitReports ? 1 : 0;
- rv = regKey->WriteIntValue(u"SubmitCrashReport"_ns, value);
- regKey->Close();
- return rv;
- }
-
- // We're reading the pref value, so we need to first look under
- // ROOT_KEY_LOCAL_MACHINE to see if it's set there, and then fall back to
- // ROOT_KEY_CURRENT_USER. If it's not set in either place, the pref defaults
- // to "true".
- uint32_t value;
- rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_QUERY_VALUE);
- if (NS_SUCCEEDED(rv)) {
- rv = regKey->ReadIntValue(u"SubmitCrashReport"_ns, &value);
- regKey->Close();
- if (NS_SUCCEEDED(rv)) {
- *aSubmitReports = !!value;
- return NS_OK;
- }
- }
-
- rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
- NS_ConvertUTF8toUTF16(regPath),
- nsIWindowsRegKey::ACCESS_QUERY_VALUE);
- if (NS_FAILED(rv)) {
- *aSubmitReports = true;
- return NS_OK;
- }
+ std::optional<xpstring> file_path = CreatePathFromFile(reporterSettings);
- rv = regKey->ReadIntValue(u"SubmitCrashReport"_ns, &value);
- // default to true on failure
- if (NS_FAILED(rv)) {
- value = 1;
- rv = NS_OK;
+ if (!file_path) {
+ return NS_ERROR_FAILURE;
}
- regKey->Close();
- *aSubmitReports = !!value;
- return NS_OK;
-#elif defined(XP_MACOSX)
- rv = NS_OK;
- if (writePref) {
- CFPropertyListRef cfValue =
- (CFPropertyListRef)(*aSubmitReports ? kCFBooleanTrue : kCFBooleanFalse);
- ::CFPreferencesSetAppValue(CFSTR("submitReport"), cfValue,
- reporterClientAppID);
- if (!::CFPreferencesAppSynchronize(reporterClientAppID))
- rv = NS_ERROR_FAILURE;
- } else {
- *aSubmitReports = true;
- Boolean keyExistsAndHasValidFormat = false;
- Boolean prefValue = ::CFPreferencesGetAppBooleanValue(
- CFSTR("submitReport"), reporterClientAppID,
- &keyExistsAndHasValidFormat);
- if (keyExistsAndHasValidFormat) *aSubmitReports = !!prefValue;
- }
- return rv;
-#elif defined(XP_UNIX)
- /*
- * NOTE! This needs to stay in sync with the preference checking code
- * in toolkit/crashreporter/client/crashreporter_linux.cpp
- */
- nsCOMPtr<nsIFile> reporterINI;
- rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(reporterINI));
- NS_ENSURE_SUCCESS(rv, rv);
- reporterINI->AppendNative("Crash Reports"_ns);
- reporterINI->AppendNative("crashreporter.ini"_ns);
+ Json::Value root;
bool exists;
- rv = reporterINI->Exists(&exists);
+ rv = reporterSettings->Exists(&exists);
NS_ENSURE_SUCCESS(rv, rv);
if (!exists) {
if (!writePref) {
- // If reading the pref, default to true if .ini doesn't exist.
+ // If reading the pref, default to true if the settings file doesn't
+ // exist.
*aSubmitReports = true;
return NS_OK;
}
- // Create the file so the INI processor can write to it.
- rv = reporterINI->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+ // Create the file so the JSON processor can write to it.
+ rv = reporterSettings->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Read the root value
+ std::ifstream file(*file_path);
+ file >> root;
}
- nsCOMPtr<nsIINIParserFactory> iniFactory =
- do_GetService("@mozilla.org/xpcom/ini-parser-factory;1", &rv);
- NS_ENSURE_SUCCESS(rv, rv);
-
- nsCOMPtr<nsIINIParser> iniParser;
- rv = iniFactory->CreateINIParser(reporterINI, getter_AddRefs(iniParser));
- NS_ENSURE_SUCCESS(rv, rv);
-
- // If we're writing the pref, just set and we're done.
if (writePref) {
- nsCOMPtr<nsIINIParserWriter> iniWriter = do_QueryInterface(iniParser);
- NS_ENSURE_TRUE(iniWriter, NS_ERROR_FAILURE);
-
- rv = iniWriter->SetString("Crash Reporter"_ns, "SubmitReport"_ns,
- *aSubmitReports ? "1"_ns : "0"_ns);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = iniWriter->WriteFile(reporterINI);
- return rv;
- }
-
- nsAutoCString submitReportValue;
- rv = iniParser->GetString("Crash Reporter"_ns, "SubmitReport"_ns,
- submitReportValue);
-
- // Default to "true" if the pref can't be found.
- if (NS_FAILED(rv))
- *aSubmitReports = true;
- else if (submitReportValue.EqualsASCII("0"))
- *aSubmitReports = false;
- else
+ root["submit_report"] = *aSubmitReports;
+ std::ofstream file(*file_path);
+ file << root;
+ } else if (root["submit_report"].isBool()) {
+ *aSubmitReports = root["submit_report"].asBool();
+ } else {
+ // Default to "true" if the pref can't be found.
*aSubmitReports = true;
+ }
return NS_OK;
#else
@@ -2973,19 +2852,17 @@ static void SetCrashEventsDir(nsIFile* aDir) {
EnsureDirectoryExists(eventsDir);
}
- xpstring* path = CreatePathFromFile(eventsDir);
+ std::optional<xpstring> path = CreatePathFromFile(eventsDir);
if (!path) {
return; // There's no clean failure from this
}
- eventsDirectory = xpstring(*path);
+ eventsDirectory = *path;
#ifdef XP_WIN
SetEnvironmentVariableW(eventsDirectoryEnv, path->c_str());
#else
setenv(eventsDirectoryEnv, path->c_str(), /* overwrite */ 1);
#endif
-
- delete path;
}
void SetProfileDirectory(nsIFile* aDir) {
diff --git a/toolkit/crashreporter/process_reader/Cargo.toml b/toolkit/crashreporter/process_reader/Cargo.toml
index f84a00a29f..5fb20cf95a 100644
--- a/toolkit/crashreporter/process_reader/Cargo.toml
+++ b/toolkit/crashreporter/process_reader/Cargo.toml
@@ -10,14 +10,18 @@ license = "MPL-2.0"
[dependencies]
goblin = { version = "0.7", features = ["elf32", "elf64", "pe32", "pe64"] }
memoffset = "0.9"
-mozilla-central-workspace-hack = { version = "0.1", optional = true }
thiserror = "1.0"
+[target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies]
+libc = "0.2"
+
[target."cfg(target_os = \"windows\")".dependencies]
-[dependencies.windows-sys]
-version = "0.52"
-features = [
+windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_System_Diagnostics_Debug",
"Win32_System_ProcessStatus",
-]
+] }
+mozilla-central-workspace-hack = { version = "0.1", optional = true }
+
+[target."cfg(target_os = \"macos\")".dependencies]
+mach2 = { version = "0.4" }
diff --git a/toolkit/crashreporter/process_reader/src/error.rs b/toolkit/crashreporter/process_reader/src/error.rs
index af4d803a7a..2c681f78fb 100644
--- a/toolkit/crashreporter/process_reader/src/error.rs
+++ b/toolkit/crashreporter/process_reader/src/error.rs
@@ -8,6 +8,8 @@ use thiserror::Error;
pub enum ProcessReaderError {
#[error("Could not convert address {0}")]
ConvertAddressError(#[from] std::num::TryFromIntError),
+ #[error("Could not parse address {0}")]
+ ParseAddressError(#[from] std::num::ParseIntError),
#[cfg(target_os = "windows")]
#[error("Cannot enumerate the target process's modules")]
EnumProcessModulesError,
@@ -17,14 +19,52 @@ pub enum ProcessReaderError {
InvalidAddress,
#[error("Could not read from the target process address space")]
ReadFromProcessError(#[from] ReadError),
- #[cfg(target_os = "windows")]
+ #[cfg(any(target_os = "windows", target_os = "macos"))]
#[error("Section was not found")]
SectionNotFound,
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("Could not attach to the target process")]
+ AttachError(#[from] PtraceError),
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("Note not found")]
+ NoteNotFound,
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("waitpid() failed when attaching to the process")]
+ WaitPidError,
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("Could not parse a line in /proc/<pid>/maps")]
+ ProcMapsParseError,
+ #[error("Module not found")]
+ ModuleNotFound,
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("IO error for file {0}")]
+ IOError(#[from] std::io::Error),
+ #[cfg(target_os = "macos")]
+ #[error("Failure when requesting the task information")]
+ TaskInfoError,
+ #[cfg(target_os = "macos")]
+ #[error("The task dyld information format is unknown or invalid")]
+ ImageFormatError,
}
#[derive(Debug, Error)]
pub enum ReadError {
+ #[cfg(target_os = "macos")]
+ #[error("mach call failed")]
+ MachError,
+ #[cfg(any(target_os = "linux", target_os = "android"))]
+ #[error("ptrace-specific error")]
+ PtraceError(#[from] PtraceError),
#[cfg(target_os = "windows")]
#[error("ReadProcessMemory failed")]
ReadProcessMemoryError,
}
+
+#[cfg(any(target_os = "linux", target_os = "android"))]
+#[derive(Debug, Error)]
+pub enum PtraceError {
+ #[error("Could not read from the target process address space")]
+ ReadError(#[source] std::io::Error),
+ #[error("Could not trace the process")]
+ TraceError(#[source] std::io::Error),
+}
diff --git a/toolkit/crashreporter/process_reader/src/lib.rs b/toolkit/crashreporter/process_reader/src/lib.rs
index 1189ff144e..09936e46f1 100644
--- a/toolkit/crashreporter/process_reader/src/lib.rs
+++ b/toolkit/crashreporter/process_reader/src/lib.rs
@@ -2,12 +2,78 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+use std::{ffi::CString, mem::size_of};
+
+use error::ReadError;
+
+pub mod error;
+mod platform;
+
#[cfg(target_os = "windows")]
-type ProcessHandle = windows_sys::Win32::Foundation::HANDLE;
+pub type ProcessHandle = windows_sys::Win32::Foundation::HANDLE;
+
+#[cfg(any(target_os = "linux", target_os = "android"))]
+pub type ProcessHandle = libc::pid_t;
+
+#[cfg(any(target_os = "macos"))]
+pub type ProcessHandle = mach2::mach_types::task_t;
pub struct ProcessReader {
process: ProcessHandle,
}
-mod error;
-mod process_reader;
+impl ProcessReader {
+ pub fn copy_null_terminated_string(&self, address: usize) -> Result<CString, ReadError> {
+ // Try copying the string word-by-word first, this is considerably
+ // faster than one byte at a time.
+ if let Ok(string) = self.copy_null_terminated_string_word_by_word(address) {
+ return Ok(string);
+ }
+
+ // Reading the string one word at a time failed, let's try again one
+ // byte at a time. It's slow but it might work in situations where the
+ // string alignment causes word-by-word access to straddle page
+ // boundaries.
+ let mut length = 0;
+ let mut string = Vec::<u8>::new();
+
+ loop {
+ let char = self.copy_object::<u8>(address + length)?;
+ length += 1;
+ string.push(char);
+
+ if char == 0 {
+ break;
+ }
+ }
+
+ // SAFETY: If we reach this point we've read at least one byte and we
+ // know that the last one we read is nul.
+ Ok(unsafe { CString::from_vec_with_nul_unchecked(string) })
+ }
+
+ fn copy_null_terminated_string_word_by_word(
+ &self,
+ address: usize,
+ ) -> Result<CString, ReadError> {
+ const WORD_SIZE: usize = size_of::<usize>();
+ let mut length = 0;
+ let mut string = Vec::<u8>::new();
+
+ loop {
+ let array = self.copy_array::<u8>(address + length, WORD_SIZE)?;
+ let null_terminator = array.iter().position(|&e| e == 0);
+ length += null_terminator.unwrap_or(WORD_SIZE);
+ string.extend(array.into_iter());
+
+ if null_terminator.is_some() {
+ string.truncate(length + 1);
+ break;
+ }
+ }
+
+ // SAFETY: If we reach this point we've read at least one byte and we
+ // know that the last one we read is nul.
+ Ok(unsafe { CString::from_vec_with_nul_unchecked(string) })
+ }
+}
diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader.rs b/toolkit/crashreporter/process_reader/src/platform.rs
index b405b4b725..0ac354cd66 100644
--- a/toolkit/crashreporter/mozannotation_server/src/process_reader.rs
+++ b/toolkit/crashreporter/process_reader/src/platform.rs
@@ -2,12 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-use crate::ProcessHandle;
-
-pub struct ProcessReader {
- process: ProcessHandle,
-}
-
#[cfg(target_os = "windows")]
mod windows;
diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/linux.rs b/toolkit/crashreporter/process_reader/src/platform/linux.rs
index 34f8ef90d5..4a87e5480b 100644
--- a/toolkit/crashreporter/mozannotation_server/src/process_reader/linux.rs
+++ b/toolkit/crashreporter/process_reader/src/platform/linux.rs
@@ -2,7 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-use mozannotation_client::MozAnnotationNote;
use std::{
cmp::min,
fs::File,
@@ -13,12 +12,10 @@ use std::{
};
use crate::{
- errors::{FindAnnotationsAddressError, PtraceError, ReadError, RetrievalError},
- ProcessHandle,
+ error::{ProcessReaderError, PtraceError, ReadError},
+ ProcessHandle, ProcessReader,
};
-use super::ProcessReader;
-
use goblin::elf::{
self,
program_header::{PF_R, PT_NOTE},
@@ -28,11 +25,9 @@ use libc::{
c_int, c_long, c_void, pid_t, ptrace, waitpid, EINTR, PTRACE_ATTACH, PTRACE_DETACH,
PTRACE_PEEKDATA, __WALL,
};
-use memoffset::offset_of;
-use mozannotation_client::ANNOTATION_TYPE;
impl ProcessReader {
- pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> {
+ pub fn new(process: ProcessHandle) -> Result<ProcessReader, ProcessReaderError> {
let pid: pid_t = process;
ptrace_attach(pid)?;
@@ -46,7 +41,7 @@ impl ProcessReader {
EINTR => continue,
_ => {
ptrace_detach(pid)?;
- return Err(RetrievalError::WaitPidError);
+ return Err(ProcessReaderError::WaitPidError);
}
}
} else {
@@ -57,73 +52,78 @@ impl ProcessReader {
Ok(ProcessReader { process: pid })
}
- pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> {
+ pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> {
let maps_file = File::open(format!("/proc/{}/maps", self.process))?;
BufReader::new(maps_file)
.lines()
.flatten()
- .find_map(|line| self.find_annotations_in_module(&line).ok())
- .ok_or(FindAnnotationsAddressError::NotFound)
+ .map(|line| parse_proc_maps_line(&line))
+ .filter_map(Result::ok)
+ .find_map(|(name, address)| {
+ if name.is_some_and(|name| name.eq(module_name)) {
+ Some(address)
+ } else {
+ None
+ }
+ })
+ .ok_or(ProcessReaderError::ModuleNotFound)
}
- fn find_annotations_in_module(&self, line: &str) -> Result<usize, FindAnnotationsAddressError> {
- parse_proc_maps_line(line).and_then(|module_address| {
- let header_bytes = self.copy_array(module_address, size_of::<elf::Header>())?;
- let elf_header = Elf::parse_header(&header_bytes)?;
-
- let program_header_bytes = self.copy_array(
- module_address + (elf_header.e_phoff as usize),
- (elf_header.e_phnum as usize) * (elf_header.e_phentsize as usize),
- )?;
-
- let mut elf = Elf::lazy_parse(elf_header)?;
- let context = goblin::container::Ctx {
- container: elf.header.container()?,
- le: elf.header.endianness()?,
- };
-
- elf.program_headers = ProgramHeader::parse(
- &program_header_bytes,
- 0,
- elf_header.e_phnum as usize,
- context,
- )?;
-
- self.find_mozannotation_note(module_address, &elf)
- .ok_or(FindAnnotationsAddressError::ProgramHeaderNotFound)
- })
+ pub fn find_program_note(
+ &self,
+ module_address: usize,
+ note_type: u32,
+ note_size: usize,
+ ) -> Result<usize, ProcessReaderError> {
+ let header_bytes = self.copy_array(module_address, size_of::<elf::Header>())?;
+ let elf_header = Elf::parse_header(&header_bytes)?;
+
+ let program_header_bytes = self.copy_array(
+ module_address + (elf_header.e_phoff as usize),
+ (elf_header.e_phnum as usize) * (elf_header.e_phentsize as usize),
+ )?;
+
+ let mut elf = Elf::lazy_parse(elf_header)?;
+ let context = goblin::container::Ctx {
+ container: elf.header.container()?,
+ le: elf.header.endianness()?,
+ };
+
+ elf.program_headers = ProgramHeader::parse(
+ &program_header_bytes,
+ 0,
+ elf_header.e_phnum as usize,
+ context,
+ )?;
+
+ self.find_note_in_headers(&elf, module_address, note_type, note_size)
}
- // Looks through the program headers for the note contained in the
- // mozannotation_client crate. If the note is found return the address of the
- // note's desc field as well as its contents.
- fn find_mozannotation_note(&self, module_address: usize, elf: &Elf) -> Option<usize> {
+ fn find_note_in_headers(
+ &self,
+ elf: &Elf,
+ address: usize,
+ note_type: u32,
+ note_size: usize,
+ ) -> Result<usize, ProcessReaderError> {
for program_header in elf.program_headers.iter() {
// We're looking for a note in the program headers, it needs to be
// readable and it needs to be at least as large as the
- // MozAnnotationNote structure.
+ // requested size.
if (program_header.p_type == PT_NOTE)
&& ((program_header.p_flags & PF_R) != 0
- && (program_header.p_memsz as usize >= size_of::<MozAnnotationNote>()))
+ && (program_header.p_memsz as usize >= note_size))
{
// Iterate over the notes
- let notes_address = module_address + program_header.p_offset as usize;
+ let notes_address = address + program_header.p_offset as usize;
let mut notes_offset = 0;
let notes_size = program_header.p_memsz as usize;
while notes_offset < notes_size {
let note_address = notes_address + notes_offset;
if let Ok(note) = self.copy_object::<goblin::elf::note::Nhdr32>(note_address) {
- if note.n_type == ANNOTATION_TYPE {
- if let Ok(note) = self.copy_object::<MozAnnotationNote>(note_address) {
- let desc = note.desc;
- let ehdr = (-note.ehdr) as usize;
- let offset = desc + ehdr
- - (offset_of!(MozAnnotationNote, ehdr)
- - offset_of!(MozAnnotationNote, desc));
-
- return usize::checked_add(module_address, offset);
- }
+ if note.n_type == note_type {
+ return Ok(note_address);
}
notes_offset += size_of::<goblin::elf::note::Nhdr32>()
@@ -136,7 +136,7 @@ impl ProcessReader {
}
}
- None
+ Err(ProcessReaderError::NoteNotFound)
}
pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> {
@@ -189,38 +189,46 @@ impl Drop for ProcessReader {
}
}
-fn parse_proc_maps_line(line: &str) -> Result<usize, FindAnnotationsAddressError> {
+fn parse_proc_maps_line(line: &str) -> Result<(Option<String>, usize), ProcessReaderError> {
let mut splits = line.trim().splitn(6, ' ');
let address_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
let _perms_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
let _offset_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
let _dev_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
let _inode_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
- let _path_str = splits
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
+ let path_str = splits
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
let address = get_proc_maps_address(address_str)?;
- Ok(address)
+ // Note that we don't care if the mapped file has been deleted because
+ // we're reading everything from memory.
+ let name = path_str
+ .trim_end_matches(" (deleted)")
+ .rsplit('/')
+ .next()
+ .map(String::from);
+
+ Ok((name, address))
}
-fn get_proc_maps_address(addresses: &str) -> Result<usize, FindAnnotationsAddressError> {
+fn get_proc_maps_address(addresses: &str) -> Result<usize, ProcessReaderError> {
let begin = addresses
.split('-')
.next()
- .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?;
- usize::from_str_radix(begin, 16).map_err(FindAnnotationsAddressError::from)
+ .ok_or(ProcessReaderError::ProcMapsParseError)?;
+ usize::from_str_radix(begin, 16).map_err(ProcessReaderError::from)
}
fn uninit_as_bytes_mut<T>(elem: &mut MaybeUninit<T>) -> &mut [MaybeUninit<u8>] {
diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/macos.rs b/toolkit/crashreporter/process_reader/src/platform/macos.rs
index 52a3957ca9..a0dfa2b8fd 100644
--- a/toolkit/crashreporter/mozannotation_server/src/process_reader/macos.rs
+++ b/toolkit/crashreporter/process_reader/src/platform/macos.rs
@@ -15,12 +15,10 @@ use mach2::{
use std::mem::{size_of, MaybeUninit};
use crate::{
- errors::{FindAnnotationsAddressError, ReadError, RetrievalError},
- ProcessHandle,
+ error::{ProcessReaderError, ReadError},
+ ProcessHandle, ProcessReader,
};
-use super::ProcessReader;
-
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct AllImagesInfo {
@@ -55,23 +53,22 @@ struct ImageInfo {
}
const DATA_SEGMENT: &[u8; 16] = b"__DATA\0\0\0\0\0\0\0\0\0\0";
-const MOZANNOTATION_SECTION: &[u8; 16] = b"mozannotation\0\0\0";
impl ProcessReader {
- pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> {
+ pub fn new(process: ProcessHandle) -> Result<ProcessReader, ProcessReaderError> {
Ok(ProcessReader { process })
}
- pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> {
+ pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> {
let dyld_info = self.task_info()?;
if (dyld_info.all_image_info_format as u32) != TASK_DYLD_ALL_IMAGE_INFO_64 {
- return Err(FindAnnotationsAddressError::ImageFormatError);
+ return Err(ProcessReaderError::ImageFormatError);
}
let all_image_info_size = dyld_info.all_image_info_size;
let all_image_info_addr = dyld_info.all_image_info_addr;
if (all_image_info_size as usize) < size_of::<AllImagesInfo>() {
- return Err(FindAnnotationsAddressError::ImageFormatError);
+ return Err(ProcessReaderError::ImageFormatError);
}
let all_images_info = self.copy_object::<AllImagesInfo>(all_image_info_addr as _)?;
@@ -84,80 +81,94 @@ impl ProcessReader {
images
.iter()
- .find_map(|image| self.find_annotations_in_image(image))
- .ok_or(FindAnnotationsAddressError::NotFound)
- }
-
- fn task_info(&self) -> Result<task_dyld_info, FindAnnotationsAddressError> {
- let mut info = std::mem::MaybeUninit::<task_dyld_info>::uninit();
- let mut count = (std::mem::size_of::<task_dyld_info>() / std::mem::size_of::<u32>()) as u32;
-
- let res = unsafe {
- task_info(
- self.process,
- TASK_DYLD_INFO,
- info.as_mut_ptr().cast(),
- &mut count,
- )
- };
-
- if res == KERN_SUCCESS {
- // SAFETY: this will be initialized if the call succeeded
- unsafe { Ok(info.assume_init()) }
- } else {
- Err(FindAnnotationsAddressError::TaskInfoError)
- }
+ .find(|&image| {
+ let image_path = self.copy_null_terminated_string(image.file_path as usize);
+
+ if let Ok(image_path) = image_path {
+ if let Some(image_name) = image_path.into_bytes().rsplit(|&b| b == b'/').next()
+ {
+ image_name.eq(module_name.as_bytes())
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ })
+ .map(|image| image.load_address as usize)
+ .ok_or(ProcessReaderError::ModuleNotFound)
}
- fn find_annotations_in_image(&self, image: &ImageInfo) -> Option<usize> {
- self.copy_object::<Header64>(image.load_address as _)
- .map_err(FindAnnotationsAddressError::from)
- .and_then(|header| {
- let image_address = image.load_address as usize;
- let mut address = image_address + size_of::<Header64>();
-
- if header.magic == MH_MAGIC_64
- && (header.filetype == MH_EXECUTE || header.filetype == MH_DYLIB)
- {
- let end_of_commands = address + (header.sizeofcmds as usize);
-
- while address < end_of_commands {
- let command = self.copy_object::<LoadCommandHeader>(address)?;
-
- if command.cmd == LC_SEGMENT_64 {
- if let Ok(offset) = self.find_annotations_in_segment(address) {
- return image_address
- .checked_add(offset)
- .ok_or(FindAnnotationsAddressError::InvalidAddress);
- }
- }
-
- address += command.cmdsize as usize;
+ pub fn find_section(
+ &self,
+ module_address: usize,
+ section_name: &[u8; 16],
+ ) -> Result<usize, ProcessReaderError> {
+ let header = self.copy_object::<Header64>(module_address)?;
+ let mut address = module_address + size_of::<Header64>();
+
+ if header.magic == MH_MAGIC_64
+ && (header.filetype == MH_EXECUTE || header.filetype == MH_DYLIB)
+ {
+ let end_of_commands = address + (header.sizeofcmds as usize);
+
+ while address < end_of_commands {
+ let command = self.copy_object::<LoadCommandHeader>(address)?;
+
+ if command.cmd == LC_SEGMENT_64 {
+ if let Ok(offset) = self.find_section_in_segment(address, section_name) {
+ return module_address
+ .checked_add(offset)
+ .ok_or(ProcessReaderError::InvalidAddress);
}
}
- Err(FindAnnotationsAddressError::NotFound)
- })
- .ok()
+ address += command.cmdsize as usize;
+ }
+ }
+
+ Err(ProcessReaderError::SectionNotFound)
}
- fn find_annotations_in_segment(
+ fn find_section_in_segment(
&self,
segment_address: usize,
- ) -> Result<usize, FindAnnotationsAddressError> {
+ section_name: &[u8; 16],
+ ) -> Result<usize, ProcessReaderError> {
let segment = self.copy_object::<SegmentCommand64>(segment_address)?;
if segment.segname.eq(DATA_SEGMENT) {
let sections_addr = segment_address + size_of::<SegmentCommand64>();
let sections = self.copy_array::<Section64>(sections_addr, segment.nsects as usize)?;
for section in &sections {
- if section.sectname.eq(MOZANNOTATION_SECTION) {
+ if section.sectname.eq(section_name) {
return Ok(section.offset as usize);
}
}
}
- Err(FindAnnotationsAddressError::InvalidAddress)
+ Err(ProcessReaderError::SectionNotFound)
+ }
+
+ fn task_info(&self) -> Result<task_dyld_info, ProcessReaderError> {
+ let mut info = std::mem::MaybeUninit::<task_dyld_info>::uninit();
+ let mut count = (std::mem::size_of::<task_dyld_info>() / std::mem::size_of::<u32>()) as u32;
+
+ let res = unsafe {
+ task_info(
+ self.process,
+ TASK_DYLD_INFO,
+ info.as_mut_ptr().cast(),
+ &mut count,
+ )
+ };
+
+ if res == KERN_SUCCESS {
+ // SAFETY: this will be initialized if the call succeeded
+ unsafe { Ok(info.assume_init()) }
+ } else {
+ Err(ProcessReaderError::TaskInfoError)
+ }
}
pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> {
diff --git a/toolkit/crashreporter/process_reader/src/process_reader/windows.rs b/toolkit/crashreporter/process_reader/src/platform/windows.rs
index 1dd1a13049..2d8c63444e 100644
--- a/toolkit/crashreporter/process_reader/src/process_reader/windows.rs
+++ b/toolkit/crashreporter/process_reader/src/platform/windows.rs
@@ -15,7 +15,7 @@ use windows_sys::Win32::{
System::{
Diagnostics::Debug::ReadProcessMemory,
ProcessStatus::{
- GetModuleBaseNameW, K32EnumProcessModules, K32GetModuleInformation, MODULEINFO,
+ K32EnumProcessModules, K32GetModuleBaseNameW, K32GetModuleInformation, MODULEINFO,
},
},
};
@@ -30,32 +30,54 @@ impl ProcessReader {
Ok(ProcessReader { process })
}
+ pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> {
+ let modules = self.get_module_list()?;
+
+ let module = modules.iter().find_map(|&module| {
+ let name = self.get_module_name(module);
+ // Crude way of mimicking Windows lower-case comparisons but
+ // sufficient for our use-cases.
+ if name.is_some_and(|name| name.eq_ignore_ascii_case(module_name)) {
+ self.get_module_info(module)
+ .map(|module| module.lpBaseOfDll as usize)
+ } else {
+ None
+ }
+ });
+
+ module.ok_or(ProcessReaderError::ModuleNotFound)
+ }
+
pub fn find_section(
&self,
- module_name: &str,
- section_name: &str,
+ module_address: usize,
+ section_name: &[u8; 8],
) -> Result<usize, ProcessReaderError> {
- let modules = self.get_module_list()?;
+ // We read only the first page from the module, this should be more than
+ // enough to read the header and section list. In the future we might do
+ // this incrementally but for now goblin requires an array to parse
+ // so we can't do it just yet.
+ const PAGE_SIZE: usize = 4096;
+ let bytes = self.copy_array(module_address as _, PAGE_SIZE)?;
+ let header = goblin::pe::header::Header::parse(&bytes)?;
- modules
- .iter()
- .filter(|&&module| {
- let name = self.get_module_name(module);
- // Crude way of mimicking Windows lower-case comparisons but
- // sufficient for our use-cases.
- name.is_some_and(|name| name.eq_ignore_ascii_case(module_name))
- })
- .find_map(|&module| {
- self.get_module_info(module).and_then(|info| {
- self.find_section_in_module(
- section_name,
- info.lpBaseOfDll as usize,
- info.SizeOfImage as usize,
- )
- .ok()
- })
- })
- .ok_or(ProcessReaderError::InvalidAddress)
+ // Skip the PE header so we can parse the sections
+ let optional_header_offset = header.dos_header.pe_pointer as usize
+ + goblin::pe::header::SIZEOF_PE_MAGIC
+ + goblin::pe::header::SIZEOF_COFF_HEADER;
+ let offset =
+ &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize);
+
+ let sections = header.coff_header.sections(&bytes, offset)?;
+
+ for section in sections {
+ if section.name.eq(section_name) {
+ let address = module_address.checked_add(section.virtual_address as usize);
+ return address.ok_or(ProcessReaderError::InvalidAddress);
+ }
+ }
+
+ Err(ProcessReaderError::SectionNotFound)
}
fn get_module_list(&self) -> Result<Vec<HMODULE>, ProcessReaderError> {
@@ -97,8 +119,9 @@ impl ProcessReader {
fn get_module_name(&self, module: HMODULE) -> Option<String> {
let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
- let res =
- unsafe { GetModuleBaseNameW(self.process, module, (&mut path).as_mut_ptr(), MAX_PATH) };
+ let res = unsafe {
+ K32GetModuleBaseNameW(self.process, module, (&mut path).as_mut_ptr(), MAX_PATH)
+ };
if res == 0 {
None
@@ -128,46 +151,6 @@ impl ProcessReader {
}
}
- fn find_section_in_module(
- &self,
- section_name: &str,
- module_address: usize,
- size: usize,
- ) -> Result<usize, ProcessReaderError> {
- // We read only the first page from the module, this should be more than
- // enough to read the header and section list. In the future we might do
- // this incrementally but for now goblin requires an array to parse
- // so we can't do it just yet.
- let page_size = 4096;
- if size < page_size {
- // Don't try to read from the target module if it's too small
- return Err(ProcessReaderError::ReadFromProcessError(
- ReadError::ReadProcessMemoryError,
- ));
- }
-
- let bytes = self.copy_array(module_address as _, 4096)?;
- let header = goblin::pe::header::Header::parse(&bytes)?;
-
- // Skip the PE header so we can parse the sections
- let optional_header_offset = header.dos_header.pe_pointer as usize
- + goblin::pe::header::SIZEOF_PE_MAGIC
- + goblin::pe::header::SIZEOF_COFF_HEADER;
- let offset =
- &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize);
-
- let sections = header.coff_header.sections(&bytes, offset)?;
-
- for section in sections {
- if section.name.eq(section_name.as_bytes()) {
- let address = module_address.checked_add(section.virtual_address as usize);
- return address.ok_or(ProcessReaderError::InvalidAddress);
- }
- }
-
- Err(ProcessReaderError::SectionNotFound)
- }
-
pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> {
let mut object = MaybeUninit::<T>::uninit();
let res = unsafe {
diff --git a/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs b/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs
index fe5f30943e..21ae630c5e 100644
--- a/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs
+++ b/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs
@@ -3,7 +3,6 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
extern crate minidump_writer;
-use anyhow;
use libc::pid_t;
use minidump_writer::crash_context::CrashContext;
use minidump_writer::minidump_writer::MinidumpWriter;
diff --git a/toolkit/crashreporter/test/nsTestCrasher.cpp b/toolkit/crashreporter/test/nsTestCrasher.cpp
index 2148a60088..42b6a68b87 100644
--- a/toolkit/crashreporter/test/nsTestCrasher.cpp
+++ b/toolkit/crashreporter/test/nsTestCrasher.cpp
@@ -80,7 +80,7 @@ uint64_t x64CrashCFITest_EOF(uint64_t returnpfn, void*);
#endif // XP_WIN && HAVE_64BIT_BUILD && !defined(__MINGW32__)
}
-// Keep these in sync with CrashTestUtils.jsm!
+// Keep these in sync with CrashTestUtils.sys.mjs!
const int16_t CRASH_INVALID_POINTER_DEREF = 0;
const int16_t CRASH_PURE_VIRTUAL_CALL = 1;
const int16_t CRASH_OOM = 3;
diff --git a/toolkit/crashreporter/test/unit/head_win64cfi.js b/toolkit/crashreporter/test/unit/head_win64cfi.js
index 4df99213db..2daa64aecc 100644
--- a/toolkit/crashreporter/test/unit/head_win64cfi.js
+++ b/toolkit/crashreporter/test/unit/head_win64cfi.js
@@ -184,7 +184,7 @@ function assertStack(stack, expected) {
// Performs a crash, runs minidump-analyzer, and checks expected stack analysis.
//
-// how: The crash to perform. Constants defined in both CrashTestUtils.jsm
+// how: The crash to perform. Constants defined in both CrashTestUtils.sys.mjs
// and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL)
// expectedStack: An array of {"symbol", "trust"} where trust is "cfi",
// "context", "scan", et al. May be null if you don't need to check the stack.
diff --git a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js
index a47c217238..7c1d3821ec 100644
--- a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js
+++ b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js
@@ -12,7 +12,7 @@ add_task(async function run_test() {
crashType = CrashTestUtils.CRASH_STACK_OVERFLOW;
crashReporter.annotateCrashReport("TestKey", "TestValue");
},
- async function (mdump, extra, extraFile) {
+ async function (mdump, extra) {
Assert.equal(extra.TestKey, "TestValue");
},
// process will exit with a zero exit status
diff --git a/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js
index f42fe10f0c..dcf5bd3153 100644
--- a/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js
+++ b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js
@@ -4,7 +4,7 @@ add_task(async function run_test() {
let appAddr = CrashTestUtils.saveAppMemory();
crashReporter.registerAppMemory(appAddr, 32);
},
- function (mdump, extra) {
+ function (mdump) {
Assert.ok(mdump.exists());
Assert.ok(mdump.fileSize > 0);
Assert.ok(CrashTestUtils.dumpCheckMemory(mdump.path));
diff --git a/toolkit/crashreporter/test/unit/test_event_files.js b/toolkit/crashreporter/test/unit/test_event_files.js
index 844d7700ff..f4054089ea 100644
--- a/toolkit/crashreporter/test/unit/test_event_files.js
+++ b/toolkit/crashreporter/test/unit/test_event_files.js
@@ -24,7 +24,7 @@ add_task(async function test_main_process_crash() {
crashType = CrashTestUtils.CRASH_MOZ_CRASH;
crashReporter.annotateCrashReport("ShutdownProgress", "event-test");
},
- (minidump, extra) => {
+ minidump => {
basename = minidump.leafName;
Object.defineProperty(cm, "_eventsDirs", { value: [getEventDir()] });
cm.aggregateEventsFiles().then(resolve, reject);
diff --git a/toolkit/crashreporter/test/unit/test_override_exception_handler.js b/toolkit/crashreporter/test/unit/test_override_exception_handler.js
index 1b4dec8a61..caabc12de8 100644
--- a/toolkit/crashreporter/test/unit/test_override_exception_handler.js
+++ b/toolkit/crashreporter/test/unit/test_override_exception_handler.js
@@ -5,7 +5,7 @@ add_task(async function run_test() {
function () {
CrashTestUtils.TryOverrideExceptionHandler();
},
- function (mdump, extra) {},
+ function () {},
true
);
});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js
index 733d224160..b3770c7bcc 100644
--- a/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js
@@ -18,7 +18,7 @@ add_task(async function run_test() {
is_win7_or_newer = true;
}
- await do_content_crash(null, function (mdump, extra) {
+ await do_content_crash(null, function (mdump) {
Assert.ok(mdump.exists());
Assert.ok(mdump.fileSize > 0);
if (is_win7_or_newer) {
diff --git a/toolkit/crashreporter/tools/symbolstore.py b/toolkit/crashreporter/tools/symbolstore.py
index 8bc7a7120a..b7ad7752a8 100755
--- a/toolkit/crashreporter/tools/symbolstore.py
+++ b/toolkit/crashreporter/tools/symbolstore.py
@@ -23,6 +23,7 @@
import ctypes
import errno
+import io
import os
import platform
import re
@@ -390,13 +391,13 @@ def validate_install_manifests(install_manifest_args):
def make_file_mapping(install_manifests):
file_mapping = {}
for manifest, destination in install_manifests:
- destination = os.path.abspath(destination)
+ absolute_destination = os.path.abspath(destination)
reg = FileRegistry()
manifest.populate_registry(reg)
for dst, src in reg:
if hasattr(src, "path"):
# Any paths that get compared to source file names need to go through realpath.
- abs_dest = realpath(os.path.join(destination, dst))
+ abs_dest = realpath(os.path.join(absolute_destination, dst))
file_mapping[abs_dest] = realpath(src.path)
return file_mapping
@@ -551,7 +552,58 @@ class Dumper:
Get the commandline used to invoke dump_syms.
"""
# The Mac dumper overrides this.
- return [self.dump_syms, "--inlines", file]
+ cmdline = [
+ self.dump_syms,
+ "--inlines",
+ ]
+
+ cmdline.extend(self.dump_syms_extra_info())
+ cmdline.append(file)
+
+ return cmdline
+
+ def dump_syms_extra_info(self):
+ """
+ Returns an array with the additional parameters to add information
+ about the build to the dump_syms command-line
+ """
+ cmdline = [
+ "--extra-info",
+ "RELEASECHANNEL " + buildconfig.substs["MOZ_UPDATE_CHANNEL"],
+ "--extra-info",
+ "VERSION " + buildconfig.substs["MOZ_APP_VERSION"],
+ ]
+
+ if buildconfig.substs.get("MOZ_APP_VENDOR") is not None:
+ cmdline.extend(
+ [
+ "--extra-info",
+ "VENDOR " + buildconfig.substs["MOZ_APP_VENDOR"],
+ ]
+ )
+
+ if buildconfig.substs.get("MOZ_APP_BASENAME") is not None:
+ cmdline.extend(
+ [
+ "--extra-info",
+ "PRODUCTNAME " + buildconfig.substs["MOZ_APP_BASENAME"],
+ ]
+ )
+
+ # Add the build ID if it's present
+ path = os.path.join(buildconfig.topobjdir, "buildid.h")
+ try:
+ buildid = io.open(path, "r", encoding="utf-8").read().split()[2]
+ cmdline.extend(
+ [
+ "--extra-info",
+ "BUILDID " + buildid,
+ ]
+ )
+ except Exception:
+ pass
+
+ return cmdline
def ProcessFileWork(
self, file, arch_num, arch, vcs_root, dsymbundle=None, count_ctors=False
@@ -859,9 +911,8 @@ class Dumper_Linux(Dumper):
full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path))
shutil.move(file_dbg, full_path)
print(rel_path)
- else:
- if os.path.isfile(file_dbg):
- os.unlink(file_dbg)
+ elif os.path.isfile(file_dbg):
+ os.unlink(file_dbg)
class Dumper_Solaris(Dumper):
@@ -902,11 +953,22 @@ class Dumper_Mac(Dumper):
# in order to dump all the symbols.
if dsymbundle:
# This is the .dSYM bundle.
- return (
- [self.dump_syms]
- + arch.split()
- + ["--inlines", "-j", "2", dsymbundle, file]
+ cmdline = [self.dump_syms]
+
+ cmdline.extend(arch.split())
+ cmdline.extend(
+ [
+ "--inlines",
+ "-j",
+ "2",
+ ]
)
+
+ cmdline.extend(self.dump_syms_extra_info())
+ cmdline.extend([dsymbundle, file])
+
+ return cmdline
+
return Dumper.dump_syms_cmdline(self, file, arch)
def GenerateDSYM(self, file):
@@ -1066,13 +1128,13 @@ to canonical locations in the source repository. Specify
if len(args) < 3:
parser.error("not enough arguments")
- exit(1)
+ sys.exit(1)
try:
manifests = validate_install_manifests(options.install_manifests)
except (IOError, ValueError) as e:
parser.error(str(e))
- exit(1)
+ sys.exit(1)
file_mapping = make_file_mapping(manifests)
_, bucket = get_s3_region_and_bucket()
dumper = GetPlatformSpecificDumper(