// 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 "client/crash_report_database.h" #include #include #include #include #include #include #include #include "base/logging.h" #include "base/numerics/safe_math.h" #include "base/strings/string16.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "client/settings.h" #include "util/file/directory_reader.h" #include "util/file/filesystem.h" #include "util/misc/implicit_cast.h" #include "util/misc/initialization_state_dcheck.h" #include "util/misc/metrics.h" namespace crashpad { namespace { constexpr wchar_t kReportsDirectory[] = L"reports"; constexpr wchar_t kMetadataFileName[] = L"metadata"; constexpr wchar_t kSettings[] = L"settings.dat"; constexpr wchar_t kCrashReportFileExtension[] = L"dmp"; constexpr uint32_t kMetadataFileHeaderMagic = 'CPAD'; constexpr uint32_t kMetadataFileVersion = 1; constexpr base::FilePath::CharType kAttachmentsDirectory[] = L"attachments"; using OperationStatus = CrashReportDatabase::OperationStatus; // Helpers --------------------------------------------------------------------- // Adds a string to the string table and returns the byte index where it was // added. uint32_t AddStringToTable(std::string* string_table, const std::string& str) { uint32_t offset = base::checked_cast(string_table->size()); *string_table += str; *string_table += '\0'; return offset; } // Converts |str| to UTF8, adds the result to the string table and returns the // byte index where it was added. uint32_t AddStringToTable(std::string* string_table, const base::string16& str) { return AddStringToTable(string_table, base::UTF16ToUTF8(str)); } // Reads from the current file position to EOF and returns as a string of bytes. std::string ReadRestOfFileAsString(FileHandle file) { FileOffset read_from = LoggingSeekFile(file, 0, SEEK_CUR); FileOffset end = LoggingSeekFile(file, 0, SEEK_END); FileOffset original = LoggingSeekFile(file, read_from, SEEK_SET); if (read_from == -1 || end == -1 || original == -1 || read_from == end) return std::string(); DCHECK_EQ(read_from, original); DCHECK_GT(end, read_from); size_t data_length = static_cast(end - read_from); std::string buffer(data_length, '\0'); return LoggingReadFileExactly(file, &buffer[0], data_length) ? buffer : std::string(); } bool UUIDFromReportPath(const base::FilePath& path, UUID* uuid) { return uuid->InitializeFromString( path.RemoveFinalExtension().BaseName().value()); } // Helper structures, and conversions ------------------------------------------ // The format of the on disk metadata file is a MetadataFileHeader, followed by // a number of fixed size records of MetadataFileReportRecord, followed by a // string table in UTF8 format, where each string is \0 terminated. struct MetadataFileHeader { uint32_t magic; uint32_t version; uint32_t num_records; uint32_t padding; }; struct ReportDisk; enum class ReportState { //! \brief Created and filled out by caller, owned by database. kPending, //! \brief In the process of uploading, owned by caller. kUploading, //! \brief Upload completed or skipped, owned by database. kCompleted, }; enum { //! \brief Corresponds to uploaded bit of the report state. kAttributeUploaded = 1 << 0, //! \brief Corresponds to upload_explicity_requested bit of the report state. kAttributeUploadExplicitlyRequested = 1 << 1, }; struct MetadataFileReportRecord { // Note that this default constructor does no initialization. It is used only // to create an array of records that are immediately initialized by reading // from disk in Metadata::Read(). MetadataFileReportRecord() {} // Constructs from a ReportDisk, adding to |string_table| and storing indices // as strings into that table. MetadataFileReportRecord(const ReportDisk& report, std::string* string_table); UUID uuid; // UUID is a 16 byte, standard layout structure. uint32_t file_path_index; // Index into string table. File name is relative // to the reports directory when on disk. uint32_t id_index; // Index into string table. int64_t creation_time; // Holds a time_t. int64_t last_upload_attempt_time; // Holds a time_t. int32_t upload_attempts; int32_t state; // A ReportState. uint8_t attributes; // Bitfield of kAttribute*. uint8_t padding[7]; }; //! \brief A private extension of the Report class that includes additional data //! that's stored on disk in the metadata file. struct ReportDisk : public CrashReportDatabase::Report { ReportDisk(const MetadataFileReportRecord& record, const base::FilePath& report_dir, const std::string& string_table); ReportDisk(const UUID& uuid, const base::FilePath& path, time_t creation_tim, ReportState state); //! \brief The current state of the report. ReportState state; }; MetadataFileReportRecord::MetadataFileReportRecord(const ReportDisk& report, std::string* string_table) : uuid(report.uuid), file_path_index( AddStringToTable(string_table, report.file_path.BaseName().value())), id_index(AddStringToTable(string_table, report.id)), creation_time(report.creation_time), last_upload_attempt_time(report.last_upload_attempt_time), upload_attempts(report.upload_attempts), state(static_cast(report.state)), attributes((report.uploaded ? kAttributeUploaded : 0) | (report.upload_explicitly_requested ? kAttributeUploadExplicitlyRequested : 0)) { memset(&padding, 0, sizeof(padding)); } ReportDisk::ReportDisk(const MetadataFileReportRecord& record, const base::FilePath& report_dir, const std::string& string_table) : Report() { uuid = record.uuid; file_path = report_dir.Append( base::UTF8ToUTF16(&string_table[record.file_path_index])); id = &string_table[record.id_index]; creation_time = record.creation_time; last_upload_attempt_time = record.last_upload_attempt_time; upload_attempts = record.upload_attempts; state = static_cast(record.state); uploaded = (record.attributes & kAttributeUploaded) != 0; upload_explicitly_requested = (record.attributes & kAttributeUploadExplicitlyRequested) != 0; } ReportDisk::ReportDisk(const UUID& uuid, const base::FilePath& path, time_t creation_time, ReportState state) : Report() { this->uuid = uuid; this->file_path = path; this->creation_time = creation_time; this->state = state; } // Metadata -------------------------------------------------------------------- //! \brief Manages the metadata for the set of reports, handling serialization //! to disk, and queries. class Metadata { public: //! \brief Writes any changes if necessary, unlocks and closes the file //! handle. ~Metadata(); static std::unique_ptr Create(const base::FilePath& metadata_file, const base::FilePath& report_dir); //! \brief Adds a new report to the set. //! //! \param[in] new_report_disk The record to add. The #state field must be set //! to kPending. void AddNewRecord(const ReportDisk& new_report_disk); //! \brief Finds all reports in a given state. The \a reports vector is only //! valid when CrashReportDatabase::kNoError is returned. //! //! \param[in] desired_state The state to match. //! \param[out] reports Matching reports, must be empty on entry. OperationStatus FindReports( ReportState desired_state, std::vector* reports) const; //! \brief Finds the report matching the given UUID. //! //! The returned report is only valid if CrashReportDatabase::kNoError is //! returned. //! //! \param[in] uuid The report identifier. //! \param[out] report_disk The found report, valid only if //! CrashReportDatabase::kNoError is returned. Ownership is not //! transferred to the caller, and the report may not be modified. OperationStatus FindSingleReport(const UUID& uuid, const ReportDisk** report_disk) const; //! \brief Finds a single report matching the given UUID and in the desired //! state, and returns a mutable ReportDisk* if found. //! //! This marks the metadata as dirty, and on destruction, changes will be //! written to disk via Write(). //! //! \return #kNoError on success. #kReportNotFound if there was no report with //! the specified UUID, or if the report was not in the specified state //! and was not uploading. #kBusyError if the report was not in the //! specified state and was uploading. OperationStatus FindSingleReportAndMarkDirty(const UUID& uuid, ReportState desired_state, ReportDisk** report_disk); //! \brief Removes a report from the metadata database, without touching the //! on-disk file. //! //! The returned report is only valid if CrashReportDatabase::kNoError is //! returned. This will mark the database as dirty. Future metadata //! operations for this report will not succeed. //! //! \param[in] uuid The report identifier to remove. //! \param[out] report_path The found report's file_path, valid only if //! CrashReportDatabase::kNoError is returned. OperationStatus DeleteReport(const UUID& uuid, base::FilePath* report_path); //! \brief Removes reports from the metadata database, that don't have //! corresponding report files. //! //! \return number of metadata entries removed int CleanDatabase(); private: Metadata(FileHandle handle, const base::FilePath& report_dir); bool Rewind(); void Read(); void Write(); //! \brief Confirms that the corresponding report actually exists on disk //! (that is, the dump file has not been removed), and that the report is //! in the given state. static OperationStatus VerifyReport(const ReportDisk& report_disk, ReportState desired_state); //! \brief Confirms that the corresponding report actually exists on disk //! (that is, the dump file has not been removed). static OperationStatus VerifyReportAnyState(const ReportDisk& report_disk); ScopedFileHandle handle_; const base::FilePath report_dir_; bool dirty_; //! \brief `true` when a Write() is required on destruction. std::vector reports_; DISALLOW_COPY_AND_ASSIGN(Metadata); }; Metadata::~Metadata() { if (dirty_) Write(); // Not actually async, UnlockFileEx requires the Offset fields. OVERLAPPED overlapped = {0}; if (!UnlockFileEx(handle_.get(), 0, MAXDWORD, MAXDWORD, &overlapped)) PLOG(ERROR) << "UnlockFileEx"; } // static std::unique_ptr Metadata::Create(const base::FilePath& metadata_file, const base::FilePath& report_dir) { // It is important that dwShareMode be non-zero so that concurrent access to // this file results in a successful open. This allows us to get to LockFileEx // which then blocks to guard access. FileHandle handle = CreateFile(metadata_file.value().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == kInvalidFileHandle) return std::unique_ptr(); // Not actually async, LockFileEx requires the Offset fields. OVERLAPPED overlapped = {0}; if (!LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &overlapped)) { PLOG(ERROR) << "LockFileEx"; return std::unique_ptr(); } std::unique_ptr metadata(new Metadata(handle, report_dir)); // If Read() fails, for whatever reason (corruption, etc.) metadata will not // have been modified and will be in a clean empty state. We continue on and // return an empty database to hopefully recover. This means that existing // crash reports have been orphaned. metadata->Read(); return metadata; } void Metadata::AddNewRecord(const ReportDisk& new_report_disk) { DCHECK(new_report_disk.state == ReportState::kPending); reports_.push_back(new_report_disk); dirty_ = true; } OperationStatus Metadata::FindReports( ReportState desired_state, std::vector* reports) const { DCHECK(reports->empty()); for (const auto& report : reports_) { if (report.state == desired_state && VerifyReport(report, desired_state) == CrashReportDatabase::kNoError) { reports->push_back(report); } } return CrashReportDatabase::kNoError; } OperationStatus Metadata::FindSingleReport( const UUID& uuid, const ReportDisk** out_report) const { auto report_iter = std::find_if( reports_.begin(), reports_.end(), [uuid](const ReportDisk& report) { return report.uuid == uuid; }); if (report_iter == reports_.end()) return CrashReportDatabase::kReportNotFound; OperationStatus os = VerifyReportAnyState(*report_iter); if (os == CrashReportDatabase::kNoError) *out_report = &*report_iter; return os; } OperationStatus Metadata::FindSingleReportAndMarkDirty( const UUID& uuid, ReportState desired_state, ReportDisk** report_disk) { auto report_iter = std::find_if( reports_.begin(), reports_.end(), [uuid](const ReportDisk& report) { return report.uuid == uuid; }); if (report_iter == reports_.end()) return CrashReportDatabase::kReportNotFound; OperationStatus os = VerifyReport(*report_iter, desired_state); if (os == CrashReportDatabase::kNoError) { dirty_ = true; *report_disk = &*report_iter; } return os; } OperationStatus Metadata::DeleteReport(const UUID& uuid, base::FilePath* report_path) { auto report_iter = std::find_if( reports_.begin(), reports_.end(), [uuid](const ReportDisk& report) { return report.uuid == uuid; }); if (report_iter == reports_.end()) return CrashReportDatabase::kReportNotFound; *report_path = report_iter->file_path; reports_.erase(report_iter); dirty_ = true; return CrashReportDatabase::kNoError; } int Metadata::CleanDatabase() { int removed = 0; for (auto report_iter = reports_.begin(); report_iter != reports_.end();) { if (!IsRegularFile(report_iter->file_path)) { report_iter = reports_.erase(report_iter); ++removed; dirty_ = true; } else { ++report_iter; } } return removed; } Metadata::Metadata(FileHandle handle, const base::FilePath& report_dir) : handle_(handle), report_dir_(report_dir), dirty_(false), reports_() { } bool Metadata::Rewind() { FileOffset result = LoggingSeekFile(handle_.get(), 0, SEEK_SET); DCHECK_EQ(result, 0); return result == 0; } void Metadata::Read() { FileOffset length = LoggingSeekFile(handle_.get(), 0, SEEK_END); if (length <= 0) // Failed, or empty: Abort. return; if (!Rewind()) { LOG(ERROR) << "failed to rewind to read"; return; } MetadataFileHeader header; if (!LoggingReadFileExactly(handle_.get(), &header, sizeof(header))) { LOG(ERROR) << "failed to read header"; return; } if (header.magic != kMetadataFileHeaderMagic || header.version != kMetadataFileVersion) { LOG(ERROR) << "unexpected header"; return; } base::CheckedNumeric records_size = base::CheckedNumeric(header.num_records) * static_cast(sizeof(MetadataFileReportRecord)); if (!records_size.IsValid()) { LOG(ERROR) << "record size out of range"; return; } std::vector reports; if (header.num_records > 0) { std::vector records(header.num_records); if (!LoggingReadFileExactly( handle_.get(), &records[0], records_size.ValueOrDie())) { LOG(ERROR) << "failed to read records"; return; } std::string string_table = ReadRestOfFileAsString(handle_.get()); if (string_table.empty() || string_table.back() != '\0') { LOG(ERROR) << "bad string table"; return; } for (const auto& record : records) { if (record.file_path_index >= string_table.size() || record.id_index >= string_table.size()) { LOG(ERROR) << "invalid string table index"; return; } ReportDisk report_disk(record, report_dir_, string_table); // There are no attachments on Windows so the total size is the main // report size. struct _stati64 statbuf; if (_wstat64(report_disk.file_path.value().c_str(), &statbuf) == 0) { report_disk.total_size = statbuf.st_size; } else { LOG(ERROR) << "failed to stat report"; } reports.push_back(report_disk); } } reports_.swap(reports); } void Metadata::Write() { if (!Rewind()) { LOG(ERROR) << "failed to rewind to write"; return; } // Truncate to ensure that a partial write doesn't cause a mix of old and new // data causing an incorrect interpretation on read. if (!SetEndOfFile(handle_.get())) { PLOG(ERROR) << "failed to truncate"; return; } size_t num_records = reports_.size(); // Fill and write out the header. MetadataFileHeader header = {0}; header.magic = kMetadataFileHeaderMagic; header.version = kMetadataFileVersion; header.num_records = base::checked_cast(num_records); if (!LoggingWriteFile(handle_.get(), &header, sizeof(header))) { LOG(ERROR) << "failed to write header"; return; } if (num_records == 0) return; // Build the records and string table we're going to write. std::string string_table; std::vector records; records.reserve(num_records); for (const auto& report : reports_) { const base::FilePath& path = report.file_path; if (path.DirName() != report_dir_) { LOG(ERROR) << path.value().c_str() << " expected to start with " << base::UTF16ToUTF8(report_dir_.value()); return; } records.push_back(MetadataFileReportRecord(report, &string_table)); } if (!LoggingWriteFile(handle_.get(), &records[0], records.size() * sizeof(MetadataFileReportRecord))) { LOG(ERROR) << "failed to write records"; return; } if (!LoggingWriteFile( handle_.get(), string_table.c_str(), string_table.size())) { LOG(ERROR) << "failed to write string table"; return; } } // static OperationStatus Metadata::VerifyReportAnyState(const ReportDisk& report_disk) { DWORD fileattr = GetFileAttributes(report_disk.file_path.value().c_str()); if (fileattr == INVALID_FILE_ATTRIBUTES) return CrashReportDatabase::kReportNotFound; return (fileattr & FILE_ATTRIBUTE_DIRECTORY) ? CrashReportDatabase::kFileSystemError : CrashReportDatabase::kNoError; } // static OperationStatus Metadata::VerifyReport(const ReportDisk& report_disk, ReportState desired_state) { if (report_disk.state == desired_state) { return VerifyReportAnyState(report_disk); } return report_disk.state == ReportState::kUploading ? CrashReportDatabase::kBusyError : CrashReportDatabase::kReportNotFound; } bool EnsureDirectory(const base::FilePath& path) { DWORD fileattr = GetFileAttributes(path.value().c_str()); if (fileattr == INVALID_FILE_ATTRIBUTES) { PLOG(ERROR) << "GetFileAttributes " << base::UTF16ToUTF8(path.value()); return false; } if ((fileattr & FILE_ATTRIBUTE_DIRECTORY) == 0) { LOG(ERROR) << "GetFileAttributes " << base::UTF16ToUTF8(path.value()) << ": not a directory"; return false; } return true; } //! \brief Ensures that the node at path is a directory, and creates it if it //! does not exist. //! //! \return If the path points to a file, rather than a directory, or the //! directory could not be created, returns `false`. Otherwise, returns //! `true`, indicating that path already was or now is a directory. bool CreateDirectoryIfNecessary(const base::FilePath& path) { if (CreateDirectory(path.value().c_str(), nullptr)) return true; if (GetLastError() != ERROR_ALREADY_EXISTS) { PLOG(ERROR) << "CreateDirectory " << base::UTF16ToUTF8(path.value()); return false; } return EnsureDirectory(path); } } // namespace // CrashReportDatabaseWin ------------------------------------------------------ class CrashReportDatabaseWin : public CrashReportDatabase { public: explicit CrashReportDatabaseWin(const base::FilePath& path); ~CrashReportDatabaseWin() override; bool Initialize(bool may_create); // CrashReportDatabase: Settings* GetSettings() override; OperationStatus PrepareNewCrashReport( std::unique_ptr* report) override; OperationStatus FinishedWritingCrashReport(std::unique_ptr report, UUID* uuid) override; OperationStatus LookUpCrashReport(const UUID& uuid, Report* report) override; OperationStatus GetPendingReports(std::vector* reports) override; OperationStatus GetCompletedReports(std::vector* reports) override; OperationStatus GetReportForUploading( const UUID& uuid, std::unique_ptr* report, bool report_metrics) override; OperationStatus SkipReportUpload(const UUID& uuid, Metrics::CrashSkippedReason reason) override; OperationStatus DeleteReport(const UUID& uuid) override; OperationStatus RequestUpload(const UUID& uuid) override; int CleanDatabase(time_t lockfile_ttl) override; // Build a filepath for the root attachments directory. base::FilePath AttachmentsRootPath(); // Build a filepath for the directory for the report to hold attachments. base::FilePath AttachmentsPath(const UUID& uuid); private: // CrashReportDatabase: OperationStatus RecordUploadAttempt(UploadReport* report, bool successful, const std::string& id) override; // Cleans any attachments that have no associated report. void CleanOrphanedAttachments(); // Attempt to remove any attachments associated with the given report UUID. // There may not be any, so failing is not an error. void RemoveAttachmentsByUUID(const UUID& uuid); std::unique_ptr AcquireMetadata(); base::FilePath base_dir_; Settings settings_; InitializationStateDcheck initialized_; DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseWin); }; base::FilePath CrashReportDatabaseWin::AttachmentsRootPath() { return base_dir_.Append(kAttachmentsDirectory); } base::FilePath CrashReportDatabaseWin::AttachmentsPath(const UUID& uuid) { const std::wstring uuid_string = uuid.ToString16(); return base_dir_.Append(kAttachmentsDirectory).Append(uuid_string); } FileWriter* CrashReportDatabase::NewReport::AddAttachment( const std::string& name) { auto database_win = static_cast(database_); base::FilePath attachments_root_dir = database_win->AttachmentsRootPath(); base::FilePath attachments_dir = database_win->AttachmentsPath(uuid_); if (!LoggingCreateDirectory( attachments_root_dir, FilePermissions::kOwnerOnly, true) || !LoggingCreateDirectory( attachments_dir, FilePermissions::kOwnerOnly, true)) { return nullptr; } base::FilePath path = attachments_dir.Append(base::UTF8ToUTF16(name)); auto writer = std::make_unique(); if (!writer->Open( path, FileWriteMode::kCreateOrFail, FilePermissions::kOwnerOnly)) { LOG(ERROR) << "could not open " << path.value().c_str(); return nullptr; } attachment_writers_.emplace_back(std::move(writer)); attachment_removers_.emplace_back(ScopedRemoveFile(path)); return attachment_writers_.back().get(); } void CrashReportDatabase::UploadReport::InitializeAttachments() { base::FilePath attachments_dir = static_cast(database_)->AttachmentsPath(uuid); if (!IsDirectory(attachments_dir, /*allow_symlinks=*/false)) { return; } DirectoryReader dir_reader; if (!dir_reader.Open(attachments_dir)) { return; } base::FilePath filename; DirectoryReader::Result dir_result; while ((dir_result = dir_reader.NextFile(&filename)) == DirectoryReader::Result::kSuccess) { const base::FilePath filepath(attachments_dir.Append(filename)); std::unique_ptr file_reader(std::make_unique()); if (!file_reader->Open(filepath)) { LOG(ERROR) << "attachment " << filepath.value().c_str() << " couldn't be opened, skipping"; continue; } attachment_readers_.emplace_back(std::move(file_reader)); attachment_map_[base::UTF16ToUTF8(filename.value())] = attachment_readers_.back().get(); } } CrashReportDatabaseWin::CrashReportDatabaseWin(const base::FilePath& path) : CrashReportDatabase(), base_dir_(path), settings_(), initialized_() {} CrashReportDatabaseWin::~CrashReportDatabaseWin() { } bool CrashReportDatabaseWin::Initialize(bool may_create) { INITIALIZATION_STATE_SET_INITIALIZING(initialized_); // Ensure the database directory exists. if (may_create) { if (!CreateDirectoryIfNecessary(base_dir_)) return false; } else if (!EnsureDirectory(base_dir_)) { return false; } // Ensure that the report subdirectory exists. if (!CreateDirectoryIfNecessary(base_dir_.Append(kReportsDirectory))) return false; if (!settings_.Initialize(base_dir_.Append(kSettings))) return false; INITIALIZATION_STATE_SET_VALID(initialized_); return true; } Settings* CrashReportDatabaseWin::GetSettings() { INITIALIZATION_STATE_DCHECK_VALID(initialized_); return &settings_; } OperationStatus CrashReportDatabaseWin::PrepareNewCrashReport( std::unique_ptr* report) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr new_report(new NewReport()); if (!new_report->Initialize(this, base_dir_.Append(kReportsDirectory), std::wstring(L".") + kCrashReportFileExtension)) { return kFileSystemError; } report->reset(new_report.release()); return kNoError; } OperationStatus CrashReportDatabaseWin::FinishedWritingCrashReport( std::unique_ptr report, UUID* uuid) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; metadata->AddNewRecord(ReportDisk(report->ReportID(), report->file_remover_.get(), time(nullptr), ReportState::kPending)); ignore_result(report->file_remover_.release()); // Close all the attachments and disarm their removers too. for (auto& writer : report->attachment_writers_) { writer->Close(); } for (auto& remover : report->attachment_removers_) { ignore_result(remover.release()); } *uuid = report->ReportID(); Metrics::CrashReportPending(Metrics::PendingReportReason::kNewlyCreated); Metrics::CrashReportSize(report->Writer()->Seek(0, SEEK_END)); return kNoError; } OperationStatus CrashReportDatabaseWin::LookUpCrashReport(const UUID& uuid, Report* report) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; // Find and return a copy of the matching report. const ReportDisk* report_disk; OperationStatus os = metadata->FindSingleReport(uuid, &report_disk); if (os == kNoError) *report = *report_disk; return os; } OperationStatus CrashReportDatabaseWin::GetPendingReports( std::vector* reports) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); return metadata ? metadata->FindReports(ReportState::kPending, reports) : kDatabaseError; } OperationStatus CrashReportDatabaseWin::GetCompletedReports( std::vector* reports) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); return metadata ? metadata->FindReports(ReportState::kCompleted, reports) : kDatabaseError; } OperationStatus CrashReportDatabaseWin::GetReportForUploading( const UUID& uuid, std::unique_ptr* report, bool report_metrics) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; ReportDisk* report_disk; OperationStatus os = metadata->FindSingleReportAndMarkDirty( uuid, ReportState::kPending, &report_disk); if (os == kNoError) { report_disk->state = ReportState::kUploading; auto upload_report = std::make_unique(); *implicit_cast(upload_report.get()) = *report_disk; if (!upload_report->Initialize(upload_report->file_path, this)) { return kFileSystemError; } upload_report->report_metrics_ = report_metrics; report->reset(upload_report.release()); } return os; } OperationStatus CrashReportDatabaseWin::RecordUploadAttempt( UploadReport* report, bool successful, const std::string& id) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); if (report->report_metrics_) { Metrics::CrashUploadAttempted(successful); } std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; ReportDisk* report_disk; OperationStatus os = metadata->FindSingleReportAndMarkDirty( report->uuid, ReportState::kUploading, &report_disk); if (os != kNoError) return os; time_t now = time(nullptr); report_disk->uploaded = successful; report_disk->id = id; report_disk->last_upload_attempt_time = now; report_disk->upload_attempts++; if (successful) { report_disk->state = ReportState::kCompleted; report_disk->upload_explicitly_requested = false; } else { report_disk->state = ReportState::kPending; report_disk->upload_explicitly_requested = report->upload_explicitly_requested; } if (!settings_.SetLastUploadAttemptTime(now)) return kDatabaseError; return kNoError; } OperationStatus CrashReportDatabaseWin::DeleteReport(const UUID& uuid) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; base::FilePath report_path; OperationStatus os = metadata->DeleteReport(uuid, &report_path); if (os != kNoError) return os; if (!DeleteFile(report_path.value().c_str())) { PLOG(ERROR) << "DeleteFile " << base::UTF16ToUTF8(report_path.value()); return kFileSystemError; } RemoveAttachmentsByUUID(uuid); return kNoError; } void CrashReportDatabaseWin::RemoveAttachmentsByUUID(const UUID& uuid) { base::FilePath attachments_dir = AttachmentsPath(uuid); if (!IsDirectory(attachments_dir, /*allow_symlinks=*/false)) { return; } DirectoryReader reader; if (!reader.Open(attachments_dir)) { return; } base::FilePath filename; DirectoryReader::Result result; while ((result = reader.NextFile(&filename)) == DirectoryReader::Result::kSuccess) { const base::FilePath filepath(attachments_dir.Append(filename)); LoggingRemoveFile(filepath); } LoggingRemoveDirectory(attachments_dir); } OperationStatus CrashReportDatabaseWin::SkipReportUpload( const UUID& uuid, Metrics::CrashSkippedReason reason) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); Metrics::CrashUploadSkipped(reason); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; ReportDisk* report_disk; OperationStatus os = metadata->FindSingleReportAndMarkDirty( uuid, ReportState::kPending, &report_disk); if (os == kNoError) { report_disk->state = ReportState::kCompleted; report_disk->upload_explicitly_requested = false; } return os; } std::unique_ptr CrashReportDatabaseWin::AcquireMetadata() { base::FilePath metadata_file = base_dir_.Append(kMetadataFileName); return Metadata::Create(metadata_file, base_dir_.Append(kReportsDirectory)); } std::unique_ptr InitializeInternal( const base::FilePath& path, bool may_create) { std::unique_ptr database_win( new CrashReportDatabaseWin(path)); return database_win->Initialize(may_create) ? std::move(database_win) : std::unique_ptr(); } OperationStatus CrashReportDatabaseWin::RequestUpload(const UUID& uuid) { INITIALIZATION_STATE_DCHECK_VALID(initialized_); std::unique_ptr metadata(AcquireMetadata()); if (!metadata) return kDatabaseError; ReportDisk* report_disk; // TODO(gayane): Search for the report only once regardless of its state. OperationStatus os = metadata->FindSingleReportAndMarkDirty( uuid, ReportState::kCompleted, &report_disk); if (os == kReportNotFound) { os = metadata->FindSingleReportAndMarkDirty( uuid, ReportState::kPending, &report_disk); } if (os != kNoError) return os; // If the crash report has already been uploaded, don't request new upload. if (report_disk->uploaded) return kCannotRequestUpload; // Mark the crash report as having upload explicitly requested by the user, // and move it to the pending state. report_disk->upload_explicitly_requested = true; report_disk->state = ReportState::kPending; Metrics::CrashReportPending(Metrics::PendingReportReason::kUserInitiated); return kNoError; } int CrashReportDatabaseWin::CleanDatabase(time_t lockfile_ttl) { int removed = 0; const base::FilePath dir_path(base_dir_.Append(kReportsDirectory)); DirectoryReader reader; if (!reader.Open(dir_path)) { return removed; } base::FilePath filename; DirectoryReader::Result result; time_t now = time(nullptr); std::unique_ptr metadata(AcquireMetadata()); // Remove old reports without metadata. while ((result = reader.NextFile(&filename)) == DirectoryReader::Result::kSuccess) { timespec filetime; const base::FilePath report_path(dir_path.Append(filename)); if (!FileModificationTime(report_path, &filetime) || filetime.tv_sec > now - lockfile_ttl) { continue; } const ReportDisk* report_disk; UUID uuid; bool is_uuid = UUIDFromReportPath(report_path, &uuid); // ignore files whose base name is not uuid if (!is_uuid) { continue; } OperationStatus os = metadata->FindSingleReport(uuid, &report_disk); if (os == OperationStatus::kReportNotFound) { if (LoggingRemoveFile(report_path)) { ++removed; RemoveAttachmentsByUUID(uuid); } continue; } } // Remove any metadata records without report files. removed += metadata->CleanDatabase(); CleanOrphanedAttachments(); return removed; } void CrashReportDatabaseWin::CleanOrphanedAttachments() { base::FilePath root_attachments_dir(base_dir_.Append(kAttachmentsDirectory)); DirectoryReader reader; if (!reader.Open(root_attachments_dir)) { return; } base::FilePath filename; DirectoryReader::Result result; base::FilePath reports_dir = base_dir_.Append(kReportsDirectory); while ((result = reader.NextFile(&filename)) == DirectoryReader::Result::kSuccess) { const base::FilePath path(root_attachments_dir.Append(filename)); if (IsDirectory(path, false)) { UUID uuid; if (!uuid.InitializeFromString(filename.value())) { LOG(ERROR) << "unexpected attachment dir name " << filename.value().c_str(); continue; } // Remove attachments if corresponding report doen't exist. base::FilePath report_path = reports_dir.Append(uuid.ToString16() + kCrashReportFileExtension); if (!IsRegularFile(report_path)) { RemoveAttachmentsByUUID(uuid); } } } } // static std::unique_ptr CrashReportDatabase::Initialize( const base::FilePath& path) { return InitializeInternal(path, true); } // static std::unique_ptr CrashReportDatabase::InitializeWithoutCreating(const base::FilePath& path) { return InitializeInternal(path, false); } } // namespace crashpad