chromium/chromecast/crash/linux/minidump_uploader.cc

// Copyright 2016 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/minidump_uploader.h"

#include <errno.h>
#include <regex.h>
#include <stdlib.h>
#include <string.h>

#include <fstream>
#include <list>
#include <memory>
#include <sstream>
#include <vector>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_runner.h"
#include "chromecast/base/cast_paths.h"
#include "chromecast/base/pref_names.h"
#include "chromecast/base/version.h"
#include "chromecast/crash/build_info.h"
#include "chromecast/crash/cast_crashdump_uploader.h"
#include "chromecast/crash/linux/dump_info.h"
#include "chromecast/public/cast_sys_info.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/pref_service_factory.h"

namespace chromecast {

namespace {

const char kProductName[] = "Eureka";

const char kCrashServerProduction[] = "https://clients2.google.com/cr/report";

const char kVirtualChannel[] = "virtual-channel";

typedef std::vector<std::unique_ptr<DumpInfo>> DumpList;

std::unique_ptr<PrefService> CreatePrefService() {
  base::FilePath prefs_path;
  CHECK(base::PathService::Get(chromecast::FILE_CAST_CONFIG, &prefs_path));
  DVLOG(1) << "Loading prefs from " << prefs_path.value();

  PrefRegistrySimple* registry = new PrefRegistrySimple;
  registry->RegisterBooleanPref(prefs::kOptInStats, true);
  registry->RegisterStringPref(::metrics::prefs::kMetricsClientID, "");
  registry->RegisterStringPref(kVirtualChannel, "");

  PrefServiceFactory prefServiceFactory;
  prefServiceFactory.SetUserPrefsFile(
      prefs_path, base::SingleThreadTaskRunner::GetCurrentDefault().get());
  return prefServiceFactory.Create(registry);
}

bool IsDumpObsolete(const DumpInfo& dump) {
  return dump.params().cast_release_version.empty() ||
         dump.params().cast_build_number.empty();
}

}  // namespace

MinidumpUploader::MinidumpUploader(CastSysInfo* sys_info,
                                   const std::string& server_url,
                                   CastCrashdumpUploader* const uploader,
                                   PrefServiceGeneratorCallback callback)
    : release_channel_(sys_info->GetSystemReleaseChannel()),
      product_name_(sys_info->GetProductName()),
      device_model_(sys_info->GetDeviceModel()),
      board_name_(sys_info->GetBoardName()),
      board_revision_(sys_info->GetBoardRevision()),
      manufacturer_(sys_info->GetManufacturer()),
      system_version_(sys_info->GetSystemBuildNumber()),
      upload_location_(!server_url.empty() ? server_url
                                           : kCrashServerProduction),
      reboot_scheduled_(false),
      filestate_initialized_(false),
      uploader_(uploader),
      pref_service_generator_(std::move(callback)) {}

MinidumpUploader::MinidumpUploader(CastSysInfo* sys_info,
                                   const std::string& server_url)
    : MinidumpUploader(sys_info,
                       server_url,
                       nullptr,
                       base::BindRepeating(&CreatePrefService)) {}

MinidumpUploader::~MinidumpUploader() {}

bool MinidumpUploader::UploadAllMinidumps() {
  // Create the lockfile if it doesn't exist.
  if (!filestate_initialized_)
    filestate_initialized_ = InitializeFileState();

  if (HasDumps())
    return AcquireLockAndDoWork();

  return true;
}

bool MinidumpUploader::DoWork() {
  // Read the file stream line by line into a list. As each file is uploaded,
  // it is subsequently deleted from the list. If the file cannot be found
  // (which is a possible scenario if the file uploaded previously, but the
  // device powered off before the log could be updated), it is also deleted.
  // Whenever an upload fails (due to lost connection), the remaining entries
  // on the list will overwrite the log file. This way, the log file reflects
  // the state of the un-uploaded dumps as best as it can.
  // Note: it is also possible that a dump previously uploaded exists in the
  // list, *and* can also be found. This might happen if the device powered
  // off before the dump can be deleted and the log updated. This is
  // unpreventable.

  DumpList dumps(GetDumps());

  int num_uploaded = 0;

  std::unique_ptr<PrefService> pref_service = pref_service_generator_.Run();
  const std::string& client_id(
      pref_service->GetString(::metrics::prefs::kMetricsClientID));
  std::string virtual_channel(pref_service->GetString(kVirtualChannel));
  if (virtual_channel.empty()) {
    virtual_channel = release_channel_;
  }
  bool opt_in_stats = pref_service->GetBoolean(prefs::kOptInStats);
  // Handle each dump and consume it out of the structure.
  while (dumps.size()) {
    const DumpInfo& dump = *(dumps.front());
    const base::FilePath dump_path(dump.crashed_process_dump());
    base::FilePath log_path(dump.logfile());
    const std::vector<std::string>& attachments(dump.attachments());

    bool ignore_and_erase_dump = false;
    if (!opt_in_stats) {
      LOG(INFO) << "OptInStats is false, removing crash dump";
      ignore_and_erase_dump = true;
    } else if (IsDumpObsolete(dump)) {
      NOTREACHED_IN_MIGRATION();
      LOG(INFO) << "DumpInfo belongs to older version, removing crash dump";
      ignore_and_erase_dump = true;
    }

    // Ratelimiting persists across reboots.
    if (reboot_scheduled_) {
      LOG(INFO) << "Already rate limited with a reboot scheduled, removing "
                   "crash dump";
      ignore_and_erase_dump = true;
    } else if (CanUploadDump()) {
      // Record dump for ratelimiting
      IncrementNumDumpsInCurrentPeriod();
    } else {
      LOG(INFO) << "Can't upload dump due to rate limit, will reboot";
      ResetRateLimitPeriod();
      ignore_and_erase_dump = true;
      reboot_scheduled_ = true;
    }

    if (ignore_and_erase_dump) {
      base::DeleteFile(dump_path);
      base::DeleteFile(log_path);
      for (const auto& attachment : attachments) {
        base::FilePath attachment_path(attachment);
        if (attachment_path.DirName() == dump_path.DirName()) {
          base::DeleteFile(attachment_path);
        }
      }
      dumps.erase(dumps.begin());
      continue;
    }

    LOG(INFO) << "OptInStats is true, uploading crash dump";

    int64_t size;
    if (!dump_path.empty() && !base::GetFileSize(dump_path, &size)) {
      // either the file does not exist, or there was an error logging its
      // path, or settings its permission; regardless, we can't upload it.
      for (const auto& attachment : attachments) {
        base::FilePath attachment_path(attachment);
        if (attachment_path.DirName() == dump_path.DirName()) {
          base::DeleteFile(attachment_path);
        }
      }
      dumps.erase(dumps.begin());
      continue;
    }

    std::stringstream comment;
    if (log_path.empty()) {
      comment << "Log file not specified. ";
    } else if (!base::GetFileSize(log_path, &size)) {
      comment << "Can't get size of " << log_path.value() << ": "
              << strerror(errno);
      // if we can't find the log file, don't upload the log
      log_path.clear();
    } else {
      comment << "Log size is " << size << ". ";
    }

    std::stringstream uptime_stream;
    uptime_stream << dump.params().process_uptime;

    // attempt to upload
    LOG(INFO) << "Uploading crash to " << upload_location_;
    CastCrashdumpData crashdump_data;
    crashdump_data.product = kProductName;
    crashdump_data.version = GetVersionString(
        dump.params().cast_release_version, dump.params().cast_build_number);
    crashdump_data.guid = client_id;
    crashdump_data.ptime = uptime_stream.str();
    crashdump_data.comments = comment.str();
    crashdump_data.minidump_pathname = dump_path.value();
    crashdump_data.crash_server = upload_location_;

    // set upload_file parameter based on exec_name
    std::string upload_filename;
    if (dump.params().exec_name == "kernel") {
      upload_filename = "upload_file_ramoops";
    } else {
      upload_filename = "upload_file_minidump";
    }
    crashdump_data.upload_filename = std::move(upload_filename);

    // Depending on if a testing CastCrashdumpUploader object has been set,
    // assign |g| as a reference to the correct object.
    CastCrashdumpUploader vanilla(crashdump_data);
    CastCrashdumpUploader& g = (uploader_ ? *uploader_ : vanilla);

    if (!log_path.empty() && !g.AddAttachment("log_file", log_path.value())) {
      LOG(ERROR) << "Could not attach log file " << log_path.value();
      // Don't fail to upload just because of this.
      comment << "Could not attach log file " << log_path.value() << ". ";
    }

    int attachment_count = 0;
    for (const auto& attachment : attachments) {
      std::string label =
          "attachment_" + base::NumberToString(attachment_count++);
      g.AddAttachment(label, attachment);
    }

    // Dump some Android properties directly into product data.
    g.SetParameter("ro.revision", board_revision_);
    g.SetParameter("ro.product.release.track", release_channel_);
    g.SetParameter("ro.hardware", board_name_);
    g.SetParameter("ro.product.name", product_name_);
    g.SetParameter("device", product_name_);
    g.SetParameter("ro.product.model", device_model_);
    g.SetParameter("ro.product.manufacturer", manufacturer_);
    g.SetParameter("ro.system.version", system_version_);
    g.SetParameter("release.virtual-channel", virtual_channel);
    g.SetParameter("ro.build.type", GetBuildVariant());
    // Add app state information
    if (!dump.params().previous_app_name.empty()) {
      g.SetParameter("previous_app", dump.params().previous_app_name);
    }
    if (!dump.params().current_app_name.empty()) {
      g.SetParameter("current_app", dump.params().current_app_name);
    }
    if (!dump.params().last_app_name.empty()) {
      g.SetParameter("last_app", dump.params().last_app_name);
    }
    if (!dump.params().reason.empty()) {
      g.SetParameter("reason", dump.params().reason);
    }
    if (!dump.params().exec_name.empty()) {
      g.SetParameter("exec_name", dump.params().exec_name);
    }
    if (!dump.params().stadia_session_id.empty()) {
      g.SetParameter("stadia_session_id", dump.params().stadia_session_id);
    }
    if (!dump.params().signature.empty()) {
      g.SetParameter("signature", dump.params().signature);
    }
    if (!dump.params().extra_info.empty()) {
      std::vector<std::string> pairs = base::SplitString(dump.params().extra_info,
                                                         " ",
                                                         base::TRIM_WHITESPACE,
                                                         base::SPLIT_WANT_NONEMPTY
                                                        );
      for (const auto& pair : pairs) {
        std::vector<std::string> key_value =
                base::SplitString(pair, "=", base::TRIM_WHITESPACE,
                                  base::SPLIT_WANT_NONEMPTY);
        if (key_value.size() == 2) {
          g.SetParameter(key_value[0], key_value[1]);
        }
      }
    }

    // Set CastLite specific crash report data.
    if (!dump.params().comments.empty()) {
      g.SetParameter("comments", dump.params().comments);
    }
    if (!dump.params().js_engine.empty()) {
      g.SetParameter("js_engine", dump.params().js_engine);
    }
    if (!dump.params().js_build_label.empty()) {
      g.SetParameter("js_build_label", dump.params().js_build_label);
    }
    if (!dump.params().js_exception_category.empty()) {
      g.SetParameter("js_exception_category",
                     dump.params().js_exception_category);
    }
    if (!dump.params().js_exception_details.empty()) {
      g.SetParameter("js_exception_details",
                     dump.params().js_exception_details);
    }
    if (!dump.params().js_exception_signature.empty()) {
      // Upload as "signature" to populate the "Stable Signature" field
      g.SetParameter("signature", dump.params().js_exception_signature);
    }

    std::string response;
    if (!g.Upload(&response)) {
      // We have failed to upload this file.
      // Save our state by flushing our dumps to the lockfile
      // We'll come back around later and try again.
      LOG(ERROR) << "Upload report failed. response: " << response;
      // The increment will happen when it retries the upload.
      DecrementNumDumpsInCurrentPeriod();
      SetCurrentDumps(dumps);
      return true;
    }

    LOG(INFO) << "Uploaded report id " << response;
    // upload succeeded, so delete the entry
    dumps.erase(dumps.begin());
    // delete the dump if it exists in /data/minidumps.
    // (We may use a fake dump file which should not be deleted.)
    if (!dump_path.empty() && dump_path.DirName() == dump_path_ &&
        !base::DeleteFile(dump_path)) {
      PLOG(WARNING) << "remove dump " << dump_path.value() << " failed";
    }
    // delete the log if exists
    if (!log_path.empty() && !base::DeleteFile(log_path)) {
      PLOG(WARNING) << "remove log " << log_path.value() << " failed";
    }
    // delete the attachments
    if (!dump_path.empty()) {
      for (const auto& attachment : attachments) {
        base::FilePath attachment_path(attachment);
        if (attachment_path.DirName() == dump_path.DirName() &&
            !base::DeleteFile(attachment_path)) {
          PLOG(WARNING) << "remove attachment " << attachment << " failed";
        }
      }
    }
    ++num_uploaded;
  }

  // This will simply empty the log file.
  // Entries should either be skipped/deleted or processed/deleted.
  SetCurrentDumps(dumps);

  // If we reach here, then the log file should be empty, and there should
  // be no more dumps to upload. However, it is possible that there are
  // lingering files (for example, if the dump was written, but the log
  // updating failed). Since we have no entries on these files, we cannot
  // upload them. Therefore we should delete them. This is also a good way
  // to make sure system resources aren't being drained.

  int num_deleted = GetNumDumps(true /* delete_all_dumps */);
  if (num_deleted > 0) {
    LOG(WARNING) << num_deleted << " lingering dump files deleted.";
  }

  LOG(INFO) << num_uploaded << " dumps were uploaded.";
  return true;
}

}  // namespace chromecast