michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "crashreporter.h" michael@0: michael@0: #ifdef _MSC_VER michael@0: // Disable exception handler warnings. michael@0: # pragma warning( disable : 4530 ) michael@0: #endif michael@0: michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include "mozilla/NullPtr.h" michael@0: michael@0: using std::string; michael@0: using std::istream; michael@0: using std::ifstream; michael@0: using std::istringstream; michael@0: using std::ostringstream; michael@0: using std::ostream; michael@0: using std::ofstream; michael@0: using std::vector; michael@0: using std::auto_ptr; michael@0: michael@0: namespace CrashReporter { michael@0: michael@0: StringTable gStrings; michael@0: string gSettingsPath; michael@0: int gArgc; michael@0: char** gArgv; michael@0: michael@0: static auto_ptr gLogStream(nullptr); michael@0: static string gReporterDumpFile; michael@0: static string gExtraFile; michael@0: michael@0: static string kExtraDataExtension = ".extra"; michael@0: michael@0: void UIError(const string& message) michael@0: { michael@0: string errorMessage; michael@0: if (!gStrings[ST_CRASHREPORTERERROR].empty()) { michael@0: char buf[2048]; michael@0: UI_SNPRINTF(buf, 2048, michael@0: gStrings[ST_CRASHREPORTERERROR].c_str(), michael@0: message.c_str()); michael@0: errorMessage = buf; michael@0: } else { michael@0: errorMessage = message; michael@0: } michael@0: michael@0: UIError_impl(errorMessage); michael@0: } michael@0: michael@0: static string Unescape(const string& str) michael@0: { michael@0: string ret; michael@0: for (string::const_iterator iter = str.begin(); michael@0: iter != str.end(); michael@0: iter++) { michael@0: if (*iter == '\\') { michael@0: iter++; michael@0: if (*iter == '\\'){ michael@0: ret.push_back('\\'); michael@0: } else if (*iter == 'n') { michael@0: ret.push_back('\n'); michael@0: } else if (*iter == 't') { michael@0: ret.push_back('\t'); michael@0: } michael@0: } else { michael@0: ret.push_back(*iter); michael@0: } michael@0: } michael@0: michael@0: return ret; michael@0: } michael@0: michael@0: static string Escape(const string& str) michael@0: { michael@0: string ret; michael@0: for (string::const_iterator iter = str.begin(); michael@0: iter != str.end(); michael@0: iter++) { michael@0: if (*iter == '\\') { michael@0: ret += "\\\\"; michael@0: } else if (*iter == '\n') { michael@0: ret += "\\n"; michael@0: } else if (*iter == '\t') { michael@0: ret += "\\t"; michael@0: } else { michael@0: ret.push_back(*iter); michael@0: } michael@0: } michael@0: michael@0: return ret; michael@0: } michael@0: michael@0: bool ReadStrings(istream& in, StringTable& strings, bool unescape) michael@0: { michael@0: string currentSection; michael@0: while (!in.eof()) { michael@0: string line; michael@0: std::getline(in, line); michael@0: int sep = line.find('='); michael@0: if (sep >= 0) { michael@0: string key, value; michael@0: key = line.substr(0, sep); michael@0: value = line.substr(sep + 1); michael@0: if (unescape) michael@0: value = Unescape(value); michael@0: strings[key] = value; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bool ReadStringsFromFile(const string& path, michael@0: StringTable& strings, michael@0: bool unescape) michael@0: { michael@0: ifstream* f = UIOpenRead(path); michael@0: bool success = false; michael@0: if (f->is_open()) { michael@0: success = ReadStrings(*f, strings, unescape); michael@0: f->close(); michael@0: } michael@0: michael@0: delete f; michael@0: return success; michael@0: } michael@0: michael@0: bool WriteStrings(ostream& out, michael@0: const string& header, michael@0: StringTable& strings, michael@0: bool escape) michael@0: { michael@0: out << "[" << header << "]" << std::endl; michael@0: for (StringTable::iterator iter = strings.begin(); michael@0: iter != strings.end(); michael@0: iter++) { michael@0: out << iter->first << "="; michael@0: if (escape) michael@0: out << Escape(iter->second); michael@0: else michael@0: out << iter->second; michael@0: michael@0: out << std::endl; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bool WriteStringsToFile(const string& path, michael@0: const string& header, michael@0: StringTable& strings, michael@0: bool escape) michael@0: { michael@0: ofstream* f = UIOpenWrite(path.c_str()); michael@0: bool success = false; michael@0: if (f->is_open()) { michael@0: success = WriteStrings(*f, header, strings, escape); michael@0: f->close(); michael@0: } michael@0: michael@0: delete f; michael@0: return success; michael@0: } michael@0: michael@0: void LogMessage(const std::string& message) michael@0: { michael@0: if (gLogStream.get()) { michael@0: char date[64]; michael@0: time_t tm; michael@0: time(&tm); michael@0: if (strftime(date, sizeof(date) - 1, "%c", localtime(&tm)) == 0) michael@0: date[0] = '\0'; michael@0: (*gLogStream) << "[" << date << "] " << message << std::endl; michael@0: } michael@0: } michael@0: michael@0: static void OpenLogFile() michael@0: { michael@0: string logPath = gSettingsPath + UI_DIR_SEPARATOR + "submit.log"; michael@0: gLogStream.reset(UIOpenWrite(logPath.c_str(), true)); michael@0: } michael@0: michael@0: static bool ReadConfig() michael@0: { michael@0: string iniPath; michael@0: if (!UIGetIniPath(iniPath)) michael@0: return false; michael@0: michael@0: if (!ReadStringsFromFile(iniPath, gStrings, true)) michael@0: return false; michael@0: michael@0: // See if we have a string override file, if so process it michael@0: char* overrideEnv = getenv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE"); michael@0: if (overrideEnv && *overrideEnv && UIFileExists(overrideEnv)) michael@0: ReadStringsFromFile(overrideEnv, gStrings, true); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: static string GetExtraDataFilename(const string& dumpfile) michael@0: { michael@0: string filename(dumpfile); michael@0: int dot = filename.rfind('.'); michael@0: if (dot < 0) michael@0: return ""; michael@0: michael@0: filename.replace(dot, filename.length() - dot, kExtraDataExtension); michael@0: return filename; michael@0: } michael@0: michael@0: static string Basename(const string& file) michael@0: { michael@0: int slashIndex = file.rfind(UI_DIR_SEPARATOR); michael@0: if (slashIndex >= 0) michael@0: return file.substr(slashIndex + 1); michael@0: else michael@0: return file; michael@0: } michael@0: michael@0: static bool MoveCrashData(const string& toDir, michael@0: string& dumpfile, michael@0: string& extrafile) michael@0: { michael@0: if (!UIEnsurePathExists(toDir)) { michael@0: UIError(gStrings[ST_ERROR_CREATEDUMPDIR]); michael@0: return false; michael@0: } michael@0: michael@0: string newDump = toDir + UI_DIR_SEPARATOR + Basename(dumpfile); michael@0: string newExtra = toDir + UI_DIR_SEPARATOR + Basename(extrafile); michael@0: michael@0: if (!UIMoveFile(dumpfile, newDump)) { michael@0: UIError(gStrings[ST_ERROR_DUMPFILEMOVE]); michael@0: return false; michael@0: } michael@0: michael@0: if (!UIMoveFile(extrafile, newExtra)) { michael@0: UIError(gStrings[ST_ERROR_EXTRAFILEMOVE]); michael@0: return false; michael@0: } michael@0: michael@0: dumpfile = newDump; michael@0: extrafile = newExtra; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: static bool AddSubmittedReport(const string& serverResponse) michael@0: { michael@0: StringTable responseItems; michael@0: istringstream in(serverResponse); michael@0: ReadStrings(in, responseItems, false); michael@0: michael@0: if (responseItems.find("StopSendingReportsFor") != responseItems.end()) { michael@0: // server wants to tell us to stop sending reports for a certain version michael@0: string reportPath = michael@0: gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + michael@0: responseItems["StopSendingReportsFor"]; michael@0: michael@0: ofstream* reportFile = UIOpenWrite(reportPath); michael@0: if (reportFile->is_open()) { michael@0: // don't really care about the contents michael@0: *reportFile << 1 << "\n"; michael@0: reportFile->close(); michael@0: } michael@0: delete reportFile; michael@0: } michael@0: michael@0: if (responseItems.find("Discarded") != responseItems.end()) { michael@0: // server discarded this report... save it so the user can resubmit it michael@0: // manually michael@0: return false; michael@0: } michael@0: michael@0: if (responseItems.find("CrashID") == responseItems.end()) michael@0: return false; michael@0: michael@0: string submittedDir = michael@0: gSettingsPath + UI_DIR_SEPARATOR + "submitted"; michael@0: if (!UIEnsurePathExists(submittedDir)) { michael@0: return false; michael@0: } michael@0: michael@0: string path = submittedDir + UI_DIR_SEPARATOR + michael@0: responseItems["CrashID"] + ".txt"; michael@0: michael@0: ofstream* file = UIOpenWrite(path); michael@0: if (!file->is_open()) { michael@0: delete file; michael@0: return false; michael@0: } michael@0: michael@0: char buf[1024]; michael@0: UI_SNPRINTF(buf, 1024, michael@0: gStrings["CrashID"].c_str(), michael@0: responseItems["CrashID"].c_str()); michael@0: *file << buf << "\n"; michael@0: michael@0: if (responseItems.find("ViewURL") != responseItems.end()) { michael@0: UI_SNPRINTF(buf, 1024, michael@0: gStrings["CrashDetailsURL"].c_str(), michael@0: responseItems["ViewURL"].c_str()); michael@0: *file << buf << "\n"; michael@0: } michael@0: michael@0: file->close(); michael@0: delete file; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: void DeleteDump() michael@0: { michael@0: const char* noDelete = getenv("MOZ_CRASHREPORTER_NO_DELETE_DUMP"); michael@0: if (!noDelete || *noDelete == '\0') { michael@0: if (!gReporterDumpFile.empty()) michael@0: UIDeleteFile(gReporterDumpFile); michael@0: if (!gExtraFile.empty()) michael@0: UIDeleteFile(gExtraFile); michael@0: } michael@0: } michael@0: michael@0: void SendCompleted(bool success, const string& serverResponse) michael@0: { michael@0: if (success) { michael@0: if (AddSubmittedReport(serverResponse)) { michael@0: DeleteDump(); michael@0: } michael@0: else { michael@0: string directory = gReporterDumpFile; michael@0: int slashpos = directory.find_last_of("/\\"); michael@0: if (slashpos < 2) michael@0: return; michael@0: directory.resize(slashpos); michael@0: UIPruneSavedDumps(directory); michael@0: } michael@0: } michael@0: } michael@0: michael@0: bool ShouldEnableSending() michael@0: { michael@0: srand(time(0)); michael@0: return ((rand() % 100) < MOZ_CRASHREPORTER_ENABLE_PERCENT); michael@0: } michael@0: michael@0: } // namespace CrashReporter michael@0: michael@0: using namespace CrashReporter; michael@0: michael@0: void RewriteStrings(StringTable& queryParameters) michael@0: { michael@0: // rewrite some UI strings with the values from the query parameters michael@0: string product = queryParameters["ProductName"]; michael@0: string vendor = queryParameters["Vendor"]; michael@0: if (vendor.empty()) { michael@0: // Assume Mozilla if no vendor is specified michael@0: vendor = "Mozilla"; michael@0: } michael@0: michael@0: char buf[4096]; michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_CRASHREPORTERVENDORTITLE].c_str(), michael@0: vendor.c_str()); michael@0: gStrings[ST_CRASHREPORTERTITLE] = buf; michael@0: michael@0: michael@0: string str = gStrings[ST_CRASHREPORTERPRODUCTERROR]; michael@0: // Only do the replacement here if the string has two michael@0: // format specifiers to start. Otherwise michael@0: // we assume it has the product name hardcoded. michael@0: string::size_type pos = str.find("%s"); michael@0: if (pos != string::npos) michael@0: pos = str.find("%s", pos+2); michael@0: if (pos != string::npos) { michael@0: // Leave a format specifier for UIError to fill in michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_CRASHREPORTERPRODUCTERROR].c_str(), michael@0: product.c_str(), michael@0: "%s"); michael@0: gStrings[ST_CRASHREPORTERERROR] = buf; michael@0: } michael@0: else { michael@0: // product name is hardcoded michael@0: gStrings[ST_CRASHREPORTERERROR] = str; michael@0: } michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(), michael@0: product.c_str()); michael@0: gStrings[ST_CRASHREPORTERDESCRIPTION] = buf; michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_CHECKSUBMIT].c_str(), michael@0: vendor.c_str()); michael@0: gStrings[ST_CHECKSUBMIT] = buf; michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_CHECKEMAIL].c_str(), michael@0: vendor.c_str()); michael@0: gStrings[ST_CHECKEMAIL] = buf; michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_RESTART].c_str(), michael@0: product.c_str()); michael@0: gStrings[ST_RESTART] = buf; michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_QUIT].c_str(), michael@0: product.c_str()); michael@0: gStrings[ST_QUIT] = buf; michael@0: michael@0: UI_SNPRINTF(buf, sizeof(buf), michael@0: gStrings[ST_ERROR_ENDOFLIFE].c_str(), michael@0: product.c_str()); michael@0: gStrings[ST_ERROR_ENDOFLIFE] = buf; michael@0: } michael@0: michael@0: bool CheckEndOfLifed(string version) michael@0: { michael@0: string reportPath = michael@0: gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + version; michael@0: return UIFileExists(reportPath); michael@0: } michael@0: michael@0: int main(int argc, char** argv) michael@0: { michael@0: gArgc = argc; michael@0: gArgv = argv; michael@0: michael@0: if (!ReadConfig()) { michael@0: UIError("Couldn't read configuration."); michael@0: return 0; michael@0: } michael@0: michael@0: if (!UIInit()) michael@0: return 0; michael@0: michael@0: if (argc > 1) { michael@0: gReporterDumpFile = argv[1]; michael@0: } michael@0: michael@0: if (gReporterDumpFile.empty()) { michael@0: // no dump file specified, run the default UI michael@0: UIShowDefaultUI(); michael@0: } else { michael@0: gExtraFile = GetExtraDataFilename(gReporterDumpFile); michael@0: if (gExtraFile.empty()) { michael@0: UIError(gStrings[ST_ERROR_BADARGUMENTS]); michael@0: return 0; michael@0: } michael@0: michael@0: if (!UIFileExists(gExtraFile)) { michael@0: UIError(gStrings[ST_ERROR_EXTRAFILEEXISTS]); michael@0: return 0; michael@0: } michael@0: michael@0: StringTable queryParameters; michael@0: if (!ReadStringsFromFile(gExtraFile, queryParameters, true)) { michael@0: UIError(gStrings[ST_ERROR_EXTRAFILEREAD]); michael@0: return 0; michael@0: } michael@0: michael@0: if (queryParameters.find("ProductName") == queryParameters.end()) { michael@0: UIError(gStrings[ST_ERROR_NOPRODUCTNAME]); michael@0: return 0; michael@0: } michael@0: michael@0: // There is enough information in the extra file to rewrite strings michael@0: // to be product specific michael@0: RewriteStrings(queryParameters); michael@0: michael@0: if (queryParameters.find("ServerURL") == queryParameters.end()) { michael@0: UIError(gStrings[ST_ERROR_NOSERVERURL]); michael@0: return 0; michael@0: } michael@0: michael@0: // Hopefully the settings path exists in the environment. Try that before michael@0: // asking the platform-specific code to guess. michael@0: #ifdef XP_WIN32 michael@0: static const wchar_t kDataDirKey[] = L"MOZ_CRASHREPORTER_DATA_DIRECTORY"; michael@0: const wchar_t *settingsPath = _wgetenv(kDataDirKey); michael@0: if (settingsPath && *settingsPath) { michael@0: gSettingsPath = WideToUTF8(settingsPath); michael@0: } michael@0: #else michael@0: static const char kDataDirKey[] = "MOZ_CRASHREPORTER_DATA_DIRECTORY"; michael@0: const char *settingsPath = getenv(kDataDirKey); michael@0: if (settingsPath && *settingsPath) { michael@0: gSettingsPath = settingsPath; michael@0: } michael@0: #endif michael@0: else { michael@0: string product = queryParameters["ProductName"]; michael@0: string vendor = queryParameters["Vendor"]; michael@0: if (!UIGetSettingsPath(vendor, product, gSettingsPath)) { michael@0: gSettingsPath.clear(); michael@0: } michael@0: } michael@0: michael@0: if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) { michael@0: UIError(gStrings[ST_ERROR_NOSETTINGSPATH]); michael@0: return 0; michael@0: } michael@0: michael@0: OpenLogFile(); michael@0: michael@0: if (!UIFileExists(gReporterDumpFile)) { michael@0: UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]); michael@0: return 0; michael@0: } michael@0: michael@0: string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending"; michael@0: if (!MoveCrashData(pendingDir, gReporterDumpFile, gExtraFile)) { michael@0: return 0; michael@0: } michael@0: michael@0: string sendURL = queryParameters["ServerURL"]; michael@0: // we don't need to actually send this michael@0: queryParameters.erase("ServerURL"); michael@0: michael@0: queryParameters["Throttleable"] = "1"; michael@0: michael@0: // re-set XUL_APP_FILE for xulrunner wrapped apps michael@0: const char *appfile = getenv("MOZ_CRASHREPORTER_RESTART_XUL_APP_FILE"); michael@0: if (appfile && *appfile) { michael@0: const char prefix[] = "XUL_APP_FILE="; michael@0: char *env = (char*) malloc(strlen(appfile) + strlen(prefix) + 1); michael@0: if (!env) { michael@0: UIError("Out of memory"); michael@0: return 0; michael@0: } michael@0: strcpy(env, prefix); michael@0: strcat(env, appfile); michael@0: putenv(env); michael@0: free(env); michael@0: } michael@0: michael@0: vector restartArgs; michael@0: michael@0: ostringstream paramName; michael@0: int i = 0; michael@0: paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++; michael@0: const char *param = getenv(paramName.str().c_str()); michael@0: while (param && *param) { michael@0: restartArgs.push_back(param); michael@0: michael@0: paramName.str(""); michael@0: paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++; michael@0: param = getenv(paramName.str().c_str()); michael@0: }; michael@0: michael@0: // allow override of the server url via environment variable michael@0: //XXX: remove this in the far future when our robot michael@0: // masters force everyone to use XULRunner michael@0: char* urlEnv = getenv("MOZ_CRASHREPORTER_URL"); michael@0: if (urlEnv && *urlEnv) { michael@0: sendURL = urlEnv; michael@0: } michael@0: michael@0: // see if this version has been end-of-lifed michael@0: if (queryParameters.find("Version") != queryParameters.end() && michael@0: CheckEndOfLifed(queryParameters["Version"])) { michael@0: UIError(gStrings[ST_ERROR_ENDOFLIFE]); michael@0: DeleteDump(); michael@0: return 0; michael@0: } michael@0: michael@0: if (!UIShowCrashUI(gReporterDumpFile, queryParameters, sendURL, restartArgs)) michael@0: DeleteDump(); michael@0: } michael@0: michael@0: UIShutdown(); michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: #if defined(XP_WIN) && !defined(__GNUC__) michael@0: #include michael@0: michael@0: // We need WinMain in order to not be a console app. This function is unused michael@0: // if we are a console application. michael@0: int WINAPI wWinMain( HINSTANCE, HINSTANCE, LPWSTR args, int ) michael@0: { michael@0: // Remove everything except close window from the context menu michael@0: { michael@0: HKEY hkApp; michael@0: RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0, michael@0: nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr, michael@0: &hkApp, nullptr); michael@0: RegCloseKey(hkApp); michael@0: if (RegCreateKeyExW(HKEY_CURRENT_USER, michael@0: L"Software\\Classes\\Applications\\crashreporter.exe", michael@0: 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, michael@0: nullptr, &hkApp, nullptr) == ERROR_SUCCESS) { michael@0: RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0); michael@0: RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0); michael@0: RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0); michael@0: RegCloseKey(hkApp); michael@0: } michael@0: } michael@0: michael@0: char** argv = static_cast(malloc(__argc * sizeof(char*))); michael@0: for (int i = 0; i < __argc; i++) { michael@0: argv[i] = strdup(WideToUTF8(__wargv[i]).c_str()); michael@0: } michael@0: michael@0: // Do the real work. michael@0: return main(__argc, argv); michael@0: } michael@0: #endif