// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/crash/linux/synchronized_minidump_manager.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/files/dir_reader_posix.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "chromecast/base/path_utils.h"
#include "chromecast/crash/linux/dump_info.h"
// if |cond| is false, returns |retval|.
#define RCHECK(cond, retval) \
do { \
if (!(cond)) { \
return (retval); \
} \
} while (0)
namespace chromecast {
namespace {
// Allows overriding default placement of minidumps in $HOME.
const char kMinidumpPathSwitch[] = "minidump-path";
const char kLockfileName[] = "lockfile";
const char kMetadataName[] = "metadata";
const char kMinidumpsDir[] = "minidumps";
const char kLockfileRatelimitKey[] = "ratelimit";
const char kLockfileRatelimitPeriodStartKey[] = "period_start";
const char kLockfileRatelimitPeriodDumpsKey[] = "period_dumps";
const uint64_t kLockfileNumRatelimitParams = 2;
base::FilePath GetMinidumpPath() {
base::FilePath result =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
kMinidumpPathSwitch);
if (result.empty()) {
result = GetHomePathASCII(kMinidumpsDir);
}
return result;
}
// Gets the ratelimit parameter dictionary given a deserialized |metadata|.
// Returns nullptr if invalid.
base::Value::Dict* GetRatelimitParams(
std::optional<base::Value::Dict>& metadata) {
if (!metadata)
return nullptr;
return metadata->FindDict(kLockfileRatelimitKey);
}
// Returns the time of the current ratelimit period's start in |metadata|.
// Returns base::Time() if an error occurs.
base::Time GetRatelimitPeriodStart(std::optional<base::Value::Dict>& metadata) {
base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata);
RCHECK(ratelimit_params, base::Time());
std::optional<double> seconds =
ratelimit_params->FindDouble(kLockfileRatelimitPeriodStartKey);
RCHECK(seconds, base::Time());
// Return value of 0 indicates "not initialized", so we need to explicitly
// check for it and return time_t = 0 equivalent.
return *seconds ? base::Time::FromSecondsSinceUnixEpoch(*seconds)
: base::Time::UnixEpoch();
}
// Sets the time of the current ratelimit period's start in |metadata| to
// |period_start|. Returns true on success, false on error.
bool SetRatelimitPeriodStart(std::optional<base::Value::Dict>& metadata,
base::Time period_start) {
DCHECK(!period_start.is_null());
base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata);
RCHECK(ratelimit_params, false);
ratelimit_params->Set(kLockfileRatelimitPeriodStartKey,
period_start.InSecondsFSinceUnixEpoch());
return true;
}
// Gets the number of dumps added to |metadata| in the current ratelimit
// period. Returns < 0 on error.
int GetRatelimitPeriodDumps(std::optional<base::Value::Dict>& metadata) {
base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata);
if (!ratelimit_params)
return -1;
std::optional<int> period_dumps =
ratelimit_params->FindInt(kLockfileRatelimitPeriodDumpsKey);
return period_dumps.value_or(-1);
}
// Sets the current ratelimit period's number of dumps in |metadata| to
// |period_dumps|. Returns true on success, false on error.
bool SetRatelimitPeriodDumps(std::optional<base::Value::Dict>& metadata,
int period_dumps) {
DCHECK_GE(period_dumps, 0);
base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata);
RCHECK(ratelimit_params, false);
ratelimit_params->Set(kLockfileRatelimitPeriodDumpsKey, period_dumps);
return true;
}
// Returns true if |metadata| contains valid metadata, false otherwise.
bool ValidateMetadata(std::optional<base::Value::Dict>& metadata) {
RCHECK(metadata, false);
// Validate ratelimit params
base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata);
return ratelimit_params &&
ratelimit_params->size() == kLockfileNumRatelimitParams &&
!GetRatelimitPeriodStart(metadata).is_null() &&
GetRatelimitPeriodDumps(metadata) >= 0;
}
// Calls flock on valid file descriptor |fd| with flag |flag|. Returns true
// on success, false on failure.
bool CallFlockOnFileWithFlag(const int fd, int flag) {
int ret = -1;
if ((ret = HANDLE_EINTR(flock(fd, flag))) < 0)
PLOG(ERROR) << "Error locking " << fd;
return !ret;
}
int OpenAndLockFile(const base::FilePath& path, bool write) {
int fd = -1;
const char* file = path.value().c_str();
if ((fd = open(file, write ? O_RDWR : O_RDONLY)) < 0) {
PLOG(ERROR) << "Error opening " << file;
} else if (!CallFlockOnFileWithFlag(fd, LOCK_EX)) {
close(fd);
fd = -1;
}
return fd;
}
bool UnlockAndCloseFile(const int fd) {
if (!CallFlockOnFileWithFlag(fd, LOCK_UN))
return false;
return !close(fd);
}
} // namespace
// One day
const int SynchronizedMinidumpManager::kRatelimitPeriodSeconds = 24 * 3600;
const int SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps = 100;
SynchronizedMinidumpManager::SynchronizedMinidumpManager()
: dump_path_(GetMinidumpPath()),
lockfile_path_(dump_path_.Append(kLockfileName).value()),
metadata_path_(dump_path_.Append(kMetadataName).value()),
lockfile_fd_(-1) {}
SynchronizedMinidumpManager::~SynchronizedMinidumpManager() {
// Release the lock if held.
ReleaseLockFile();
}
// TODO(slan): Move some of this pruning logic to ReleaseLockFile?
int SynchronizedMinidumpManager::GetNumDumps(bool delete_all_dumps) {
int num_dumps = 0;
base::DirReaderPosix reader(dump_path_.value().c_str());
if (!reader.IsValid()) {
LOG(ERROR) << "Unable to open directory " << dump_path_.value();
return 0;
}
while (reader.Next()) {
if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
continue;
const base::FilePath dump_file(dump_path_.Append(reader.name()));
// If file cannot be found, skip.
if (!base::PathExists(dump_file))
continue;
// Do not count |lockfile_path_| and |metadata_path_|.
if (lockfile_path_ != dump_file && metadata_path_ != dump_file) {
++num_dumps;
if (delete_all_dumps) {
LOG(INFO) << "Removing " << reader.name()
<< "which was not in the lockfile";
if (!base::DeleteFile(dump_file))
PLOG(INFO) << "Removing " << dump_file.value() << " failed";
}
}
}
return num_dumps;
}
bool SynchronizedMinidumpManager::AcquireLockAndDoWork() {
bool success = false;
if (AcquireLockFile()) {
success = DoWork();
ReleaseLockFile();
}
return success;
}
bool SynchronizedMinidumpManager::AcquireLockFile() {
DCHECK_LT(lockfile_fd_, 0);
// Make the directory for the minidumps if it does not exist.
base::File::Error error;
if (!CreateDirectoryAndGetError(dump_path_, &error)) {
LOG(ERROR) << "Failed to create directory " << dump_path_.value()
<< ". error = " << error;
return false;
}
// Open the lockfile. Create it if it does not exist.
base::File lockfile(lockfile_path_, base::File::FLAG_OPEN_ALWAYS);
// If opening or creating the lockfile failed, we don't want to proceed
// with dump writing for fear of exhausting up system resources.
if (!lockfile.IsValid()) {
LOG(ERROR) << "open lockfile failed " << lockfile_path_.value();
return false;
}
if ((lockfile_fd_ = OpenAndLockFile(lockfile_path_, false)) < 0) {
ReleaseLockFile();
return false;
}
// The lockfile is open and locked. Parse it to provide subclasses with a
// record of all the current dumps.
bool create_lockfiles = false;
if (!base::PathExists(metadata_path_)) {
LOG(INFO) << "Metadata doesn't exist.";
create_lockfiles = true;
} else if (!ParseFiles()) {
LOG(ERROR) << "Lockfile did not parse correctly. ";
create_lockfiles = true;
}
if (create_lockfiles && (!InitializeFiles() || !ParseFiles())) {
LOG(ERROR) << "Failed to create a new lock file!";
ReleaseLockFile();
return false;
}
DCHECK(dumps_);
DCHECK(metadata_);
// We successfully have acquired the lock.
return true;
}
bool SynchronizedMinidumpManager::ParseFiles() {
DCHECK_GE(lockfile_fd_, 0);
DCHECK(!dumps_);
DCHECK(!metadata_);
std::string lockfile;
RCHECK(ReadFileToString(lockfile_path_, &lockfile), false);
std::vector<std::string> lines = base::SplitString(
lockfile, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
base::Value::List dumps;
// Validate dumps
for (const std::string& line : lines) {
if (line.size() == 0)
continue;
std::optional<base::Value> dump_info = base::JSONReader::Read(line);
RCHECK(dump_info.has_value(), false);
DumpInfo info(&dump_info.value());
RCHECK(info.valid(), false);
dumps.Append(std::move(dump_info.value()));
}
JSONFileValueDeserializer deserializer(metadata_path_);
int error_code = -1;
std::string error_msg;
std::unique_ptr<base::Value> metadata_ptr =
deserializer.Deserialize(&error_code, &error_msg);
DLOG_IF(ERROR, !metadata_ptr)
<< "JSON error " << error_code << ":" << error_msg;
RCHECK(metadata_ptr, false);
RCHECK(metadata_ptr->is_dict(), false);
std::optional<base::Value::Dict> metadata =
std::move(*metadata_ptr).TakeDict();
RCHECK(ValidateMetadata(metadata), false);
dumps_ = std::move(dumps);
metadata_ = std::move(metadata);
return true;
}
bool SynchronizedMinidumpManager::WriteFiles(
const base::Value::List& dumps,
const base::Value::Dict& metadata) {
std::string lockfile;
for (const auto& elem : dumps) {
std::string dump_info;
bool ret = base::JSONWriter::Write(elem, &dump_info);
RCHECK(ret, false);
lockfile += dump_info;
lockfile += "\n"; // Add line seperatators
}
if (WriteFile(lockfile_path_, lockfile.c_str(), lockfile.size()) < 0) {
return false;
}
JSONFileValueSerializer serializer(metadata_path_);
return serializer.Serialize(metadata);
}
bool SynchronizedMinidumpManager::InitializeFiles() {
base::Value::Dict metadata;
base::Value::Dict ratelimit_fields;
ratelimit_fields.Set(kLockfileRatelimitPeriodStartKey, 0.0);
ratelimit_fields.Set(kLockfileRatelimitPeriodDumpsKey, 0);
metadata.Set(kLockfileRatelimitKey, std::move(ratelimit_fields));
base::Value::List dumps;
return WriteFiles(dumps, metadata);
}
bool SynchronizedMinidumpManager::AddEntryToLockFile(
const DumpInfo& dump_info) {
DCHECK_GE(lockfile_fd_, 0);
DCHECK(dumps_);
// Make sure dump_info is valid.
if (!dump_info.valid()) {
LOG(ERROR) << "Entry to be added is invalid";
return false;
}
dumps_->Append(dump_info.GetAsValue());
return true;
}
bool SynchronizedMinidumpManager::RemoveEntryFromLockFile(int index) {
if (index < 0 || static_cast<size_t>(index) >= dumps_->size())
return false;
dumps_->erase(dumps_->begin() + index);
return true;
}
void SynchronizedMinidumpManager::ReleaseLockFile() {
// flock is associated with the fd entry in the open fd table, so closing
// all fd's will release the lock. To be safe, we explicitly unlock.
if (lockfile_fd_ >= 0) {
if (dumps_ && metadata_)
WriteFiles(*dumps_, *metadata_);
UnlockAndCloseFile(lockfile_fd_);
lockfile_fd_ = -1;
}
dumps_.reset();
metadata_.reset();
}
std::vector<std::unique_ptr<DumpInfo>> SynchronizedMinidumpManager::GetDumps() {
std::vector<std::unique_ptr<DumpInfo>> dumps;
for (const auto& elem : *dumps_) {
dumps.push_back(std::unique_ptr<DumpInfo>(new DumpInfo(&elem)));
}
return dumps;
}
bool SynchronizedMinidumpManager::SetCurrentDumps(
const std::vector<std::unique_ptr<DumpInfo>>& dumps) {
dumps_->clear();
for (auto& dump : dumps) {
dumps_->Append(dump->GetAsValue());
}
return true;
}
bool SynchronizedMinidumpManager::IncrementNumDumpsInCurrentPeriod() {
DCHECK(metadata_);
int last_dumps = GetRatelimitPeriodDumps(metadata_);
RCHECK(last_dumps >= 0, false);
return SetRatelimitPeriodDumps(metadata_, last_dumps + 1);
}
bool SynchronizedMinidumpManager::DecrementNumDumpsInCurrentPeriod() {
DCHECK(metadata_);
int last_dumps = GetRatelimitPeriodDumps(metadata_);
if (last_dumps > 0) {
return SetRatelimitPeriodDumps(metadata_, last_dumps - 1);
}
return true;
}
void SynchronizedMinidumpManager::ResetRateLimitPeriod() {
SetRatelimitPeriodStart(metadata_, base::Time::Now());
SetRatelimitPeriodDumps(metadata_, 0);
}
bool SynchronizedMinidumpManager::CanUploadDump() {
base::Time cur_time = base::Time::Now();
base::Time period_start = GetRatelimitPeriodStart(metadata_);
int period_dumps_count = GetRatelimitPeriodDumps(metadata_);
// If we're in invalid state, or we passed the period, reset the ratelimit.
// When the device reboots, |cur_time| may be incorrectly reported to be a
// very small number for a short period of time. So only consider
// |period_start| invalid when |cur_time| is less if |cur_time| is not very
// close to 0.
if (period_dumps_count < 0 ||
(cur_time < period_start &&
cur_time.InSecondsFSinceUnixEpoch() > kRatelimitPeriodSeconds) ||
(cur_time - period_start).InSeconds() >= kRatelimitPeriodSeconds) {
ResetRateLimitPeriod();
return true;
}
return period_dumps_count < kRatelimitPeriodMaxDumps;
}
bool SynchronizedMinidumpManager::HasDumps() {
// Check if lockfile has entries.
int64_t size = 0;
if (base::GetFileSize(lockfile_path_, &size) && size > 0)
return true;
// Check if any files are in minidump directory
base::DirReaderPosix reader(dump_path_.value().c_str());
if (!reader.IsValid()) {
DLOG(ERROR) << "Could not open minidump dir: " << dump_path_.value();
return false;
}
while (reader.Next()) {
if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
continue;
const base::FilePath file_path = dump_path_.Append(reader.name());
if (file_path != lockfile_path_ && file_path != metadata_path_)
return true;
}
return false;
}
bool SynchronizedMinidumpManager::InitializeFileState() {
if (!AcquireLockFile())
return false; // Error logged
ReleaseLockFile();
return true;
}
} // namespace chromecast