// Copyright 2015 The Crashpad Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "handler/crash_report_upload_thread.h" #include #include #include #include #include #include #include "base/logging.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "client/settings.h" #include "snapshot/minidump/process_snapshot_minidump.h" #include "snapshot/module_snapshot.h" #include "util/file/file_reader.h" #include "util/misc/metrics.h" #include "util/net/http_body.h" #include "util/net/http_multipart_builder.h" #include "util/net/http_transport.h" #include "util/stdlib/map_insert.h" namespace crashpad { namespace { void InsertOrReplaceMapEntry(std::map* map, const std::string& key, const std::string& value) { std::string old_value; if (!MapInsertOrReplace(map, key, value, &old_value)) { LOG(WARNING) << "duplicate key " << key << ", discarding value " << old_value; } } // Given a minidump file readable by |minidump_file_reader|, returns a map of // key-value pairs to use as HTTP form parameters for upload to a Breakpad // server. The map is built by combining the process simple annotations map with // each module’s simple annotations map. In the case of duplicate keys, the map // will retain the first value found for any key, and will log a warning about // discarded values. Each module’s annotations vector is also examined and built // into a single string value, with distinct elements separated by newlines, and // stored at the key named “list_annotations”, which supersedes any other key // found by that name. The client ID stored in the minidump is converted to // a string and stored at the key named “guid”, which supersedes any other key // found by that name. // // In the event of an error reading the minidump file, a message will be logged. std::map BreakpadHTTPFormParametersFromMinidump( FileReader* minidump_file_reader) { ProcessSnapshotMinidump minidump_process_snapshot; if (!minidump_process_snapshot.Initialize(minidump_file_reader)) { return std::map(); } std::map parameters = minidump_process_snapshot.AnnotationsSimpleMap(); std::string list_annotations; for (const ModuleSnapshot* module : minidump_process_snapshot.Modules()) { for (const auto& kv : module->AnnotationsSimpleMap()) { if (!parameters.insert(kv).second) { LOG(WARNING) << "duplicate key " << kv.first << ", discarding value " << kv.second; } } for (std::string annotation : module->AnnotationsVector()) { list_annotations.append(annotation); list_annotations.append("\n"); } } if (!list_annotations.empty()) { // Remove the final newline character. list_annotations.resize(list_annotations.size() - 1); InsertOrReplaceMapEntry(¶meters, "list_annotations", list_annotations); } UUID client_id; minidump_process_snapshot.ClientID(&client_id); InsertOrReplaceMapEntry(¶meters, "guid", client_id.ToString()); return parameters; } // Calls CrashReportDatabase::RecordUploadAttempt() with |successful| set to // false upon destruction unless disarmed by calling Fire() or Disarm(). Fire() // triggers an immediate call. Armed upon construction. class CallRecordUploadAttempt { public: CallRecordUploadAttempt(CrashReportDatabase* database, const CrashReportDatabase::Report* report) : database_(database), report_(report) { } ~CallRecordUploadAttempt() { Fire(); } void Fire() { if (report_) { database_->RecordUploadAttempt(report_, false, std::string()); } Disarm(); } void Disarm() { report_ = nullptr; } private: CrashReportDatabase* database_; // weak const CrashReportDatabase::Report* report_; // weak DISALLOW_COPY_AND_ASSIGN(CallRecordUploadAttempt); }; } // namespace CrashReportUploadThread::CrashReportUploadThread(CrashReportDatabase* database, const std::string& url, bool watch_pending_reports, bool rate_limit, bool upload_gzip) : url_(url), // When watching for pending reports, check every 15 minutes, even in the // absence of a signal from the handler thread. This allows for failed // uploads to be retried periodically, and for pending reports written by // other processes to be recognized. thread_(watch_pending_reports ? 15 * 60.0 : WorkerThread::kIndefiniteWait, this), known_pending_report_uuids_(), database_(database), watch_pending_reports_(watch_pending_reports), rate_limit_(rate_limit), upload_gzip_(upload_gzip) { } CrashReportUploadThread::~CrashReportUploadThread() { } void CrashReportUploadThread::Start() { thread_.Start(watch_pending_reports_ ? 0.0 : WorkerThread::kIndefiniteWait); } void CrashReportUploadThread::Stop() { thread_.Stop(); } void CrashReportUploadThread::ReportPending(const UUID& report_uuid) { known_pending_report_uuids_.PushBack(report_uuid); thread_.DoWorkNow(); } void CrashReportUploadThread::ProcessPendingReports() { std::vector known_report_uuids = known_pending_report_uuids_.Drain(); for (const UUID& report_uuid : known_report_uuids) { CrashReportDatabase::Report report; if (database_->LookUpCrashReport(report_uuid, &report) != CrashReportDatabase::kNoError) { continue; } ProcessPendingReport(report); // Respect Stop() being called after at least one attempt to process a // report. if (!thread_.is_running()) { return; } } // Known pending reports are always processed (above). The rest of this // function is concerned with scanning for pending reports not already known // to this thread. if (!watch_pending_reports_) { return; } std::vector reports; if (database_->GetPendingReports(&reports) != CrashReportDatabase::kNoError) { // The database is sick. It might be prudent to stop trying to poke it from // this thread by abandoning the thread altogether. On the other hand, if // the problem is transient, it might be possible to talk to it again on the // next pass. For now, take the latter approach. return; } for (const CrashReportDatabase::Report& report : reports) { if (std::find(known_report_uuids.begin(), known_report_uuids.end(), report.uuid) != known_report_uuids.end()) { // An attempt to process the report already occurred above. The report is // still pending, so upload must have failed. Don’t retry it immediately, // it can wait until at least the next pass through this method. continue; } ProcessPendingReport(report); // Respect Stop() being called after at least one attempt to process a // report. if (!thread_.is_running()) { return; } } } void CrashReportUploadThread::ProcessPendingReport( const CrashReportDatabase::Report& report) { Settings* const settings = database_->GetSettings(); bool uploads_enabled; if (url_.empty() || (!report.upload_explicitly_requested && (!settings->GetUploadsEnabled(&uploads_enabled) || !uploads_enabled))) { // Don’t attempt an upload if there’s no URL to upload to. Allow upload if // it has been explicitly requested by the user, otherwise, respect the // upload-enabled state stored in the database’s settings. database_->SkipReportUpload(report.uuid, Metrics::CrashSkippedReason::kUploadsDisabled); return; } // This currently implements very simplistic rate-limiting, compatible with // the Breakpad client, where the strategy is to permit one upload attempt per // hour, and retire reports that would exceed this limit or for which the // upload fails on the first attempt. // // If upload was requested explicitly (i.e. by user action), we do not // throttle the upload. // // TODO(mark): Provide a proper rate-limiting strategy and allow for failed // upload attempts to be retried. if (!report.upload_explicitly_requested && rate_limit_) { time_t last_upload_attempt_time; if (settings->GetLastUploadAttemptTime(&last_upload_attempt_time)) { time_t now = time(nullptr); if (now >= last_upload_attempt_time) { // If the most recent upload attempt occurred within the past hour, // don’t attempt to upload the new report. If it happened longer ago, // attempt to upload the report. const int kUploadAttemptIntervalSeconds = 60 * 60; // 1 hour if (now - last_upload_attempt_time < kUploadAttemptIntervalSeconds) { database_->SkipReportUpload( report.uuid, Metrics::CrashSkippedReason::kUploadThrottled); return; } } else { // The most recent upload attempt purportedly occurred in the future. If // it “happened” at least one day in the future, assume that the last // upload attempt time is bogus, and attempt to upload the report. If // the most recent upload time is in the future but within one day, // accept it and don’t attempt to upload the report. const int kBackwardsClockTolerance = 60 * 60 * 24; // 1 day if (last_upload_attempt_time - now < kBackwardsClockTolerance) { database_->SkipReportUpload( report.uuid, Metrics::CrashSkippedReason::kUnexpectedTime); return; } } } } const CrashReportDatabase::Report* upload_report; CrashReportDatabase::OperationStatus status = database_->GetReportForUploading(report.uuid, &upload_report); switch (status) { case CrashReportDatabase::kNoError: break; case CrashReportDatabase::kBusyError: case CrashReportDatabase::kReportNotFound: // Someone else may have gotten to it first. If they’re working on it now, // this will be kBusyError. If they’ve already finished with it, it’ll be // kReportNotFound. return; case CrashReportDatabase::kFileSystemError: case CrashReportDatabase::kDatabaseError: // In these cases, SkipReportUpload() might not work either, but it’s best // to at least try to get the report out of the way. database_->SkipReportUpload(report.uuid, Metrics::CrashSkippedReason::kDatabaseError); return; case CrashReportDatabase::kCannotRequestUpload: NOTREACHED(); return; } CallRecordUploadAttempt call_record_upload_attempt(database_, upload_report); std::string response_body; UploadResult upload_result = UploadReport(upload_report, &response_body); switch (upload_result) { case UploadResult::kSuccess: call_record_upload_attempt.Disarm(); database_->RecordUploadAttempt(upload_report, true, response_body); break; case UploadResult::kPermanentFailure: case UploadResult::kRetry: call_record_upload_attempt.Fire(); // TODO(mark): Deal with retries properly: don’t call SkipReportUplaod() // if the result was kRetry and the report hasn’t already been retried // too many times. database_->SkipReportUpload(report.uuid, Metrics::CrashSkippedReason::kUploadFailed); break; } } CrashReportUploadThread::UploadResult CrashReportUploadThread::UploadReport( const CrashReportDatabase::Report* report, std::string* response_body) { std::map parameters; { FileReader minidump_file_reader; if (!minidump_file_reader.Open(report->file_path)) { // If the minidump file can’t be opened, all hope is lost. return UploadResult::kPermanentFailure; } // If the minidump file could be opened, ignore any errors that might occur // when attempting to interpret it. This may result in its being uploaded // with few or no parameters, but as long as there’s a dump file, the server // can decide what to do with it. parameters = BreakpadHTTPFormParametersFromMinidump(&minidump_file_reader); } HTTPMultipartBuilder http_multipart_builder; http_multipart_builder.SetGzipEnabled(upload_gzip_); const char kMinidumpKey[] = "upload_file_minidump"; for (const auto& kv : parameters) { if (kv.first == kMinidumpKey) { LOG(WARNING) << "reserved key " << kv.first << ", discarding value " << kv.second; } else { http_multipart_builder.SetFormData(kv.first, kv.second); } } http_multipart_builder.SetFileAttachment( kMinidumpKey, #if defined(OS_WIN) base::UTF16ToUTF8(report->file_path.BaseName().value()), #else report->file_path.BaseName().value(), #endif report->file_path, "application/octet-stream"); std::unique_ptr http_transport(HTTPTransport::Create()); http_transport->SetURL(url_); HTTPHeaders content_headers; http_multipart_builder.PopulateContentHeaders(&content_headers); for (const auto& content_header : content_headers) { http_transport->SetHeader(content_header.first, content_header.second); } http_transport->SetBodyStream(http_multipart_builder.GetBodyStream()); // TODO(mark): The timeout should be configurable by the client. http_transport->SetTimeout(60.0); // 1 minute. if (!http_transport->ExecuteSynchronously(response_body)) { return UploadResult::kRetry; } return UploadResult::kSuccess; } void CrashReportUploadThread::DoWork(const WorkerThread* thread) { ProcessPendingReports(); } } // namespace crashpad