diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/crashreporter/client | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/crashreporter/client')
26 files changed, 5281 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/Makefile.in b/toolkit/crashreporter/client/Makefile.in new file mode 100644 index 000000000..88be52dff --- /dev/null +++ b/toolkit/crashreporter/client/Makefile.in @@ -0,0 +1,20 @@ +# 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 + sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \ + iconv -f UTF-8 -t UTF-16 > $(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 000000000..640ea62c0 --- /dev/null +++ b/toolkit/crashreporter/client/Throbber-small.avi diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp new file mode 100644 index 000000000..10aa65dd2 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.cpp @@ -0,0 +1,759 @@ +/* -*- 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 <sstream> +#include <memory> +#include <time.h> +#include <stdlib.h> +#include <string.h> + +using std::string; +using std::istream; +using std::ifstream; +using std::istringstream; +using std::ostringstream; +using std::ostream; +using std::ofstream; +using std::vector; +using std::auto_ptr; + +namespace CrashReporter { + +StringTable gStrings; +string gSettingsPath; +string gEventsPath; +int gArgc; +char** gArgv; + +enum SubmissionResult {Succeeded, Failed}; + +static auto_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) +{ + string errorMessage; + if (!gStrings[ST_CRASHREPORTERERROR].empty()) { + char buf[2048]; + UI_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; +} + +static string Escape(const string& str) +{ + string ret; + for (string::const_iterator iter = str.begin(); + iter != str.end(); + iter++) { + if (*iter == '\\') { + ret += "\\\\"; + } else if (*iter == '\n') { + ret += "\\n"; + } else if (*iter == '\t') { + ret += "\\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); + bool success = false; + if (f->is_open()) { + success = ReadStrings(*f, strings, unescape); + f->close(); + } + + delete f; + return success; +} + +bool WriteStrings(ostream& out, + const string& header, + StringTable& strings, + bool escape) +{ + out << "[" << header << "]" << std::endl; + for (StringTable::iterator iter = strings.begin(); + iter != strings.end(); + iter++) { + out << iter->first << "="; + if (escape) + out << Escape(iter->second); + else + out << iter->second; + + out << std::endl; + } + + return true; +} + +bool WriteStringsToFile(const string& path, + const string& header, + StringTable& strings, + bool escape) +{ + ofstream* f = UIOpenWrite(path.c_str()); + bool success = false; + if (f->is_open()) { + success = WriteStrings(*f, header, strings, escape); + f->close(); + } + + 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); + else + return file; +} + +static string GetDumpLocalID() +{ + string localId = Basename(gReporterDumpFile); + string::size_type dot = localId.rfind('.'); + + if (dot == string::npos) + return ""; + + return localId.substr(0, dot); +} + +// This appends the StackTraces entry generated by the minidump analyzer to the +// main crash event so that it can be picked up by Firefox once it restarts +static void AppendStackTracesToEventFile(const string& aStackTraces) +{ + if (gEventsPath.empty()) { + // If there is no path for finding the crash event, skip this step. + return; + } + + string localId = GetDumpLocalID(); + string path = gEventsPath + UI_DIR_SEPARATOR + localId; + ofstream* f = UIOpenWrite(path.c_str(), true); + + if (f->is_open()) { + *f << "StackTraces=" << aStackTraces; + f->close(); + } + + delete f; +} + +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 = GetDumpLocalID(); + string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission"; + ofstream* f = UIOpenWrite(fpath.c_str(), false, true); + 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.c_str(), true)); +} + +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); + 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); + if (!file->is_open()) { + delete file; + return false; + } + + char buf[1024]; + UI_SNPRINTF(buf, 1024, + gStrings["CrashID"].c_str(), + responseItems["CrashID"].c_str()); + *file << buf << "\n"; + + if (responseItems.find("ViewURL") != responseItems.end()) { + UI_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, ""); + } +} + +bool ShouldEnableSending() +{ + srand(time(0)); + return ((rand() % 100) < MOZ_CRASHREPORTER_ENABLE_PERCENT); +} + +} // namespace CrashReporter + +using namespace CrashReporter; + +void RewriteStrings(StringTable& queryParameters) +{ + // rewrite some UI strings with the values from the query parameters + string product = queryParameters["ProductName"]; + string vendor = queryParameters["Vendor"]; + if (vendor.empty()) { + // Assume Mozilla if no vendor is specified + vendor = "Mozilla"; + } + + char buf[4096]; + UI_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 + UI_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; + } + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(), + product.c_str()); + gStrings[ST_CRASHREPORTERDESCRIPTION] = buf; + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_CHECKSUBMIT].c_str(), + vendor.c_str()); + gStrings[ST_CHECKSUBMIT] = buf; + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_CHECKEMAIL].c_str(), + vendor.c_str()); + gStrings[ST_CHECKEMAIL] = buf; + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_RESTART].c_str(), + product.c_str()); + gStrings[ST_RESTART] = buf; + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_QUIT].c_str(), + product.c_str()); + gStrings[ST_QUIT] = buf; + + UI_SNPRINTF(buf, sizeof(buf), + gStrings[ST_ERROR_ENDOFLIFE].c_str(), + product.c_str()); + gStrings[ST_ERROR_ENDOFLIFE] = buf; +} + +bool CheckEndOfLifed(string version) +{ + string reportPath = + gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + version; + return UIFileExists(reportPath); +} + +#ifndef RELEASE_OR_BETA + +static string +GetMinidumpAnalyzerPath() +{ + string path = gArgv[0]; + size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX); + path.erase(pos); + path.append(UI_MINIDUMP_ANALYZER_FILENAME BIN_SUFFIX); + + return path; +} + +#endif + +int main(int argc, char** argv) +{ + gArgc = argc; + gArgv = argv; + + 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 + UIShowDefaultUI(); + } else { +#ifndef RELEASE_OR_BETA + // start by running minidump analyzer, this is currently enabled only in + // nightly and aurora + UIRunMinidumpAnalyzer(GetMinidumpAnalyzerPath(), gReporterDumpFile); +#endif + + // 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(); + } + + StringTable queryParameters; + if (!ReadStringsFromFile(gExtraFile, queryParameters, true)) { + UIError(gStrings[ST_ERROR_EXTRAFILEREAD]); + return 0; + } + + if (queryParameters.find("ProductName") == queryParameters.end()) { + UIError(gStrings[ST_ERROR_NOPRODUCTNAME]); + return 0; + } + + // There is enough information in the extra file to rewrite strings + // to be product specific + RewriteStrings(queryParameters); + + if (queryParameters.find("ServerURL") == queryParameters.end()) { + 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. +#ifdef XP_WIN32 + static const wchar_t kDataDirKey[] = L"MOZ_CRASHREPORTER_DATA_DIRECTORY"; + const wchar_t *settingsPath = _wgetenv(kDataDirKey); + if (settingsPath && *settingsPath) { + gSettingsPath = WideToUTF8(settingsPath); + } +#else + static const char kDataDirKey[] = "MOZ_CRASHREPORTER_DATA_DIRECTORY"; + const char *settingsPath = getenv(kDataDirKey); + if (settingsPath && *settingsPath) { + gSettingsPath = settingsPath; + } +#endif + else { + string product = queryParameters["ProductName"]; + string vendor = queryParameters["Vendor"]; + if (!UIGetSettingsPath(vendor, product, gSettingsPath)) { + gSettingsPath.clear(); + } + } + + if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) { + UIError(gStrings[ST_ERROR_NOSETTINGSPATH]); + return 0; + } + + OpenLogFile(); + +#ifdef XP_WIN32 + static const wchar_t kEventsDirKey[] = L"MOZ_CRASHREPORTER_EVENTS_DIRECTORY"; + const wchar_t *eventsPath = _wgetenv(kEventsDirKey); + if (eventsPath && *eventsPath) { + gEventsPath = WideToUTF8(eventsPath); + } +#else + static const char kEventsDirKey[] = "MOZ_CRASHREPORTER_EVENTS_DIRECTORY"; + const char *eventsPath = getenv(kEventsDirKey); + if (eventsPath && *eventsPath) { + gEventsPath = eventsPath; + } +#endif + else { + gEventsPath.clear(); + } + + // Update the crash event with stacks if they are present + auto stackTracesItr = queryParameters.find("StackTraces"); + if (stackTracesItr != queryParameters.end()) { + AppendStackTracesToEventFile(stackTracesItr->second); + } + + 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 = queryParameters["ServerURL"]; + // we don't need to actually send this + queryParameters.erase("ServerURL"); + + queryParameters["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 (queryParameters.find("Version") != queryParameters.end() && + CheckEndOfLifed(queryParameters["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, queryParameters, 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 000000000..f8587bfad --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.exe.manifest @@ -0,0 +1,38 @@ +<?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> + <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}"/> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + </application> + </compatibility> +</assembly> diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h new file mode 100644 index 000000000..8c5ca3613 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.h @@ -0,0 +1,158 @@ +/* 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 500 + +#if defined(XP_WIN32) + +#include <windows.h> + +#define UI_SNPRINTF _snprintf +#define UI_DIR_SEPARATOR "\\" + +std::string WideToUTF8(const std::wstring& wide, bool* success = 0); + +#else + +#define UI_SNPRINTF snprintf +#define UI_DIR_SEPARATOR "/" + +#endif + +#define UI_CRASH_REPORTER_FILENAME "crashreporter" +#define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer" + +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_CHECKEMAIL "CheckAllowEmail" +#define ST_EMAILGRAYTEXT "EmailGrayText" +#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 +//============================================================================= + +namespace CrashReporter { + extern StringTable gStrings; + extern std::string gSettingsPath; + extern std::string gEventsPath; + extern int gArgc; + extern char** gArgv; + + 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); + bool WriteStrings(std::ostream& out, + const std::string& header, + StringTable& strings, + bool escape); + bool WriteStringsToFile(const std::string& path, + const std::string& header, + StringTable& strings, + bool escape); + void LogMessage(const std::string& message); + void DeleteDump(); + bool ShouldEnableSending(); + + static const unsigned int kSaveCount = 10; +} + +//============================================================================= +// 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 StringTable& 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::ofstream* UIOpenWrite(const std::string& filename, + bool append=false, + bool binary=false); +void UIPruneSavedDumps(const std::string& directory); +void UIRunMinidumpAnalyzer(const std::string& exename, + const std::string& filename); + +#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 000000000..29ac3c618 --- /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 000000000..0ccd0757f --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter.rc @@ -0,0 +1,148 @@ +/* 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 + +1 24 "crashreporter.exe.manifest" + +///////////////////////////////////////////////////////////////////////////// +// +// 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 "tell mozilla to email me with more information",IDC_EMAILMECHECK, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,24,120,210,10 + EDITTEXT IDC_EMAILTEXT,36,133,198,14,ES_AUTOHSCROLL + 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 000000000..395a339bf --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp @@ -0,0 +1,453 @@ +/* -*- 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; +StringTable gQueryParameters; +string gHttpProxy; +string gAuth; +string gCACertificateFile; +string gSendURL; +string gURLParameter; +vector<string> gRestartArgs; +GThread* gSendThreadID; + +// From crashreporter_linux.cpp +void SaveSettings(); +void SendReport(); +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) + 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 +static gboolean CloseApp(gpointer data) +{ + 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; +} + +#ifdef MOZ_ENABLE_GCONF +#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(). +} +#endif + +gpointer SendThread(gpointer args) +{ + string response, error; + long response_code; + + bool success = google_breakpad::HTTPUpload::SendRequest + (gSendURL, + gQueryParameters, + 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); + // 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; + SendReport(); + } 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.erase("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 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 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); +} + +bool UIDeleteFile(const string& file) +{ + return (unlink(file.c_str()) != -1); +} + +std::ifstream* UIOpenRead(const string& filename) +{ + return new std::ifstream(filename.c_str(), std::ios::in); +} + +std::ofstream* UIOpenWrite(const string& filename, + bool append, // append=false + bool binary) // binary=false +{ + std::ios_base::openmode mode = std::ios::out; + + if (append) { + mode = mode | std::ios::app; + } + + if (binary) { + mode = mode | std::ios::binary; + } + + return new std::ofstream(filename.c_str(), mode); +} diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.h b/toolkit/crashreporter/client/crashreporter_gtk_common.h new file mode 100644 index 000000000..3a6350c5b --- /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> + +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 StringTable gQueryParameters; +extern std::string gHttpProxy; +extern std::string gAuth; +extern std::string gCACertificateFile; +extern std::string gSendURL; +extern std::string gURLParameter; + +void LoadProxyinfo(); +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); + +#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 000000000..6e7ccce40 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_linux.cpp @@ -0,0 +1,576 @@ +/* -*- 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::string; +using std::vector; + +using namespace CrashReporter; + +static GtkWidget* gViewReportButton = 0; +static GtkWidget* gCommentTextLabel = 0; +static GtkWidget* gCommentText = 0; +static GtkWidget* gEmailMeCheck = 0; +static GtkWidget* gEmailEntryLabel = 0; +static GtkWidget* gEmailEntry = 0; + +static bool gEmailFieldHint = true; +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. + */ + + StringTable settings; + if (ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true)) { + if (settings.find("Email") != settings.end()) { + gtk_entry_set_text(GTK_ENTRY(gEmailEntry), settings["Email"].c_str()); + gEmailFieldHint = false; + } + if (settings.find("EmailMe") != settings.end()) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gEmailMeCheck), + settings["EmailMe"][0] != '0'); + } + if (settings.find("IncludeURL") != settings.end() && + gIncludeURLCheck != 0) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck), + settings["IncludeURL"][0] != '0'); + } + bool enabled; + if (settings.find("SubmitReport") != settings.end()) + enabled = settings["SubmitReport"][0] != '0'; + else + enabled = ShouldEnableSending(); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck), + enabled); + } +} + +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 (!gEmailFieldHint) + settings["Email"] = gtk_entry_get_text(GTK_ENTRY(gEmailEntry)); + else + settings.erase("Email"); + + settings["EmailMe"] = + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gEmailMeCheck)) ? "1" : "0"; + 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() +{ + // 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(gEmailMeCheck, FALSE); + gtk_widget_set_sensitive(gEmailEntry, 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()); + +#ifdef MOZ_ENABLE_GCONF + LoadProxyinfo(); +#endif + + // and spawn a thread to do the sending + GError* err; + gSendThreadID = g_thread_create(SendThread, nullptr, TRUE, &err); +} + +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 (StringTable::iterator iter = gQueryParameters.begin(); + iter != gQueryParameters.end(); + iter++) { + gtk_text_buffer_insert(buffer, &end, iter->first.c_str(), -1); + gtk_text_buffer_insert(buffer, &end, ": ", -1); + gtk_text_buffer_insert(buffer, &end, iter->second.c_str(), -1); + 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_widget_set_sensitive(gEmailMeCheck, TRUE); + gtk_widget_set_sensitive(gEmailEntry, + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gEmailMeCheck))); + 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_widget_set_sensitive(gEmailMeCheck, FALSE); + gtk_widget_set_sensitive(gEmailEntry, 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); +#if (MOZ_WIDGET_GTK >= 3) + gtk_widget_set_vexpand(scrolled, TRUE); +#endif + + 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.erase("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; +} + +static void UpdateEmail() +{ + const char* email = gtk_entry_get_text(GTK_ENTRY(gEmailEntry)); + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gEmailMeCheck))) { + gtk_widget_set_sensitive(gEmailEntry, TRUE); + } else { + email = ""; + gtk_widget_set_sensitive(gEmailEntry, FALSE); + } + if (email[0] == '\0' || gEmailFieldHint) + gQueryParameters.erase("Email"); + else + gQueryParameters["Email"] = email; +} + +static void EmailMeClicked(GtkButton* sender, gpointer userData) +{ + UpdateEmail(); +} + +static void EmailChanged(GtkEditable* editable, gpointer userData) +{ + UpdateEmail(); +} + +static gboolean EmailFocusChange(GtkWidget* widget, GdkEventFocus* event, + gpointer userData) +{ + UpdateHintText(widget, event->in, &gEmailFieldHint, + gStrings[ST_EMAILGRAYTEXT].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 StringTable& queryParameters, + const string& sendURL, + const vector<string>& restartArgs) +{ + gFiles = files; + gQueryParameters = queryParameters; + gSendURL = sendURL; + gRestartArgs = restartArgs; + if (gQueryParameters.find("URL") != gQueryParameters.end()) + gURLParameter = gQueryParameters["URL"]; + + 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 +#if (MOZ_WIDGET_GTK == 2) + gtk_widget_set_size_request(descriptionLabel, 400, -1); +#else + gtk_label_set_max_width_chars(GTK_LABEL(descriptionLabel), LABEL_MAX_CHAR_WIDTH); +#endif + 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); +#if (MOZ_WIDGET_GTK >= 3) + gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolled), 100); +#endif + + 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); + gtk_widget_set_size_request(GTK_WIDGET(gCommentText), -1, 100); + + if (gQueryParameters.find("URL") != gQueryParameters.end()) { + 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); + } + + gEmailMeCheck = + gtk_check_button_new_with_label(gStrings[ST_CHECKEMAIL].c_str()); + gtk_box_pack_start(GTK_BOX(innerVBox), gEmailMeCheck, FALSE, FALSE, 0); + g_signal_connect(gEmailMeCheck, "clicked", G_CALLBACK(EmailMeClicked), 0); + + GtkWidget* emailIndentBox = gtk_hbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(innerVBox), emailIndentBox, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(emailIndentBox), gtk_label_new(""), + FALSE, FALSE, 9); + + gEmailEntryLabel = gtk_label_new(gStrings[ST_EMAILGRAYTEXT].c_str()); + gEmailEntry = gtk_entry_new(); + gtk_label_set_mnemonic_widget(GTK_LABEL(gEmailEntryLabel), gEmailEntry); + gtk_box_pack_start(GTK_BOX(emailIndentBox), gEmailEntry, TRUE, TRUE, 0); + g_signal_connect(gEmailEntry, "changed", G_CALLBACK(EmailChanged), 0); + g_signal_connect(gEmailEntry, "focus-in-event", G_CALLBACK(EmailFocusChange), 0); + g_signal_connect(gEmailEntry, "focus-out-event", G_CALLBACK(EmailFocusChange), 0); + + 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 +#if (MOZ_WIDGET_GTK == 2) + gtk_widget_set_size_request(gProgressLabel, 400, -1); +#else + gtk_label_set_max_width_chars(GTK_LABEL(gProgressLabel), LABEL_MAX_CHAR_WIDTH); +#endif + 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.size() > 0) { + 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(); + + UpdateEmail(); + UpdateSubmit(); + + UpdateHintText(gCommentText, FALSE, &gCommentFieldHint, + gStrings[ST_COMMENTGRAYTEXT].c_str()); + UpdateHintText(gEmailEntry, FALSE, &gEmailFieldHint, + gStrings[ST_EMAILGRAYTEXT].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 000000000..e274de0de --- /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" + +// 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 StringTable&)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; +- (IBAction)emailMeClicked:(id)sender; + +- (void)controlTextDidChange:(NSNotification *)note; +- (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 000000000..0768d6da3 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_osx.mm @@ -0,0 +1,922 @@ +/* -*- 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::string; +using std::vector; +using std::ostringstream; + +using namespace CrashReporter; + +static NSAutoreleasePool* gMainPool; +static CrashReporterUI* gUI = 0; +static StringTable gFiles; +static StringTable 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, +#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"]]; +} + +-(void)showCrashUI:(const StringTable&)files + queryParameters:(const StringTable&)queryParameters + sendURL:(const string&)sendURL +{ + gFiles = files; + gQueryParameters = queryParameters; + gSendURL = sendURL; + + [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)]; + [mEmailMeButton setTitle:Str(ST_CHECKEMAIL)]; + [mViewReportOkButton setTitle:Str(ST_OK)]; + + [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)]; + if (gRTLlayout) + [mCommentText toggleBaseWritingDirection:self]; + [[mEmailText cell] setPlaceholderString:Str(ST_EMAILGRAYTEXT)]; + + if (gQueryParameters.find("URL") != gQueryParameters.end()) { + // save the URL value in case the checkbox gets unchecked + gURLParameter = gQueryParameters["URL"]; + } + 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 = NO; + NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; + if (nil != [userDefaults objectForKey:@"submitReport"]) { + submitChecked = [userDefaults boolForKey:@"submitReport"]; + } + else { + // use compile-time specified enable percentage + submitChecked = ShouldEnableSending(); + [userDefaults setBool:submitChecked forKey:@"submitReport"]; + } + [mSubmitReportButton setState:(submitChecked ? 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 = [NSDictionary + dictionaryWithObject: + [NSFont boldSystemFontOfSize: + [NSFont smallSystemFontSize]] + forKey:NSFontAttributeName]; + NSDictionary* normalAttr = [NSDictionary + dictionaryWithObject: + [NSFont systemFontOfSize: + [NSFont smallSystemFontSize]] + forKey:NSFontAttributeName]; + + [mViewReportTextView setString:@""]; + for (StringTable::iterator iter = gQueryParameters.begin(); + iter != gQueryParameters.end(); + iter++) { + NSAttributedString* key = [[NSAttributedString alloc] + initWithString:NSSTR(iter->first + ": ") + attributes:boldAttr]; + NSAttributedString* value = [[NSAttributedString alloc] + initWithString:NSSTR(iter->second + "\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]; +} + +-(IBAction)emailMeClicked:(id)sender +{ + [self updateEmail]; +} + +-(void)controlTextDidChange:(NSNotification *)note +{ + [self updateEmail]; +} + +- (void)textDidChange:(NSNotification *)aNotification +{ + // update comment parameter + if ([[[mCommentText textStorage] mutableString] length] > 0) + gQueryParameters["Comments"] = [[[mCommentText textStorage] mutableString] + UTF8String]; + else + gQueryParameters.erase("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, + mEmailMeButton + }; + + for (int i=0; i<3; i++) { + NSRect frame = [checkboxes[i] frame]; + [checkboxes[i] sizeToFit]; + if (gRTLlayout) { + // sizeToFit will keep the left side fixed, so realign + float oldWidth = frame.size.width; + frame = [checkboxes[i] frame]; + frame.origin.x += oldWidth - frame.size.width; + [checkboxes[i] setFrame: frame]; + } + // keep existing spacing on left side, + 20 px spare on right + float neededWidth = frame.origin.x + frame.size.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, + mEmailMeButton, + mEmailText, + mProgressIndicator, + mProgressText + }; + for (unsigned int i=0; i<sizeof(views)/sizeof(views[0]); i++) { + [views[i] 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]; + [mEmailMeButton setEnabled:enabled]; + [mCommentText setEnabled:enabled]; + [mCommentScrollView setHasVerticalScroller:enabled]; + [self updateEmail]; +} + +-(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.erase("URL"); + } +} + +-(void)updateEmail +{ + if ([mEmailMeButton state] == NSOnState && + [mSubmitReportButton state] == NSOnState) { + NSString* email = [mEmailText stringValue]; + gQueryParameters["Email"] = [email UTF8String]; + [mEmailText setEnabled:YES]; + } else { + gQueryParameters.erase("Email"); + [mEmailText setEnabled:NO]; + } +} + +-(void)sendReport +{ + if (![self setupPost]) { + LogMessage("Crash report submission failed: could not set up POST data"); + [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; + + NSMutableDictionary* parameters = + [[NSMutableDictionary alloc] initWithCapacity: gQueryParameters.size()]; + if (!parameters) return false; + + StringTable::const_iterator end = gQueryParameters.end(); + for (StringTable::const_iterator i = gQueryParameters.begin(); + i != end; + i++) { + NSString* key = NSSTR(i->first); + NSString* value = NSSTR(i->second); + if (key && value) { + [parameters setObject: value forKey: key]; + } else { + ostringstream message; + message << "Warning: skipping annotation '" << i->first + << "' due to malformed UTF-8 encoding"; + LogMessage(message.str()); + } + } + + for (StringTable::const_iterator i = gFiles.begin(); + i != gFiles.end(); + i++) { + [mPost addFileAtPath: NSSTR(i->second) name: NSSTR(i->first)]; + } + + [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); + + [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:NSRightTextAlignment + 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; + + [NSBundle loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu") + owner:NSApp]; + + return true; +} + +void UIShutdown() +{ + [gMainPool release]; +} + +void UIShowDefaultUI() +{ + [gUI showErrorUI: gStrings[ST_CRASHREPORTERDEFAULT]]; + [NSApp run]; +} + +bool UIShowCrashUI(const StringTable& files, + const StringTable& 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) +{ + FSRef foundRef; + OSErr err = FSFindFolder(kUserDomain, kApplicationSupportFolderType, + kCreateFolder, &foundRef); + if (err != noErr) + return false; + + unsigned char path[PATH_MAX]; + FSRefMakePath(&foundRef, path, sizeof(path)); + NSString* destPath = [NSString stringWithUTF8String:reinterpret_cast<char*>(path)]; + + // 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 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 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); +} + +bool UIDeleteFile(const string& file) +{ + return (unlink(file.c_str()) != -1); +} + +std::ifstream* UIOpenRead(const string& filename) +{ + return new std::ifstream(filename.c_str(), std::ios::in); +} + +std::ofstream* UIOpenWrite(const string& filename, + bool append, // append=false + bool binary) // binary=false +{ + std::ios_base::openmode mode = std::ios::out; + + if (append) { + mode = mode | std::ios::app; + } + + if (binary) { + mode = mode | std::ios::binary; + } + + return new std::ofstream(filename.c_str(), mode); +} diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp new file mode 100644 index 000000000..f42a35616 --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp @@ -0,0 +1,85 @@ +/* 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 <dirent.h> +#include <errno.h> +#include <sys/stat.h> +#include <unistd.h> + +using namespace CrashReporter; +using std::string; +using std::vector; +using std::sort; + +struct FileData +{ + time_t timestamp; + string path; +}; + +static bool CompareFDTime(const FileData& fd1, const FileData& fd2) +{ + return fd1.timestamp > fd2.timestamp; +} + +void UIPruneSavedDumps(const std::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); + } + + 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(); + } +} + +void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +{ + // Run the minidump analyzer and wait for it to finish + pid_t pid = fork(); + + if (pid == -1) { + return; // Nothing to do upon failure + } else if (pid == 0) { + execl(exename.c_str(), exename.c_str(), filename.c_str(), nullptr); + } else { + waitpid(pid, nullptr, 0); + } +} diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp new file mode 100644 index 000000000..57ca495ba --- /dev/null +++ b/toolkit/crashreporter/client/crashreporter_win.cpp @@ -0,0 +1,1568 @@ +/* -*- 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 "client/windows/sender/crash_report_sender.h" +#include "common/windows/string_utils-inl.h" + +#define CRASH_REPORTER_VALUE L"Enabled" +#define SUBMIT_REPORT_VALUE L"SubmitCrashReport" +#define SUBMIT_REPORT_OLD L"SubmitReport" +#define INCLUDE_URL_VALUE L"IncludeURL" +#define EMAIL_ME_VALUE L"EmailMe" +#define EMAIL_VALUE L"Email" +#define MAX_EMAIL_LENGTH 1024 + +#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::string; +using std::wstring; +using std::map; +using std::vector; +using std::set; +using std::ios; +using std::ifstream; +using std::ofstream; + +using namespace CrashReporter; + +typedef struct { + HWND hDlg; + map<wstring,wstring> 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 map<wstring,wstring> gQueryParameters; +static wstring gCrashReporterKey(L"Software\\Mozilla\\Crash Reporter"); +static wstring 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_EMAILMECHECK, + IDC_EMAILTEXT, + 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; +} + +// Removes a value from HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER, if it exists. +static void RemoveUnusedValues(const wchar_t* key, LPCTSTR valueName) +{ + HKEY hRegKey; + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, KEY_SET_VALUE, &hRegKey) + == ERROR_SUCCESS) { + RegDeleteValue(hRegKey, valueName); + RegCloseKey(hRegKey); + } + + if (RegOpenKeyEx(HKEY_CURRENT_USER, key, 0, KEY_SET_VALUE, &hRegKey) + == ERROR_SUCCESS) { + RegDeleteValue(hRegKey, valueName); + RegCloseKey(hRegKey); + } +} + +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; + + // remove the old value from the registry if it exists + RemoveUnusedValues(key, SUBMIT_REPORT_OLD); + + 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 bool GetStringValue(HKEY hRegKey, LPCTSTR valueName, wstring& value) +{ + DWORD type, dataSize; + wchar_t buf[2048]; + dataSize = sizeof(buf); + if (RegQueryValueEx(hRegKey, valueName, nullptr, + &type, (LPBYTE)buf, &dataSize) == ERROR_SUCCESS && + type == REG_SZ) { + value = buf; + return true; + } + + return false; +} + +static bool GetStringKey(const wchar_t* key, + const wchar_t* valueName, + wstring& value) +{ + value = L""; + bool found = false; + HKEY hRegKey; + // see if our reg key is set globally + if (RegOpenKey(HKEY_LOCAL_MACHINE, key, &hRegKey) == ERROR_SUCCESS) { + if (GetStringValue(hRegKey, valueName, value)) { + found = true; + } + RegCloseKey(hRegKey); + } else { + // look for it in user settings + if (RegOpenKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) { + if (GetStringValue(hRegKey, valueName, value)) { + found = true; + } + RegCloseKey(hRegKey); + } + } + + return found; +} + +static void SetStringKey(const wchar_t* key, + const wchar_t* valueName, + const wstring& value) +{ + HKEY hRegKey; + if (RegCreateKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) { + RegSetValueEx(hRegKey, valueName, 0, REG_SZ, + (LPBYTE)value.c_str(), + (value.length() + 1) * sizeof(wchar_t)); + 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", 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); +} + +static void SetDlgItemDisabled(HWND hwndDlg, UINT item, bool disabled) +{ + HWND hwnd = GetDlgItem(hwndDlg, item); + LONG style = GetWindowLong(hwnd, GWL_STYLE); + if (!disabled) + style |= WS_DISABLED; + else + style &= ~WS_DISABLED; + + SetWindowLong(hwnd, GWL_STYLE, style); +} + +/* === 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 { + google_breakpad::CrashReportSender sender(L""); + finishedOk = (sender.SendCrashReport(td->sendURL, + td->queryParameters, + 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()); + } + } + + 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 + wchar_t email[MAX_EMAIL_LENGTH]; + GetDlgItemTextW(hwndDlg, IDC_EMAILTEXT, email, + sizeof(email) / sizeof(email[0])); + SetStringKey(gCrashReporterKey.c_str(), EMAIL_VALUE, email); + + SetBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE, + IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK) != 0); + SetBoolKey(gCrashReporterKey.c_str(), EMAIL_ME_VALUE, + IsDlgButtonChecked(hwndDlg, IDC_EMAILMECHECK) != 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 (int 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_EMAILMECHECK), false); + EnableWindow(GetDlgItem(hwndDlg, IDC_EMAILTEXT), 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 (map<wstring,wstring>::const_iterator i = gQueryParameters.begin(); + i != gQueryParameters.end(); + i++) { + description += i->first; + description += L": "; + description += i->second; + 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[L"URL"] = gURLParameter; + } else { + gQueryParameters.erase(L"URL"); + } +} + +static void UpdateEmail(HWND hwndDlg) +{ + if (IsDlgButtonChecked(hwndDlg, IDC_EMAILMECHECK)) { + wchar_t email[MAX_EMAIL_LENGTH]; + GetDlgItemTextW(hwndDlg, IDC_EMAILTEXT, email, + sizeof(email) / sizeof(email[0])); + gQueryParameters[L"Email"] = email; + if (IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK)) + EnableWindow(GetDlgItem(hwndDlg, IDC_EMAILTEXT), true); + } else { + gQueryParameters.erase(L"Email"); + EnableWindow(GetDlgItem(hwndDlg, IDC_EMAILTEXT), false); + } +} + +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[L"Comments"] = comment; + else + gQueryParameters.erase(L"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_EMAILMECHECK, + IDC_EMAILTEXT, + IDC_PROGRESSTEXT + }; + + RECT dlgRect; + GetClientRect(hwndDlg, &dlgRect); + + for (int 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); + EnableWindow(GetDlgItem(hwndDlg, IDC_EMAILMECHECK), enabled); + EnableWindow(GetDlgItem(hwndDlg, IDC_EMAILTEXT), + enabled && (IsDlgButtonChecked(hwndDlg, IDC_EMAILMECHECK) + != 0)); + 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 = ShouldEnableSending(); + + 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); + } + + hwnd = GetDlgItem(hwndDlg, IDC_EMAILMECHECK); + GetRelativeRect(hwnd, hwndDlg, &rect); + diff = ResizeControl(hwnd, rect, Str(ST_CHECKEMAIL), false, + gCheckboxPadding); + maxdiff = std::max(diff, maxdiff); + SetDlgItemText(hwndDlg, IDC_EMAILMECHECK, Str(ST_CHECKEMAIL).c_str()); + + if (CheckBoolKey(gCrashReporterKey.c_str(), EMAIL_ME_VALUE, &enabled) && + enabled) { + CheckDlgButton(hwndDlg, IDC_EMAILMECHECK, BST_CHECKED); + } else { + CheckDlgButton(hwndDlg, IDC_EMAILMECHECK, BST_UNCHECKED); + } + + wstring email; + if (GetStringKey(gCrashReporterKey.c_str(), EMAIL_VALUE, email)) { + SetDlgItemText(hwndDlg, IDC_EMAILTEXT, email.c_str()); + } + + // Subclass email edit control to get placeholder text + HWND hwndEmail = GetDlgItem(hwndDlg, IDC_EMAILTEXT); + OldWndProc = (WNDPROC)SetWindowLongPtr(hwndEmail, + GWLP_WNDPROC, + (LONG_PTR)EditSubclassProc); + SetWindowLongPtr(hwndEmail, GWLP_USERDATA, (LONG_PTR)OldWndProc); + wstring emailGrayText = Str(ST_EMAILGRAYTEXT); + hMem = (wchar_t*)GlobalAlloc(GPTR, (emailGrayText.length() + 1)*sizeof(wchar_t)); + wcscpy(hMem, emailGrayText.c_str()); + SetProp(hwndEmail, L"PROP_GRAYTEXT", hMem); + + 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.find(L"URL") == gQueryParameters.end()) { + RECT urlCheckRect, emailCheckRect; + GetWindowRect(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), &urlCheckRect); + GetWindowRect(GetDlgItem(hwndDlg, IDC_EMAILMECHECK), &emailCheckRect); + + SetDlgItemVisible(hwndDlg, IDC_INCLUDEURLCHECK, false); + + gAttachedBottom.erase(IDC_VIEWREPORTBUTTON); + gAttachedBottom.erase(IDC_SUBMITREPORTCHECK); + gAttachedBottom.erase(IDC_COMMENTTEXT); + + StretchDialog(hwndDlg, urlCheckRect.top - emailCheckRect.top); + + 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); + UpdateEmail(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_EMAILMECHECK: + UpdateEmail(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_EMAILTEXT: + UpdateEmail(hwndDlg); + break; + 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_LBUTTONDOWN: { + HWND hwndEmail = GetDlgItem(hwndDlg, IDC_EMAILTEXT); + POINT p = { LOWORD(lParam), HIWORD(lParam) }; + // if the email edit control is clicked, enable it, + // check the email checkbox, and focus the email edit control + if (ChildWindowFromPoint(hwndDlg, p) == hwndEmail && + IsWindowEnabled(GetDlgItem(hwndDlg, IDC_RESTARTBUTTON)) && + !IsWindowEnabled(hwndEmail) && + IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0) { + CheckDlgButton(hwndDlg, IDC_EMAILMECHECK, BST_CHECKED); + UpdateEmail(hwndDlg); + SetFocus(hwndEmail); + } + break; + } + + 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 (int 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 StringTable& 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); + } + + for (StringTable::const_iterator i = queryParameters.begin(); + i != queryParameters.end(); + i++) { + gQueryParameters[UTF8ToWide(i->first)] = UTF8ToWide(i->second); + } + + if (gQueryParameters.find(L"Vendor") != gQueryParameters.end()) { + gCrashReporterKey = L"Software\\"; + if (!gQueryParameters[L"Vendor"].empty()) { + gCrashReporterKey += gQueryParameters[L"Vendor"] + L"\\"; + } + gCrashReporterKey += gQueryParameters[L"ProductName"] + L"\\Crash Reporter"; + } + + if (gQueryParameters.find(L"URL") != gQueryParameters.end()) + gURLParameter = gQueryParameters[L"URL"]; + + gRestartArgs = restartArgs; + + if (gStrings.find("isRTL") != gStrings.end() && + gStrings["isRTL"] == "yes") + gRTLlayout = 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, size, dwRes; + dwRes = ::RegOpenKeyExW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\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) +{ + // adapted from breakpad's src/common/windows/http_upload.cc + +#if defined(_MSC_VER) + ifstream* file = new ifstream(); + file->open(UTF8ToWide(filename).c_str(), ios::in); +#else // GCC + ifstream* file = new ifstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), + ios::in); +#endif // _MSC_VER + + return file; +} + +ofstream* UIOpenWrite(const string& filename, + bool append, // append=false + bool binary) // binary=false +{ + // adapted from breakpad's src/common/windows/http_upload.cc + std::ios_base::openmode mode = ios::out; + if (append) { + mode = mode | ios::app; + } + if (binary) { + mode = mode | ios::binary; + } + +#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(); + } +} + +void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +{ + wstring cmdLine; + + cmdLine += L"\"" + UTF8ToWide(exename) + L"\" "; + cmdLine += L"\"" + UTF8ToWide(filename) + L"\" "; + + STARTUPINFO si = {}; + PROCESS_INFORMATION pi = {}; + + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOWNORMAL; + + if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE, + 0, nullptr, nullptr, &si, &pi)) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } +} diff --git a/toolkit/crashreporter/client/macbuild/Contents/Info.plist b/toolkit/crashreporter/client/macbuild/Contents/Info.plist new file mode 100644 index 000000000..299581f52 --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Info.plist @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//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>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo b/toolkit/crashreporter/client/macbuild/Contents/PkgInfo new file mode 100644 index 000000000..cae6d0a58 --- /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 000000000..6fe086e35 --- /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 = "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 000000000..e31ff0bfb --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,102 @@ +<?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>emailMeClicked</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 000000000..517349ffc --- /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 000000000..bfdcccb74 --- /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 000000000..e31ff0bfb --- /dev/null +++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib @@ -0,0 +1,102 @@ +<?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>emailMeClicked</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 000000000..4a2251aaf --- /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 000000000..6c93849b9 --- /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 000000000..341cd05a4 --- /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 000000000..456c794af --- /dev/null +++ b/toolkit/crashreporter/client/moz.build @@ -0,0 +1,78 @@ +# -*- 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 += [ + 'crashreporter.cpp', + ] + +if CONFIG['OS_ARCH'] == 'WINNT': + UNIFIED_SOURCES += [ + 'crashreporter_win.cpp', + ] + DEFINES['UNICODE'] = True + DEFINES['_UNICODE'] = True + USE_LIBS += [ + 'google_breakpad_libxul_s', + ] + OS_LIBS += [ + 'comctl32', + 'shell32', + 'wininet', + 'shlwapi', + ] +elif CONFIG['OS_ARCH'] == 'Darwin': + UNIFIED_SOURCES += [ + 'crashreporter_osx.mm', + 'crashreporter_unix_common.cpp', + ] + LOCAL_INCLUDES += [ + '../google-breakpad/src/common/mac', + ] + OS_LIBS += ['-framework Cocoa'] + USE_LIBS += [ + 'breakpad_common_s', + 'breakpad_mac_common_s', + ] +elif CONFIG['OS_ARCH'] == 'SunOS': + SOURCES += [ + 'crashreporter_linux.cpp', + 'crashreporter_unix.cpp', + ] + USE_LIBS += [ + 'breakpad_solaris_common_s', + ] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + UNIFIED_SOURCES += [ + 'crashreporter_gtk_common.cpp', + 'crashreporter_linux.cpp', + 'crashreporter_unix_common.cpp' + ] + USE_LIBS += [ + 'breakpad_linux_common_s', + ] + OS_LIBS += CONFIG['TK_LIBS'] + OS_LIBS += CONFIG['MOZ_GTHREAD_LIBS'] + CXXFLAGS += CONFIG['TK_CFLAGS'] + CXXFLAGS += CONFIG['MOZ_GTHREAD_CFLAGS'] + +if CONFIG['OS_ARCH'] == 'Linux' or CONFIG['OS_ARCH'] == 'SunOS': + FINAL_TARGET_FILES += [ + '/toolkit/themes/windows/global/throbber/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. +DISABLE_STL_WRAPPING = True + +include('/toolkit/crashreporter/crashreporter.mozbuild') diff --git a/toolkit/crashreporter/client/resource.h b/toolkit/crashreporter/client/resource.h new file mode 100644 index 000000000..d736b0367 --- /dev/null +++ b/toolkit/crashreporter/client/resource.h @@ -0,0 +1,37 @@ +/* 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_EMAILMECHECK 1008 +#define IDC_EMAILTEXT 1009 +#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 |