blob: 2839ffeb478276289039c64fce994c7b4fd259d3 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/base/crash/crashpad_database_manager.h"
#include <stdlib.h>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/time_formatting.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "remoting/base/file_path_util_linux.h"
#include "third_party/crashpad/crashpad/client/crash_report_database.h"
#include "third_party/crashpad/crashpad/client/settings.h"
#if BUILDFLAG(IS_WIN)
#include "base/base_paths.h"
#include "base/strings/utf_string_conversions.h"
#endif // BUILDFLAG(IS_WIN)
namespace {
const base::FilePath::CharType kChromotingCrashpadDatabasePath[] =
FILE_PATH_LITERAL("crashpad");
// Maximum number of crash reports to log. Reports are sorted by timestamp so
// the most recent N reports will be logged.
const size_t kMaxReportsToLog = 2;
// Maximum number of crash reports to retain in the database. When the database
// contains more than this number, the oldest ones will be deleted, regardless
// of |kMaxReportDays|.
const size_t kMaxReportsToRetain = 20;
// Maximum number of days to keep reports around in the local database.
const size_t kMaxReportAgeDays = 7;
} // namespace
namespace remoting {
base::FilePath GetCrashpadDatabasePath() {
base::FilePath database_path;
#if BUILDFLAG(IS_WIN)
base::PathService::Get(base::BasePathKey::DIR_ASSETS, &database_path);
#else
database_path = GetConfigDirectoryPath();
#endif
return database_path.Append(kChromotingCrashpadDatabasePath);
}
CrashpadDatabaseManager::CrashpadDatabaseManager(Logger& logger)
: logger_(logger) {}
CrashpadDatabaseManager::~CrashpadDatabaseManager() = default;
bool CrashpadDatabaseManager::InitializeCrashpadDatabase() {
base::FilePath database_path = GetCrashpadDatabasePath();
base::File::Error error;
if (!base::CreateDirectoryAndGetError(database_path, &error)) {
#if BUILDFLAG(IS_WIN)
logger_->LogError("Unable to get directory for crash database: " +
base::WideToUTF8(database_path.value()));
#else
logger_->LogError("Unable to get directory for crash database: " +
database_path.value());
#endif
logger_->LogError("File Error: " + base::File::ErrorToString(error));
return false;
}
database_ = crashpad::CrashReportDatabase::Initialize(database_path);
if (!database_) {
logger_->LogError("Unable to initialize crash database");
return false;
}
// Load and sort the completed crash reports.
if (!LoadCompletedReports()) {
return false;
}
if (!LoadPendingReports()) {
return false;
}
return true;
}
bool CrashpadDatabaseManager::EnableReportUploads() {
if (!database_ || !database_->GetSettings()->SetUploadsEnabled(true)) {
logger_->LogError("Unable to enable Crashpad report uploads");
return false;
}
return true;
}
void CrashpadDatabaseManager::LogCompletedCrashpadReports() {
if (!database_) {
logger_->LogError("Crashpad database has not been initialized");
return;
}
LogCrashReports(completed_reports_, "Completed");
}
void CrashpadDatabaseManager::LogPendingCrashpadReports() {
if (!database_) {
logger_->LogError("Crashpad database has not been initialized");
return;
}
LogCrashReports(pending_reports_, "Pending");
}
bool CrashpadDatabaseManager::CleanupCompletedCrashpadReports() {
if (!database_) {
logger_->LogError("Crashpad database has not been initialized");
return false;
}
CleanupCrashReports(completed_reports_);
// Reload the completed reports since we may have deleted some entries.
return LoadCompletedReports();
}
// private
bool CrashpadDatabaseManager::LoadCompletedReports() {
crashpad::CrashReportDatabase::OperationStatus status =
database_->GetCompletedReports(&completed_reports_);
if (status != crashpad::CrashReportDatabase::OperationStatus::kNoError) {
logger_->LogError("Unable to read completed crash reports: " +
base::NumberToString(status));
return false;
}
SortCrashReports(completed_reports_);
return true;
}
bool CrashpadDatabaseManager::LoadPendingReports() {
crashpad::CrashReportDatabase::OperationStatus status =
database_->GetPendingReports(&pending_reports_);
if (status != crashpad::CrashReportDatabase::OperationStatus::kNoError) {
logger_->LogError("Unable to read pending crash reports: " +
base::NumberToString(status));
return false;
}
SortCrashReports(pending_reports_);
return true;
}
void CrashpadDatabaseManager::SortCrashReports(
std::vector<crashpad::CrashReportDatabase::Report>& reports) {
std::sort(reports.begin(), reports.end(),
[](crashpad::CrashReportDatabase::Report const& a,
crashpad::CrashReportDatabase::Report const& b) {
return a.creation_time > b.creation_time;
});
}
void CrashpadDatabaseManager::LogCrashReportInfo(
const crashpad::CrashReportDatabase::Report& report) {
const std::string& id = report.id;
// |id| will only be assigned if the report has been successfully uploaded.
if (id.empty()) {
logger_->Log(" Crash id: <unassigned>");
} else {
logger_->Log(" Crash id: " + id + " (http://go/crash/" + id + ")");
}
#if BUILDFLAG(IS_WIN)
logger_->Log(" path: " + base::WideToUTF8(report.file_path.value()));
#else
logger_->Log(" path: " + report.file_path.value());
#endif
logger_->Log(" uuid: " + report.uuid.ToString());
logger_->Log(" created: " +
TimeFormatHTTP(base::Time::FromTimeT(report.creation_time)));
std::string uploaded = report.uploaded ? "yes" : "no";
logger_->Log(" uploaded: " + uploaded + " (attempts: " +
base::NumberToString(report.upload_attempts) + ")");
}
void CrashpadDatabaseManager::LogCrashReports(
std::vector<crashpad::CrashReportDatabase::Report>& reports,
std::string_view report_type) {
size_t num_reports = reports.size();
if (num_reports > kMaxReportsToLog) {
logger_->Log("Recent " + std::string(report_type) + " crash reports: " +
base::NumberToString(num_reports) + " (most recent " +
base::NumberToString(kMaxReportsToLog) + " shown)");
} else {
logger_->Log("Recent " + std::string(report_type) +
" crash reports: " + base::NumberToString(num_reports));
}
for (size_t i = 0; i < num_reports && i < kMaxReportsToLog; ++i) {
LogCrashReportInfo(reports[i]);
}
}
bool CrashpadDatabaseManager::CleanupCrashReports(
std::vector<crashpad::CrashReportDatabase::Report>& reports) {
if (!database_) {
return false;
}
// Cleanup the oldest reports if we have too many in the database.
size_t num_reports = reports.size();
if (num_reports > kMaxReportsToRetain) {
logger_->Log("Too many crash reports in database. Retaining most recent " +
base::NumberToString(kMaxReportsToRetain));
for (size_t i = kMaxReportsToRetain; i < num_reports; ++i) {
const auto& report = reports[i];
std::string creation_time =
base::TimeFormatHTTP(base::Time::FromTimeT(report.creation_time));
logger_->Log(" Deleting crash report: " + report.id + " (" +
report.uuid.ToString() + ") " + creation_time);
auto status = database_->DeleteReport(report.uuid);
if (status != crashpad::CrashReportDatabase::OperationStatus::kNoError) {
logger_->LogError(
" Unable to delete crash report: " + base::NumberToString(status) +
" " + report.id + " (" + report.uuid.ToString() + ")");
return false;
}
}
reports.resize(kMaxReportsToRetain);
}
base::Time now = base::Time::Now();
base::Time threshold = now - base::Days(kMaxReportAgeDays);
// Cleanup old uploaded reports.
bool header_shown = false;
for (const auto& report : reports) {
base::Time created = base::Time::FromTimeT(report.creation_time);
if (report.uploaded && created < threshold) {
if (!header_shown) {
header_shown = true;
logger_->Log("Deleting uploaded crash reports older than " +
base::NumberToString(kMaxReportAgeDays) + " days:");
}
logger_->Log(" Deleting crash report: " + report.id + " (" +
base::TimeFormatHTTP(created) + ")");
auto status = database_->DeleteReport(report.uuid);
if (status != crashpad::CrashReportDatabase::OperationStatus::kNoError) {
logger_->LogError(" Unable to delete uploaded crash report: " +
base::NumberToString(status) + " " + report.id +
" (" + report.uuid.ToString() + ")");
return false;
}
}
}
// Cleanup old reports that haven't been uploaded.
header_shown = false;
for (const auto& report : reports) {
base::Time created = base::Time::FromTimeT(report.creation_time);
if (!report.uploaded && created < threshold) {
if (!header_shown) {
header_shown = true;
logger_->Log("Deleting crash reports older than " +
base::NumberToString(kMaxReportAgeDays) + " days:");
}
// We need to log |uuid| here because only uploaded reports have an |id|.
logger_->Log(" Deleting crash report: " + report.uuid.ToString() + " (" +
TimeFormatHTTP(created) + ")");
auto status = database_->DeleteReport(report.uuid);
if (status != crashpad::CrashReportDatabase::OperationStatus::kNoError) {
logger_->LogError(" Unable to delete old crash report: " +
base::NumberToString(status) + " (" +
report.uuid.ToString() + ")");
return false;
}
}
}
return true;
}
} // namespace remoting