chromium/chrome/browser/ash/policy/reporting/metrics_reporting/fatal_crash/fatal_crash_events_observer_reported_local_id_manager.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/policy/reporting/metrics_reporting/fatal_crash/fatal_crash_events_observer_reported_local_id_manager.h"

#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/task_runner.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/fatal_crash/fatal_crash_events_observer.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace reporting {

FatalCrashEventsObserver::ReportedLocalIdManager::ReportedLocalIdManager(
    base::FilePath save_file_path,
    SaveFileLoadedCallback save_file_loaded_callback,
    scoped_refptr<base::SequencedTaskRunner> io_task_runner)
    : save_file_{std::move(save_file_path)},
      save_file_loaded_callback_{std::move(save_file_loaded_callback)},
      io_task_runner_{io_task_runner == nullptr
                          ? base::ThreadPool::CreateSequencedTaskRunner(
                                {base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
                                 base::MayBlock()})
                          : std::move(io_task_runner)} {
  LoadSaveFile();
}

FatalCrashEventsObserver::ReportedLocalIdManager::~ReportedLocalIdManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

// static
std::unique_ptr<FatalCrashEventsObserver::ReportedLocalIdManager>
FatalCrashEventsObserver::ReportedLocalIdManager::Create(
    base::FilePath save_file_path,
    SaveFileLoadedCallback save_file_loaded_callback,
    scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
  return base::WrapUnique(new ReportedLocalIdManager(
      std::move(save_file_path), std::move(save_file_loaded_callback),
      io_task_runner));
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::HasBeenReported(
    const std::string& local_id) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return base::Contains(local_ids_, local_id);
}

FatalCrashEventsObserver::ReportedLocalIdManager::ShouldReportResult
FatalCrashEventsObserver::ReportedLocalIdManager::ShouldReport(
    const std::string& local_id,
    int64_t capture_timestamp_us) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (capture_timestamp_us < 0) {
    // Only possible when loading a corrupt save file.
    LOG(ERROR) << "Negative timestamp found: " << local_id << ','
               << capture_timestamp_us;
    return ShouldReportResult::kNegativeTimestamp;
  }

  // Local ID already reported.
  if (HasBeenReported(local_id)) {
    return ShouldReportResult::kHasBeenReported;
  }

  // Max number of crash events reached and the current crash event is too old.
  if (local_ids_.size() >= kMaxNumOfLocalIds &&
      capture_timestamp_us <= GetEarliestLocalIdEntry().capture_timestamp_us) {
    return ShouldReportResult::kCrashTooOldAndMaxNumOfSavedLocalIdsReached;
  }

  return ShouldReportResult::kYes;
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::UpdateLocalId(
    const std::string& local_id,
    int64_t capture_timestamp_us) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const auto result = UpdateLocalIdInRam(local_id, capture_timestamp_us);
  WriteSaveFile();
  return result;
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::UpdateLocalIdInRam(
    const std::string& local_id,
    int64_t capture_timestamp_us) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (ShouldReport(local_id, capture_timestamp_us) !=
      ReportedLocalIdManager::ShouldReportResult::kYes) {
    return false;
  }

  // Keep only the most recent kMaxNumOfLocalIds local IDs. Remove that oldest
  // local IDs if too many are saved.
  if (local_ids_.size() >= kMaxNumOfLocalIds) {
    RemoveEarliestLocalIdEntry();
  }

  return Add(local_id, capture_timestamp_us);
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::Add(
    const std::string& local_id,
    int64_t capture_timestamp_us) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Clean up before adding more entries. Otherwise it is possible that the
  // queue can grow uncontrolled.
  CleanUpLocalIdEntryQueue();
  if (!local_ids_.try_emplace(local_id, capture_timestamp_us).second) {
    return false;
  }
  local_id_entry_queue_.emplace(local_id, capture_timestamp_us);

  return true;
}

void FatalCrashEventsObserver::ReportedLocalIdManager::Remove(
    const std::string& local_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  local_ids_.erase(local_id);
  WriteSaveFile();
}

void FatalCrashEventsObserver::ReportedLocalIdManager::LoadSaveFile() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          [](base::FilePath save_file) -> std::string {
            if (!base::PathExists(save_file)) {
              // File has never been written yet, skip loading it.
              return std::string();
            }

            std::string content;
            if (!base::ReadFileToString(save_file, &content)) {
              LOG(ERROR) << "Failed to read save file: " << save_file;
              return std::string();
            }

            return content;
          },
          save_file_),
      base::BindOnce(&ReportedLocalIdManager::ResumeLoadingSaveFile,
                     weak_factory_.GetWeakPtr()));
}

void FatalCrashEventsObserver::ReportedLocalIdManager::ResumeLoadingSaveFile(
    const std::string& content) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(save_file_loaded_callback_);

  absl::Cleanup run_callback_on_return = [this] {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    save_file_loaded_ = true;
    std::move(save_file_loaded_callback_).Run();
  };

  // Parse the CSV file line by line. If one line is erroneous, stop parsing the
  // rest.
  for (const auto line : base::SplitStringPiece(
           content, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
    // Parse.
    const auto csv_line_items = base::SplitStringPiece(
        base::TrimWhitespaceASCII(line, base::TRIM_ALL), ",",
        base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
    if (csv_line_items.size() != 2u) {
      LOG(ERROR) << "CSV line does not contain 2 rows: " << line;
      return;
    }

    const auto local_id = csv_line_items[0];
    int64_t capture_timestamp_us;
    if (!base::StringToInt64(csv_line_items[1], &capture_timestamp_us)) {
      LOG(ERROR) << "Failed to parse the timestamp: " << line;
      return;
    }

    // Load to RAM.
    if (!UpdateLocalIdInRam(std::string(local_id), capture_timestamp_us)) {
      LOG(ERROR) << "Not able to add the current crash: " << line;
      return;
    }
  }
}

void FatalCrashEventsObserver::ReportedLocalIdManager::WriteSaveFile() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Create the content of the CSV.
  std::ostringstream csv_content;
  for (const auto& [local_id, capture_timestamp_us] : local_ids_) {
    csv_content << local_id << ',' << capture_timestamp_us << '\n';
  }

  io_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](base::FilePath save_file, base::FilePath save_file_tmp,
             std::string content, uint64_t cur_save_file_writing_task_id,
             const std::atomic<uint64_t>* latest_save_file_writing_task_id) {
            if (cur_save_file_writing_task_id <
                latest_save_file_writing_task_id->load()) {
              // Another file writing task has been posted. Skip this one.
              return;
            }
            // Write to the temp save file first, then rename it to the official
            // save file. This would prevent partly written file to be
            // effective, as renaming within the same partition is atomic on
            // POSIX systems.
            if (!base::WriteFile(save_file_tmp, content)) {
              LOG(ERROR) << "Failed to write save file " << save_file_tmp;
              return;
            }
            if (base::File::Error err;
                !base::ReplaceFile(save_file_tmp, save_file, &err)) {
              LOG(ERROR) << "Failed to move file from " << save_file_tmp
                         << " to " << save_file << ": " << err;
              return;
            }
            // Successfully written the save file.
          },
          save_file_, save_file_tmp_, std::move(csv_content).str(),
          latest_save_file_writing_task_id_->load() + 1u,
          latest_save_file_writing_task_id_.get()));

  // Increase the latest task ID only after the latest task has been posted, not
  // before. Otherwise, in a rare case that this thread hangs after the latest
  // task ID has increased, the IO thread would prematurely skip all file
  // writing tasks.
  latest_save_file_writing_task_id_->fetch_add(1u);
}

void FatalCrashEventsObserver::ReportedLocalIdManager::
    CleanUpLocalIdEntryQueue() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (local_id_entry_queue_.size() >= kMaxSizeOfLocalIdEntryQueue) {
    // In extreme situations, such as when the oldest unuploaded crash remains
    // unuploaded for an extended amount of time, it's possible to leave a lot
    // of uploaded crashes' local IDs in local_id_entry_queue_. In this case,
    // rebuild the queue.
    ReconstructLocalIdEntries();
    return;
  }

  // Clean up uploaded crashes from the top of the priority queue.
  while (!local_id_entry_queue_.empty()) {
    if (base::Contains(local_ids_, local_id_entry_queue_.top().local_id)) {
      break;
    }
    local_id_entry_queue_.pop();
  }
}

void FatalCrashEventsObserver::ReportedLocalIdManager::
    ReconstructLocalIdEntries() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // First build the container.
  std::vector<LocalIdEntry> queue_container;
  queue_container.reserve(local_ids_.size());
  std::transform(local_ids_.begin(), local_ids_.end(),
                 std::back_inserter(queue_container),
                 [](const std::pair<std::string, int64_t>& input) {
                   return LocalIdEntry{.local_id = input.first,
                                       .capture_timestamp_us = input.second};
                 });

  // Then reconstruct the local ID entries priority queue from the container.
  local_id_entry_queue_ = decltype(local_id_entry_queue_)(
      decltype(local_id_entry_queue_)::value_compare(),
      std::move(queue_container));
}

const FatalCrashEventsObserver::LocalIdEntry&
FatalCrashEventsObserver::ReportedLocalIdManager::GetEarliestLocalIdEntry() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CleanUpLocalIdEntryQueue();
  // After the cleanup, the top of `local_id_entry_queue_` is the earliest.
  return local_id_entry_queue_.top();
}

void FatalCrashEventsObserver::ReportedLocalIdManager::
    RemoveEarliestLocalIdEntry() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CleanUpLocalIdEntryQueue();
  // After the cleanup, the top of `local_id_entry_queue_` is the earliest.
  local_ids_.erase(local_id_entry_queue_.top().local_id);
  local_id_entry_queue_.pop();
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::IsSaveFileLoaded()
    const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return save_file_loaded_;
}

bool FatalCrashEventsObserver::ReportedLocalIdManager::LocalIdEntryComparator::
operator()(const LocalIdEntry& a, const LocalIdEntry& b) const {
  return a.capture_timestamp_us > b.capture_timestamp_us;
}

}  // namespace reporting