chromium/chrome/browser/win/conflicts/module_inspector.cc

// Copyright 2017 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/win/conflicts/module_inspector.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/browser/win/conflicts/module_info_util.h"
#include "chrome/browser/win/util_win_service.h"
#include "chrome/common/chrome_paths.h"
#include "content/public/browser/browser_thread.h"

namespace {

// The maximum amount of time a stale entry is kept in the cache before it is
// deleted.
constexpr base::TimeDelta kMaxEntryAge = base::Days(30);

constexpr int kConnectionErrorRetryCount = 10;

StringMapping GetPathMapping() {
  return GetEnvironmentVariablesMapping({
      L"LOCALAPPDATA",
      L"ProgramFiles",
      L"ProgramData",
      L"USERPROFILE",
      L"SystemRoot",
      L"TEMP",
      L"TMP",
      L"CommonProgramFiles",
  });
}

// Reads the inspection results cache.
InspectionResultsCache ReadInspectionResultsCacheOnBackgroundSequence(
    const base::FilePath& file_path) {
  InspectionResultsCache inspection_results_cache;

  uint32_t min_time_stamp =
      CalculateTimeStamp(base::Time::Now() - kMaxEntryAge);
  ReadInspectionResultsCache(file_path, min_time_stamp,
                             &inspection_results_cache);

  return inspection_results_cache;
}

// Writes the inspection results cache to disk.
void WriteInspectionResultCacheOnBackgroundSequence(
    const base::FilePath& file_path,
    const InspectionResultsCache& inspection_results_cache) {
  WriteInspectionResultsCache(file_path, inspection_results_cache);
}

}  // namespace

// static
constexpr base::TimeDelta ModuleInspector::kFlushInspectionResultsTimerTimeout;

ModuleInspector::ModuleInspector(
    const OnModuleInspectedCallback& on_module_inspected_callback)
    : on_module_inspected_callback_(on_module_inspected_callback),
      is_started_(false),
      util_win_factory_callback_(
          base::BindRepeating(&LaunchUtilWinServiceInstance)),
      path_mapping_(GetPathMapping()),
      cache_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})),
      inspection_results_cache_read_(false),
      flush_inspection_results_timer_(
          FROM_HERE,
          kFlushInspectionResultsTimerTimeout,
          base::BindRepeating(
              &ModuleInspector::MaybeUpdateInspectionResultsCache,
              base::Unretained(this))),
      has_new_inspection_results_(false),
      connection_error_retry_count_(kConnectionErrorRetryCount),
      is_waiting_on_util_win_service_(false) {}

ModuleInspector::~ModuleInspector() = default;

void ModuleInspector::StartInspection() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // This function can be invoked multiple times.
  if (is_started_) {
    return;
  }

  is_started_ = true;

  // Read the inspection cache now that it is needed.
  cache_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&ReadInspectionResultsCacheOnBackgroundSequence,
                     GetInspectionResultsCachePath()),
      base::BindOnce(&ModuleInspector::OnInspectionResultsCacheRead,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ModuleInspector::AddModule(const ModuleInfoKey& module_key) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  bool was_queue_empty = queue_.empty();

  queue_.push(module_key);

  // If the queue was empty before adding the current module, then the
  // inspection must be started.
  if (inspection_results_cache_read_ && was_queue_empty)
    StartInspectingModule();
}

bool ModuleInspector::IsIdle() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return queue_.empty();
}

void ModuleInspector::OnModuleDatabaseIdle() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  MaybeUpdateInspectionResultsCache();
}

// static
base::FilePath ModuleInspector::GetInspectionResultsCachePath() {
  base::FilePath user_data_dir;
  if (!base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir))
    return base::FilePath();

  return user_data_dir.Append(L"Module Info Cache");
}

void ModuleInspector::SetUtilWinFactoryCallbackForTesting(
    UtilWinFactoryCallback util_win_factory_callback) {
  util_win_factory_callback_ = std::move(util_win_factory_callback);
}

void ModuleInspector::EnsureUtilWinServiceBound() {
  if (remote_util_win_)
    return;

  remote_util_win_ = util_win_factory_callback_.Run();
  remote_util_win_.reset_on_idle_timeout(base::Seconds(5));
  remote_util_win_.set_disconnect_handler(
      base::BindOnce(&ModuleInspector::OnUtilWinServiceConnectionError,
                     base::Unretained(this)));
}

void ModuleInspector::OnInspectionResultsCacheRead(
    InspectionResultsCache inspection_results_cache) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(is_started_);
  DCHECK(!inspection_results_cache_read_);

  inspection_results_cache_read_ = true;
  inspection_results_cache_ = std::move(inspection_results_cache);

  if (!queue_.empty())
    StartInspectingModule();
}

void ModuleInspector::OnUtilWinServiceConnectionError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Disconnect from the service.
  remote_util_win_.reset();

  // If the retry limit was reached, give up.
  if (connection_error_retry_count_ == 0)
    return;
  --connection_error_retry_count_;

  bool was_waiting_on_util_win_service = is_waiting_on_util_win_service_;
  is_waiting_on_util_win_service_ = false;

  // If this connection error happened while the ModuleInspector was waiting on
  // the service, restart the inspection process.
  if (was_waiting_on_util_win_service)
    StartInspectingModule();
}

void ModuleInspector::StartInspectingModule() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(inspection_results_cache_read_);
  DCHECK(!queue_.empty());

  const ModuleInfoKey& module_key = queue_.front();

  // First check if the cache already contains the inspection result.
  auto inspection_result =
      GetInspectionResultFromCache(module_key, &inspection_results_cache_);
  if (inspection_result) {
    // Send asynchronously or this might cause a stack overflow.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&ModuleInspector::OnInspectionFinished,
                                  weak_ptr_factory_.GetWeakPtr(), module_key,
                                  std::move(*inspection_result)));
    return;
  }

  EnsureUtilWinServiceBound();

  is_waiting_on_util_win_service_ = true;
  remote_util_win_->InspectModule(
      module_key.module_path,
      base::BindOnce(&ModuleInspector::OnModuleNewlyInspected,
                     weak_ptr_factory_.GetWeakPtr(), module_key));
}

void ModuleInspector::OnModuleNewlyInspected(
    const ModuleInfoKey& module_key,
    ModuleInspectionResult inspection_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  is_waiting_on_util_win_service_ = false;

  // Convert the prefix of known Windows directories to their environment
  // variable mappings (ie, %systemroot$). This makes i18n localized paths
  // easily comparable.
  CollapseMatchingPrefixInPath(path_mapping_, &inspection_result.location);

  has_new_inspection_results_ = true;
  if (!flush_inspection_results_timer_.IsRunning())
    flush_inspection_results_timer_.Reset();
  AddInspectionResultToCache(module_key, inspection_result,
                             &inspection_results_cache_);

  OnInspectionFinished(module_key, std::move(inspection_result));
}

void ModuleInspector::OnInspectionFinished(
    const ModuleInfoKey& module_key,
    ModuleInspectionResult inspection_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(!queue_.empty());
  DCHECK(queue_.front() == module_key);

  // Pop first, because the callback may want to know if there is any work left
  // to be done, which is caracterized by a non-empty queue.
  queue_.pop();

  on_module_inspected_callback_.Run(module_key, std::move(inspection_result));

  // Continue the work.
  if (!queue_.empty())
    StartInspectingModule();
}

void ModuleInspector::MaybeUpdateInspectionResultsCache() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!has_new_inspection_results_)
    return;

  has_new_inspection_results_ = false;
  cache_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&WriteInspectionResultCacheOnBackgroundSequence,
                                GetInspectionResultsCachePath(),
                                inspection_results_cache_));
}