| // 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 <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" |
| #include "third_party/abseil-cpp/absl/types/optional.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( |
| absl::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( |
| absl::optional<base::Value::Dict>& metadata) { |
| base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata); |
| RCHECK(ratelimit_params, base::Time()); |
| |
| absl::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(absl::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(absl::optional<base::Value::Dict>& metadata) { |
| base::Value::Dict* ratelimit_params = GetRatelimitParams(metadata); |
| if (!ratelimit_params) |
| return -1; |
| absl::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(absl::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(absl::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; |
| absl::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); |
| absl::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 |