crashpad/client/crash_report_database_win.cc

1120 lines
37 KiB
C++
Raw Normal View History

// 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 <windows.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <wchar.h>
#include <utility>
#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<uint32_t>(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<size_t>(end - read_from);
std::string buffer(data_length, '\0');
Make file_io reads more rational and predictable ReadFile() attempted to continue reading after a short read. In most cases, this is fine. However, ReadFile() would keep trying to fill a partially-filled buffer until experiencing a 0-length read(), signaling end-of-file. For certain weird file descriptors like terminal input, EOF is an ephemeral condition, and attempting to read beyond EOF doesn’t actually return 0 (EOF) provided that they remain open, it will block waiting for more input. Consequently, ReadFile() and anything based on ReadFile() had an undocumented and quirky interface, which was that any short read that it returned (not an underlying short read) actually indicated EOF. This facet of ReadFile() was unexpected, so it’s being removed. The new behavior is that ReadFile() will return an underlying short read. The behavior of FileReaderInterface::Read() is updated in accordance with this change. Upon experiencing a short read, the caller can determine the best action. Most callers were already prepared for this behavior. Outside of util/file, only crashpad_database_util properly implemented EOF detection according to previous semantics, and adapting it to new semantics is trivial. Callers who require an exact-length read can use the new ReadFileExactly(), or the newly renamed LoggingReadFileExactly() or CheckedReadFileExactly(). These functions will retry following a short read. The renamed functions were previously called LoggingReadFile() and CheckedReadFile(), but those names implied that they were simply wrapping ReadFile(), which is not the case. They wrapped ReadFile() and further, insisted on a full read. Since ReadFile()’s semantics are now changing but these functions’ are not, they’re now even more distinct from ReadFile(), and must be renamed to avoid confusion. Test: * Change-Id: I06b77e0d6ad8719bd2eb67dab93a8740542dd908 Reviewed-on: https://chromium-review.googlesource.com/456676 Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-03-16 13:36:38 -04:00
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<uint32_t>(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<ReportState>(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<Metadata> 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<CrashReportDatabase::Report>* 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<ReportDisk> 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> 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<Metadata>();
// 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<Metadata>();
}
std::unique_ptr<Metadata> 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<CrashReportDatabase::Report>* 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;
Make file_io reads more rational and predictable ReadFile() attempted to continue reading after a short read. In most cases, this is fine. However, ReadFile() would keep trying to fill a partially-filled buffer until experiencing a 0-length read(), signaling end-of-file. For certain weird file descriptors like terminal input, EOF is an ephemeral condition, and attempting to read beyond EOF doesn’t actually return 0 (EOF) provided that they remain open, it will block waiting for more input. Consequently, ReadFile() and anything based on ReadFile() had an undocumented and quirky interface, which was that any short read that it returned (not an underlying short read) actually indicated EOF. This facet of ReadFile() was unexpected, so it’s being removed. The new behavior is that ReadFile() will return an underlying short read. The behavior of FileReaderInterface::Read() is updated in accordance with this change. Upon experiencing a short read, the caller can determine the best action. Most callers were already prepared for this behavior. Outside of util/file, only crashpad_database_util properly implemented EOF detection according to previous semantics, and adapting it to new semantics is trivial. Callers who require an exact-length read can use the new ReadFileExactly(), or the newly renamed LoggingReadFileExactly() or CheckedReadFileExactly(). These functions will retry following a short read. The renamed functions were previously called LoggingReadFile() and CheckedReadFile(), but those names implied that they were simply wrapping ReadFile(), which is not the case. They wrapped ReadFile() and further, insisted on a full read. Since ReadFile()’s semantics are now changing but these functions’ are not, they’re now even more distinct from ReadFile(), and must be renamed to avoid confusion. Test: * Change-Id: I06b77e0d6ad8719bd2eb67dab93a8740542dd908 Reviewed-on: https://chromium-review.googlesource.com/456676 Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-03-16 13:36:38 -04:00
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<uint32_t> records_size =
base::CheckedNumeric<uint32_t>(header.num_records) *
static_cast<uint32_t>(sizeof(MetadataFileReportRecord));
if (!records_size.IsValid()) {
LOG(ERROR) << "record size out of range";
return;
}
std::vector<ReportDisk> reports;
if (header.num_records > 0) {
std::vector<MetadataFileReportRecord> records(header.num_records);
Make file_io reads more rational and predictable ReadFile() attempted to continue reading after a short read. In most cases, this is fine. However, ReadFile() would keep trying to fill a partially-filled buffer until experiencing a 0-length read(), signaling end-of-file. For certain weird file descriptors like terminal input, EOF is an ephemeral condition, and attempting to read beyond EOF doesn’t actually return 0 (EOF) provided that they remain open, it will block waiting for more input. Consequently, ReadFile() and anything based on ReadFile() had an undocumented and quirky interface, which was that any short read that it returned (not an underlying short read) actually indicated EOF. This facet of ReadFile() was unexpected, so it’s being removed. The new behavior is that ReadFile() will return an underlying short read. The behavior of FileReaderInterface::Read() is updated in accordance with this change. Upon experiencing a short read, the caller can determine the best action. Most callers were already prepared for this behavior. Outside of util/file, only crashpad_database_util properly implemented EOF detection according to previous semantics, and adapting it to new semantics is trivial. Callers who require an exact-length read can use the new ReadFileExactly(), or the newly renamed LoggingReadFileExactly() or CheckedReadFileExactly(). These functions will retry following a short read. The renamed functions were previously called LoggingReadFile() and CheckedReadFile(), but those names implied that they were simply wrapping ReadFile(), which is not the case. They wrapped ReadFile() and further, insisted on a full read. Since ReadFile()’s semantics are now changing but these functions’ are not, they’re now even more distinct from ReadFile(), and must be renamed to avoid confusion. Test: * Change-Id: I06b77e0d6ad8719bd2eb67dab93a8740542dd908 Reviewed-on: https://chromium-review.googlesource.com/456676 Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-03-16 13:36:38 -04:00
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<uint32_t>(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<MetadataFileReportRecord> 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<NewReport>* report) override;
OperationStatus FinishedWritingCrashReport(std::unique_ptr<NewReport> report,
UUID* uuid) override;
OperationStatus LookUpCrashReport(const UUID& uuid, Report* report) override;
OperationStatus GetPendingReports(std::vector<Report>* reports) override;
OperationStatus GetCompletedReports(std::vector<Report>* reports) override;
OperationStatus GetReportForUploading(
const UUID& uuid,
std::unique_ptr<const UploadReport>* 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<Metadata> 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<CrashReportDatabaseWin*>(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<FileWriter>();
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<CrashReportDatabaseWin*>(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<FileReader> file_reader(std::make_unique<FileReader>());
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<NewReport>* report) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<NewReport> 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<NewReport> report,
UUID* uuid) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<Metadata> 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> 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<Report>* reports) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<Metadata> metadata(AcquireMetadata());
return metadata ? metadata->FindReports(ReportState::kPending, reports)
: kDatabaseError;
}
OperationStatus CrashReportDatabaseWin::GetCompletedReports(
std::vector<Report>* reports) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<Metadata> metadata(AcquireMetadata());
return metadata ? metadata->FindReports(ReportState::kCompleted, reports)
: kDatabaseError;
}
OperationStatus CrashReportDatabaseWin::GetReportForUploading(
const UUID& uuid,
std::unique_ptr<const UploadReport>* report,
bool report_metrics) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<Metadata> 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<UploadReport>();
*implicit_cast<Report*>(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> 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> 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> 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<Metadata> CrashReportDatabaseWin::AcquireMetadata() {
base::FilePath metadata_file = base_dir_.Append(kMetadataFileName);
return Metadata::Create(metadata_file, base_dir_.Append(kReportsDirectory));
}
std::unique_ptr<CrashReportDatabase> InitializeInternal(
const base::FilePath& path,
bool may_create) {
std::unique_ptr<CrashReportDatabaseWin> database_win(
new CrashReportDatabaseWin(path));
return database_win->Initialize(may_create)
? std::move(database_win)
: std::unique_ptr<CrashReportDatabaseWin>();
}
OperationStatus CrashReportDatabaseWin::RequestUpload(const UUID& uuid) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
std::unique_ptr<Metadata> 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> 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> CrashReportDatabase::Initialize(
const base::FilePath& path) {
return InitializeInternal(path, true);
}
// static
std::unique_ptr<CrashReportDatabase>
CrashReportDatabase::InitializeWithoutCreating(const base::FilePath& path) {
return InitializeInternal(path, false);
}
} // namespace crashpad