diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/crashreporter/client | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/crashreporter/client')
28 files changed, 5172 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/Makefile.in b/toolkit/crashreporter/client/Makefile.in new file mode 100644 index 0000000000..a3110090b2 --- /dev/null +++ b/toolkit/crashreporter/client/Makefile.in @@ -0,0 +1,19 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +ifeq ($(OS_ARCH),WINNT) +MOZ_WINCONSOLE = 0 +endif + +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,-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) + $(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/Throbber-small.avi b/toolkit/crashreporter/client/Throbber-small.avi Binary files differnew file mode 100644 index 0000000000..640ea62c0e --- /dev/null +++ b/toolkit/crashreporter/client/Throbber-small.avi diff --git a/toolkit/crashreporter/client/Throbber-small.gif b/toolkit/crashreporter/client/Throbber-small.gif Binary files differnew file mode 100644 index 0000000000..cce32f20f4 --- /dev/null +++ b/toolkit/crashreporter/client/Throbber-small.gif diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp new file mode 100644 index 0000000000..4426e6446c --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.cpp @@ -0,0 +1,833 @@ +/* -*- 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) { + string currentSection; + 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 << std::endl; + } +} + +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.append("../../../"); +#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; + + ostringstream paramName; + int i = 0; + 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 new file mode 100644 index 0000000000..81aa1465c6 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.exe.manifest @@ -0,0 +1,42 @@ +<?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 new file mode 100644 index 0000000000..d0ac7a8626 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.h @@ -0,0 +1,159 @@ +/* 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 <iostream> +#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 Binary files differnew file mode 100644 index 0000000000..29ac3c6189 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.ico diff --git a/toolkit/crashreporter/client/crashreporter.rc b/toolkit/crashreporter/client/crashreporter.rc new file mode 100755 index 0000000000..f6042bf2e5 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.rc @@ -0,0 +1,143 @@ +/* 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 new file mode 100644 index 0000000000..d4bed0209c --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp @@ -0,0 +1,361 @@ +/* -*- 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 <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; + } else 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 new file mode 100644 index 0000000000..208c7ba6b0 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_gtk_common.h @@ -0,0 +1,50 @@ +/* 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 new file mode 100644 index 0000000000..d11f10a472 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_linux.cpp @@ -0,0 +1,525 @@ +/* -*- 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 << "]" << std::endl; + for (const auto& iter : strings) { + out << iter.first << "="; + if (escape) { + out << Escape(iter.second); + } else { + out << iter.second; + } + + out << std::endl; + } + + 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, + >k_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 new file mode 100644 index 0000000000..19282a3fe6 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_osx.h @@ -0,0 +1,107 @@ +/* -*- 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 new file mode 100644 index 0000000000..38b3349a04 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_osx.mm @@ -0,0 +1,762 @@ +/* -*- 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 new file mode 100644 index 0000000000..0cecc6262a --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp @@ -0,0 +1,139 @@ +/* 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.c_str()); + + // s/.dmp/.extra/ + path.replace(path.size() - 4, 4, ".extra"); + UIDeleteFile(path.c_str()); + + 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 new file mode 100644 index 0000000000..e680aa37c9 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_win.cpp @@ -0,0 +1,1295 @@ +/* -*- 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 <versionhelpers.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 SENDURL_ORIGINAL L"https://crash-reports.mozilla.com/submit" +#define SENDURL_XPSP2 L"https://crash-reports-xpsp2.mozilla.com/submit" + +#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); +} + +static bool CanUseMainCrashReportServer() { + // Any NT from 6.0 and above is fine. + if (IsWindowsVersionOrGreater(6, 0, 0)) { + return true; + } + + // On NT 5 servers, we need Server 2003 SP2. + if (IsWindowsServer()) { + return IsWindowsVersionOrGreater(5, 2, 2); + } + + // Otherwise we have an NT 5 client. + // We need exactly XP SP3 (version 5.1 SP3 but not version 5.2). + return (IsWindowsVersionOrGreater(5, 1, 3) && + !IsWindowsVersionOrGreater(5, 2, 0)); +} + +bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters, + const string& sendURL, const vector<string>& restartArgs) { + gSendData.hDlg = nullptr; + gSendData.sendURL = UTF8ToWide(sendURL); + + // Older Windows don't support the crash report server's crypto. + // This is a hack to use an alternate server. + if (!CanUseMainCrashReportServer() && + gSendData.sendURL.find(SENDURL_ORIGINAL) == 0) { + gSendData.sendURL.replace(0, ARRAYSIZE(SENDURL_ORIGINAL) - 1, + SENDURL_XPSP2); + } + + 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/macbuild/Contents/Info.plist b/toolkit/crashreporter/client/macbuild/Contents/Info.plist new file mode 100644 index 0000000000..51d6c4de37 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Info.plist @@ -0,0 +1,36 @@ +<?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>crashreporter</string> + <key>CFBundleExecutable</key> + <string>crashreporter</string> + <key>CFBundleIconFile</key> + <string>crashreporter.icns</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.crashreporter</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>crashreporter</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>LSHasLocalizedDisplayName</key> + <true/> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSRequiresAquaSystemAppearance</key> + <false/> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>LSUIElement</key> + <true/> +</dict> +</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo b/toolkit/crashreporter/client/macbuild/Contents/PkgInfo new file mode 100644 index 0000000000..cae6d0a58f --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/PkgInfo @@ -0,0 +1,2 @@ +APPL???? + diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in new file mode 100644 index 0000000000..e08ce59eb6 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in @@ -0,0 +1,8 @@ +/* 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/. */ + +/* Localized versions of Info.plist keys */ + +CFBundleName = "Crash Reporter"; +CFBundleDisplayName = "@APP_NAME@ Crash Reporter"; 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 new file mode 100644 index 0000000000..254131e431 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,100 @@ +<?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 new file mode 100644 index 0000000000..517349ffce --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,18 @@ +<?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 Binary files differnew file mode 100644 index 0000000000..bfdcccb74c --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib 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 new file mode 100644 index 0000000000..254131e431 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib @@ -0,0 +1,100 @@ +<?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 new file mode 100644 index 0000000000..4a2251aaf5 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib @@ -0,0 +1,18 @@ +<?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 Binary files differnew file mode 100644 index 0000000000..6c93849b94 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns b/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns Binary files differnew file mode 100644 index 0000000000..341cd05a4d --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build new file mode 100644 index 0000000000..f678ca1cd6 --- /dev/null +++ b/toolkit/crashreporter/client/moz.build @@ -0,0 +1,96 @@ +# -*- 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["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 new file mode 100644 index 0000000000..72dc163de2 --- /dev/null +++ b/toolkit/crashreporter/client/ping.cpp @@ -0,0 +1,324 @@ +/* -*- 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 (IsAnnotationAllowlistedForPing(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 new file mode 100644 index 0000000000..2e7917daa4 --- /dev/null +++ b/toolkit/crashreporter/client/resource.h @@ -0,0 +1,35 @@ +/* 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 |