chromium/chrome/browser/ash/policy/reporting/metrics_reporting/fatal_crash/fatal_crash_events_observer_uploaded_crash_info_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_uploaded_crash_info_manager.h"

#include <atomic>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/fatal_crash/fatal_crash_events_observer.h"
#include "chromeos/ash/services/cros_healthd/public/mojom/cros_healthd_events.mojom.h"
#include "components/reporting/util/status.h"
#include "components/reporting/util/statusor.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace reporting {

using ::ash::cros_healthd::mojom::CrashUploadInfoPtr;

namespace {
// Truncates a string to a maximum of length of `size`.
[[nodiscard]] std::string_view TruncateString(std::string_view str,
                                              size_t size) {
  return str.substr(0u, size);
}
}  // namespace

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

FatalCrashEventsObserver::UploadedCrashInfoManager::UploadedCrashInfoManager(
    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::UploadedCrashInfoManager::
    ~UploadedCrashInfoManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

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

  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          [](base::FilePath save_file) -> StatusOr<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)) {
              std::string error_message = base::StrCat(
                  {"Failed to read save file: ", save_file.value()});
              LOG(ERROR) << error_message;
              return base::unexpected(
                  Status(error::INTERNAL, std::move(error_message)));
            }

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

void FatalCrashEventsObserver::UploadedCrashInfoManager::ResumeLoadingSaveFile(
    const StatusOr<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();
  };

  if (!content.has_value()) {
    // Error already logged immediately after reading file fails.
    return;
  }

  const auto parsed_result =
      base::JSONReader::ReadAndReturnValueWithError(content.value());
  if (!parsed_result.has_value()) {
    LOG(ERROR) << "Failed to parse the save file " << save_file_.value()
               << " as JSON: " << parsed_result.error().ToString();
    return;
  }

  const auto* const dict_result = parsed_result.value().GetIfDict();
  if (dict_result == nullptr) {
    LOG(ERROR) << "Parsed JSON string is not a dict: "
               << TruncateString(content.value(), /*size=*/200u);
    return;
  }

  const auto* const creation_timestamp_ms_string =
      dict_result->FindString(kCreationTimestampMsJsonKey);
  if (creation_timestamp_ms_string == nullptr) {
    LOG(ERROR) << "Creation timestamp key " << kCreationTimestampMsJsonKey
               << " not found in JSON: "
               << TruncateString(content.value(), /*size=*/200u);
    return;
  }

  ParseResult result;

  if (!base::StringToInt64(*creation_timestamp_ms_string,
                           &result.uploads_log_creation_timestamp_ms)) {
    LOG(ERROR) << "Failed to convert timestamp "
               << *creation_timestamp_ms_string << " to int64.";
    return;
  }

  if (result.uploads_log_creation_timestamp_ms < 0) {
    LOG(ERROR) << "Timestamp " << *creation_timestamp_ms_string
               << " is negative.";
    return;
  }

  const auto* const offset_string = dict_result->FindString(kOffsetJsonKey);
  if (offset_string == nullptr) {
    LOG(ERROR) << "Offset key " << kOffsetJsonKey << " not found in JSON: "
               << TruncateString(content.value(), /*size=*/200u);
    return;
  }

  if (!base::StringToUint64(*offset_string, &result.uploads_log_offset)) {
    LOG(ERROR) << "Failed to convert offset " << *offset_string
               << " to uint64.";
    return;
  }

  // Ignore additional keys here even if they are present, so as to keep
  // slightly better flexibility when more fields are added to this JSON file
  // (thus reversion won't break the current code).

  uploads_log_creation_time_ = base::Time::FromMillisecondsSinceUnixEpoch(
      result.uploads_log_creation_timestamp_ms);
  uploads_log_offset_ = result.uploads_log_offset;
}

Status FatalCrashEventsObserver::UploadedCrashInfoManager::WriteSaveFile()
    const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::Value::Dict info;
  info.Set(kCreationTimestampMsJsonKey,
           base::NumberToString(
               uploads_log_creation_time_.InMillisecondsSinceUnixEpoch()));
  info.Set(kOffsetJsonKey, base::NumberToString(uploads_log_offset_));

  auto content = base::WriteJson(info);
  if (!content.has_value()) {
    return Status(error::INTERNAL,
                  "Failed to create a JSON string for uploaded crash info");
  }

  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(content).value(),
          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);

  return Status::StatusOK();
}

bool FatalCrashEventsObserver::UploadedCrashInfoManager::IsNewer(
    base::Time uploads_log_creation_time,
    uint64_t uploads_log_offset) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  return std::tie(uploads_log_creation_time, uploads_log_offset) >
         std::tie(uploads_log_creation_time_, uploads_log_offset_);
}

bool FatalCrashEventsObserver::UploadedCrashInfoManager::ShouldReport(
    const CrashUploadInfoPtr& upload_info) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  return IsNewer(upload_info->creation_time, upload_info->offset);
}

void FatalCrashEventsObserver::UploadedCrashInfoManager::Update(
    base::Time uploads_log_creation_time,
    uint64_t uploads_log_offset) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!IsNewer(uploads_log_creation_time, uploads_log_offset)) {
    return;
  }

  uploads_log_creation_time_ = uploads_log_creation_time;
  uploads_log_offset_ = uploads_log_offset;
  const Status status = WriteSaveFile();
  if (!status.ok()) {
    LOG(ERROR) << "Failed to write save file: " << status;
    return;
  }
}

bool FatalCrashEventsObserver::UploadedCrashInfoManager::IsSaveFileLoaded()
    const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return save_file_loaded_;
}
}  // namespace reporting