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

#include "components/browser_watcher/postmortem_report_collector.h"

#include <utility>

#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/browser_watcher/postmortem_minidump_writer.h"
#include "components/browser_watcher/stability_data_names.h"
#include "third_party/crashpad/crashpad/client/settings.h"
#include "third_party/crashpad/crashpad/util/misc/uuid.h"

namespace browser_watcher {

using base::FilePath;
using crashpad::CrashReportDatabase;

namespace {

// DO NOT CHANGE VALUES. This is logged persistently in a histogram.
enum SystemSessionAnalysisStatus {
  SYSTEM_SESSION_ANALYSIS_SUCCESS = 0,
  SYSTEM_SESSION_ANALYSIS_NO_TIMESTAMP = 1,
  SYSTEM_SESSION_ANALYSIS_NO_ANALYZER = 2,
  SYSTEM_SESSION_ANALYSIS_FAILED = 3,
  SYSTEM_SESSION_ANALYSIS_OUTSIDE_RANGE = 4,
  SYSTEM_SESSION_ANALYSIS_STATUS_MAX = 5
};

bool GetStartTimestamp(
    const google::protobuf::Map<std::string, TypedValue>& global_data,
    base::Time* time) {
  DCHECK(time);

  const auto& it = global_data.find(kStabilityStartTimestamp);
  if (it == global_data.end())
    return false;

  const TypedValue& value = it->second;
  if (value.value_case() != TypedValue::kSignedValue)
    return false;

  *time = base::Time::FromInternalValue(value.signed_value());
  return true;
}

void LogCollectionStatus(CollectionStatus status) {
  UMA_HISTOGRAM_ENUMERATION("ActivityTracker.Collect.Status", status,
                            COLLECTION_STATUS_MAX);
}

}  // namespace

PostmortemReportCollector::PostmortemReportCollector(
    metrics::SystemSessionAnalyzer* analyzer)
    : report_database_(nullptr), system_session_analyzer_(analyzer) {}
PostmortemReportCollector::PostmortemReportCollector(
    const std::string& product_name,
    const std::string& version_number,
    const std::string& channel_name,
    crashpad::CrashReportDatabase* report_database,
    metrics::SystemSessionAnalyzer* analyzer)
    : product_name_(product_name),
      version_number_(version_number),
      channel_name_(channel_name),
      report_database_(report_database),
      system_session_analyzer_(analyzer) {
  DCHECK(!product_name_.empty());
  DCHECK(!version_number.empty());
  DCHECK(!channel_name.empty());
  DCHECK_NE(nullptr, report_database);
}

PostmortemReportCollector::~PostmortemReportCollector() {}

void PostmortemReportCollector::Process(
    const std::vector<base::FilePath>& stability_files) {
  // Determine the crashpad client id.
  crashpad::UUID client_id;
  if (report_database_) {
    crashpad::Settings* settings = report_database_->GetSettings();
    if (settings) {
      // If GetSettings() or GetClientID() fails client_id will be left at its
      // default value, all zeroes, which is appropriate.
      settings->GetClientID(&client_id);
    }
  }

  for (const FilePath& file : stability_files) {
    ProcessOneReport(client_id, file);
  }
}

void PostmortemReportCollector::ProcessOneReport(
    const crashpad::UUID& client_id,
    const FilePath& file) {
  LogCollectionStatus(COLLECTION_ATTEMPT);

  // Note: the code below involves two notions of report: chrome internal state
  // reports and the crashpad reports they get wrapped into.

  // Collect the data from the debug file to a proto.
  StabilityReport report_proto;
  CollectionStatus status = CollectOneReport(file, &report_proto);
  if (status != SUCCESS) {
    // The file was empty, or there was an error collecting the data. This is
    // not deemed an unclean shutdown. Detailed logging happens within the
    // Collect function.
    LogCollectionStatus(status);
    if (!base::DeleteFile(file, false))
      LogCollectionStatus(DEBUG_FILE_DELETION_FAILED);
    return;
  }

  // Delete the stability file. If the file cannot be deleted, do not report its
  // contents - it will be retried in a future processing run. Note that this
  // approach can lead to under reporting and retries. However, under reporting
  // is preferable to the over reporting that would happen with a file that
  // cannot be deleted. Also note that the crash registration may still fail at
  // this point: losing the report in such a case is deemed acceptable.
  if (!base::DeleteFile(file, false)) {
    DLOG(ERROR) << "Failed to delete " << file.value();
    LogCollectionStatus(DEBUG_FILE_DELETION_FAILED);
    return;
  }

  LogCollectionStatus(UNCLEAN_SHUTDOWN);
  if (report_proto.system_state().session_state() == SystemState::UNCLEAN)
    LogCollectionStatus(UNCLEAN_SESSION);

  if (report_database_)
    GenerateCrashReport(client_id, &report_proto);
}

CollectionStatus PostmortemReportCollector::CollectOneReport(
    const base::FilePath& file,
    StabilityReport* report) {
  DCHECK(report);

  CollectionStatus status = Extract(file, report);
  if (status != SUCCESS)
    return status;

  SetReporterDetails(report);
  RecordSystemShutdownState(report);

  return SUCCESS;
}

void PostmortemReportCollector::SetReporterDetails(
    StabilityReport* report) const {
  DCHECK(report);

  google::protobuf::Map<std::string, TypedValue>& global_data =
      *(report->mutable_global_data());

  // Reporter version details. These are useful as the reporter may be of a
  // different version.
  global_data[kStabilityReporterChannel].set_string_value(channel_name());
#if defined(ARCH_CPU_X86)
  global_data[kStabilityReporterPlatform].set_string_value(
      std::string("Win32"));
#elif defined(ARCH_CPU_X86_64)
  global_data[kStabilityReporterPlatform].set_string_value(
      std::string("Win64"));
#endif
  global_data[kStabilityReporterProduct].set_string_value(product_name());
  global_data[kStabilityReporterVersion].set_string_value(version_number());
}

void PostmortemReportCollector::RecordSystemShutdownState(
    StabilityReport* report) const {
  DCHECK(report);

  // The session state for the stability report, recorded to provided visibility
  // into whether the system session was clean.
  SystemState::SessionState session_state = SystemState::UNKNOWN;
  // The status of the analysis, recorded to provide insight into the success
  // or failure of the analysis.
  SystemSessionAnalysisStatus status = SYSTEM_SESSION_ANALYSIS_SUCCESS;

  base::Time time;
  if (!GetStartTimestamp(report->global_data(), &time)) {
    status = SYSTEM_SESSION_ANALYSIS_NO_TIMESTAMP;
  } else if (!system_session_analyzer_) {
    status = SYSTEM_SESSION_ANALYSIS_NO_ANALYZER;
  } else {
    metrics::SystemSessionAnalyzer::Status analyzer_status =
        system_session_analyzer_->IsSessionUnclean(time);
    switch (analyzer_status) {
      case metrics::SystemSessionAnalyzer::FAILED:
        status = SYSTEM_SESSION_ANALYSIS_FAILED;
        break;
      case metrics::SystemSessionAnalyzer::CLEAN:
        session_state = SystemState::CLEAN;
        break;
      case metrics::SystemSessionAnalyzer::UNCLEAN:
        session_state = SystemState::UNCLEAN;
        break;
      case metrics::SystemSessionAnalyzer::OUTSIDE_RANGE:
        status = SYSTEM_SESSION_ANALYSIS_OUTSIDE_RANGE;
        break;
    }
  }

  report->mutable_system_state()->set_session_state(session_state);
  UMA_HISTOGRAM_ENUMERATION(
      "ActivityTracker.Collect.SystemSessionAnalysisStatus", status,
      SYSTEM_SESSION_ANALYSIS_STATUS_MAX);
}

void PostmortemReportCollector::GenerateCrashReport(
    const crashpad::UUID& client_id,
    StabilityReport* report_proto) {
  DCHECK_NE(nullptr, report_database_);
  DCHECK(report_proto);

  // Prepare a crashpad report.
  std::unique_ptr<CrashReportDatabase::NewReport> new_report;
  CrashReportDatabase::OperationStatus database_status =
      report_database_->PrepareNewCrashReport(&new_report);
  if (database_status != CrashReportDatabase::kNoError) {
    LogCollectionStatus(PREPARE_NEW_CRASH_REPORT_FAILED);
    return;
  }

  // Write the report to a minidump.
  if (!WriteReportToMinidump(report_proto, client_id, new_report->ReportID(),
                             new_report->Writer())) {
    LogCollectionStatus(WRITE_TO_MINIDUMP_FAILED);
    return;
  }

  // Finalize the report wrt the report database. Note that this doesn't trigger
  // an immediate upload, but Crashpad will eventually upload the report (as of
  // writing, the delay is on the order of up to 15 minutes).
  crashpad::UUID unused_report_id;
  database_status = report_database_->FinishedWritingCrashReport(
      std::move(new_report), &unused_report_id);
  if (database_status != CrashReportDatabase::kNoError) {
    LogCollectionStatus(FINISHED_WRITING_CRASH_REPORT_FAILED);
    return;
  }

  LogCollectionStatus(SUCCESS);
}

bool PostmortemReportCollector::WriteReportToMinidump(
    StabilityReport* report,
    const crashpad::UUID& client_id,
    const crashpad::UUID& report_id,
    crashpad::FileWriterInterface* minidump_file) {
  DCHECK(report);
  return WritePostmortemDump(minidump_file, client_id, report_id, report);
}

}  // namespace browser_watcher
