chromium/chrome/updater/mac/keystone/agent_main.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <unistd.h>

#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <utility>

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/message_loop/message_pump_type.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_executor.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "chrome/updater/app/app.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/ipc/ipc_support.h"
#include "chrome/updater/service_proxy_factory.h"
#include "chrome/updater/update_service.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/util/util.h"

namespace updater {

namespace {

constexpr char kCommandProductID[] = "productIDToUpdate";
constexpr char kCommandPrintResults[] = "printResults";
constexpr char kCommandUserInitiated[] = "userInitiated";

// base::CommandLine can't be used because it is case-insensitive, and it does
// not support long switches name prefixed with single '-'. This argument parser
// converts an argv set into a map of switch name to switch value; for example
//    `program_name -productIDToUpdate com.google.chrome -printResults YES`
// is converted to:
//    `{"productIDToUpdate": "com.google.chrome", "printResults": "YES"}`.
std::map<std::string, std::string> ParseCommandLine(int argc,
                                                    const char* argv[]) {
  std::map<std::string, std::string> result;
  std::string key;
  for (int i = 1; i < argc; ++i) {
    std::string arg(argv[i]);
    if (base::StartsWith(arg, "-")) {
      key = arg.substr(1);
      result[key] = "";
    } else {
      if (!key.empty()) {
        result[key] = arg;
      }
      key = "";
    }
  }
  return result;
}

UpdaterScope Scope() {
  return geteuid() == 0 ? UpdaterScope::kSystem : UpdaterScope::kUser;
}

class KSAgentApp : public App {
 public:
  explicit KSAgentApp(const std::map<std::string, std::string>& switches)
      : switches_(switches) {}

 private:
  ~KSAgentApp() override = default;
  void FirstTaskRun() override;

  void ChooseServiceForApp(
      const std::string& app_id,
      base::OnceCallback<void(UpdaterScope scope)> callback) const;

  bool HasSwitch(const std::string& arg) const;
  std::string SwitchValue(const std::string& arg) const;

  void UpdateApp(const std::string& app_id);
  void DoUpdate(const std::string& app_id, UpdaterScope scope);
  void RecordUpdateResult(const UpdateService::UpdateState& update_state);
  void PrintUpdateResultAndShutDown(UpdateService::Result result);

  void Wake();

  const std::map<std::string, std::string> switches_;
  scoped_refptr<UpdateService> system_service_proxy_ =
      CreateUpdateServiceProxy(UpdaterScope::kSystem);
  scoped_refptr<UpdateService> user_service_proxy_ =
      CreateUpdateServiceProxy(UpdaterScope::kUser);
  bool update_successful_ = true;  // True when all updates succeed.
  int successful_install_count_ = 0;
  ScopedIPCSupportWrapper ipc_support_;
};

void KSAgentApp::ChooseServiceForApp(
    const std::string& app_id,
    base::OnceCallback<void(UpdaterScope)> callback) const {
  // Choose system scope if the app is a system app, otherwise choose
  // user scope.
  system_service_proxy_->GetAppStates(base::BindOnce(
      [](const std::string& app_id,
         base::OnceCallback<void(UpdaterScope)> callback,
         const std::vector<updater::UpdateService::AppState>& states) {
        std::move(callback).Run(
            base::ranges::find_if(
                states,
                [&app_id](const updater::UpdateService::AppState& state) {
                  return base::EqualsCaseInsensitiveASCII(state.app_id, app_id);
                }) == std::end(states)
                ? UpdaterScope::kUser
                : UpdaterScope::kSystem);
      },
      app_id, std::move(callback)));
}

bool KSAgentApp::HasSwitch(const std::string& arg) const {
  return switches_.contains(arg);
}

std::string KSAgentApp::SwitchValue(const std::string& arg) const {
  return HasSwitch(arg) ? switches_.at(arg) : std::string();
}

void KSAgentApp::RecordUpdateResult(
    const UpdateService::UpdateState& update_state) {
  switch (update_state.state) {
    case UpdateService::UpdateState::State::kUpdated:
      // An accurate number of successful installations is not needed.
      // A positive integer is enough to indicate that some updates are
      // installed.
      VLOG(0) << "An app update is installed successfully.";
      successful_install_count_ += 1;
      break;
    case UpdateService::UpdateState::State::kUpdateError:
      VLOG(1) << "Update error: " << update_state.error_code
              << ", extra_code1: " << update_state.extra_code1;
      update_successful_ = false;
      break;
    default:
      break;
  }
}

void KSAgentApp::PrintUpdateResultAndShutDown(UpdateService::Result result) {
  // The output string format need to be strictly followed because this is the
  // contract between the registration framework and the agent. The registration
  // framework parses the agent outputs and broadcasts the parsed results via
  // macOS notification center. Apps (like Chrome) can monitor the notification
  // center and update the UI accordingly.
  const bool update_ok =
      result == UpdateService::Result::kSuccess && update_successful_;
  std::cout << "updateCheckSuccessful_=" << (update_ok ? "YES" : "NO")
            << std::endl
            << "successfulInstallCount_=" << successful_install_count_
            << std::endl;

  Shutdown(update_ok ? 0 : 1);
}

void KSAgentApp::UpdateApp(const std::string& app_id) {
  ChooseServiceForApp(app_id,
                      base::BindOnce(&KSAgentApp::DoUpdate, this, app_id));
}

void KSAgentApp::DoUpdate(const std::string& app_id, UpdaterScope scope) {
  VLOG(0) << "Updating " << app_id << " at "
          << (scope == UpdaterScope::kSystem ? "system" : "user") << " scope.";
  scoped_refptr<UpdateService> service_proxy = scope == UpdaterScope::kSystem
                                                   ? system_service_proxy_
                                                   : user_service_proxy_;
  service_proxy->Update(
      app_id, "", UpdateService::Priority::kForeground,
      UpdateService::PolicySameVersionUpdate::kNotAllowed,
      base::BindRepeating(&KSAgentApp::RecordUpdateResult, this),
      base::BindOnce(&KSAgentApp::PrintUpdateResultAndShutDown, this));
}

void KSAgentApp::Wake() {
  for (UpdaterScope scope : {UpdaterScope::kSystem, UpdaterScope::kUser}) {
    std::optional<base::FilePath> path = GetUpdaterExecutablePath(scope);
    if (!path) {
      continue;
    }
    base::CommandLine command(*path);
    command.AppendSwitch(kWakeAllSwitch);
    if (scope == UpdaterScope::kSystem) {
      command.AppendSwitch(kSystemSwitch);
    }
    VLOG(0) << "Launching " << command.GetCommandLineString();
    base::Process process = base::LaunchProcess(command, {});
    if (process.IsValid()) {
      VLOG(0) << "Launched " << process.Pid();
    }
  }
  Shutdown(0);
}

void KSAgentApp::FirstTaskRun() {
  // The agent is a shim to trick the keystone registration framework.
  if (!SwitchValue(kCommandProductID).empty() &&
      SwitchValue(kCommandPrintResults) == "YES" &&
      SwitchValue(kCommandUserInitiated) == "YES") {
    // If the agent is run with explicit arguments to print the update result,
    // make a call directly into the service to get update details.
    UpdateApp(SwitchValue(kCommandProductID));
  } else {
    // Otherwise, just launch the --wake task. Not all callers correctly
    // provide a scope, so it will wake both scopes (if present).
    Wake();
  }
}

int KSAgentAppMain(int argc, const char* argv[]) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(argc, argv);
  updater::InitLogging(Scope());
  InitializeThreadPool("keystone");
  const base::ScopedClosureRunner shutdown_thread_pool(
      base::BindOnce([] { base::ThreadPoolInstance::Get()->Shutdown(); }));
  base::SingleThreadTaskExecutor main_task_executor(base::MessagePumpType::UI);

  return base::MakeRefCounted<KSAgentApp>(ParseCommandLine(argc, argv))->Run();
}

}  // namespace

}  // namespace updater

int main(int argc, const char* argv[]) {
  return updater::KSAgentAppMain(argc, argv);
}