chromium/chrome/browser/ash/policy/remote_commands/device_command_reboot_job.cc

// Copyright 2015 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/remote_commands/device_command_reboot_job.h"

#include <utility>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/system/sys_info.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/reboot_notifications_scheduler.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/login/session/session_termination_manager.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "third_party/cros_system_api/dbus/power_manager/dbus-constants.h"

namespace policy {

namespace {

const char kKioskRebootDescription[] = "Reboot remote command (kiosk)";
const char kLoginScreenRebootDescription[] =
    "Reboot remote command (login screen)";
const char kUserSessionRebootDescription[] =
    "Reboot remote command (user session)";

const char kPayloadUserSessionRebootDelayField[] = "user_session_delay_seconds";

constexpr base::TimeDelta kDefaultUserSessionRebootDelay = base::Minutes(5);

std::optional<base::TimeDelta> ExtractUserSessionDelayFromCommandLine() {
  const base::CommandLine* command_line =
      base::CommandLine::ForCurrentProcess();

  const std::string delay_string = command_line->GetSwitchValueASCII(
      ash::switches::kRemoteRebootCommandDelayInSecondsForTesting);

  if (delay_string.empty()) {
    return std::nullopt;
  }

  int delay_in_seconds;
  if (!base::StringToInt(delay_string, &delay_in_seconds) ||
      delay_in_seconds < 0) {
    LOG(ERROR) << "Ignored "
               << ash::switches::kRemoteRebootCommandDelayInSecondsForTesting
               << " = " << delay_string;
    return std::nullopt;
  }

  return base::Seconds(delay_in_seconds);
}

std::optional<base::TimeDelta> ExtractUserSessionDelayFromPayload(
    const std::string& command_payload) {
  const std::optional<base::Value> root =
      base::JSONReader::Read(command_payload);
  if (!root || !root->is_dict()) {
    return std::nullopt;
  }

  std::optional<int> delay_in_seconds =
      root->GetDict().FindInt(kPayloadUserSessionRebootDelayField);
  if (!delay_in_seconds || delay_in_seconds.value() < 0) {
    return std::nullopt;
  }

  return base::Seconds(delay_in_seconds.value());
}

base::TimeTicks GetBootTime() {
  return base::TimeTicks::Now() - base::SysInfo::Uptime();
}

}  // namespace

DeviceCommandRebootJob::DeviceCommandRebootJob()
    : DeviceCommandRebootJob(chromeos::PowerManagerClient::Get(),
                             ash::LoginState::Get(),
                             ash::SessionTerminationManager::Get(),
                             RebootNotificationsScheduler::Get(),
                             base::DefaultClock::GetInstance(),
                             base::DefaultTickClock::GetInstance(),
                             base::BindRepeating(GetBootTime)) {}

DeviceCommandRebootJob::DeviceCommandRebootJob(
    chromeos::PowerManagerClient* power_manager_client,
    ash::LoginState* loging_state,
    ash::SessionTerminationManager* session_termination_manager,
    RebootNotificationsScheduler* in_session_notifications_scheduler,
    const base::Clock* clock,
    const base::TickClock* tick_clock,
    GetBootTimeCallback get_boot_time_callback)
    : power_manager_client_(power_manager_client),
      login_state_(loging_state),
      session_termination_manager_(session_termination_manager),
      in_session_notifications_scheduler_(in_session_notifications_scheduler),
      in_session_reboot_timer_(clock, tick_clock),
      clock_(clock),
      get_boot_time_callback_(std::move(get_boot_time_callback)),
      user_session_delay_(kDefaultUserSessionRebootDelay) {
  DCHECK(get_boot_time_callback_);
}

DeviceCommandRebootJob::~DeviceCommandRebootJob() = default;

enterprise_management::RemoteCommand_Type DeviceCommandRebootJob::GetType()
    const {
  return enterprise_management::RemoteCommand_Type_DEVICE_REBOOT;
}

bool DeviceCommandRebootJob::ParseCommandPayload(
    const std::string& command_payload) {
  const std::optional<base::TimeDelta> commandline_delay =
      ExtractUserSessionDelayFromCommandLine();

  // Ignore payload if delay is set in command line.
  if (commandline_delay) {
    user_session_delay_ = commandline_delay.value();
    return true;
  }

  const std::optional<base::TimeDelta> payload_delay =
      ExtractUserSessionDelayFromPayload(command_payload);
  if (payload_delay) {
    user_session_delay_ = payload_delay.value();
    return true;
  }

  // Don't fail even if delay is not supplied. Use default one.
  return true;
}

void DeviceCommandRebootJob::RunImpl(CallbackWithResult result_callback) {
  result_callback_ = std::move(result_callback);

  // Determines the time delta between the command having been issued and the
  // boot time of the system.
  const base::TimeDelta delta = get_boot_time_callback_.Run() - issued_time();
  // If the reboot command was issued before the system booted, we inform the
  // server that the reboot succeeded. Otherwise, the reboot must still be
  // performed and we invoke it.
  if (delta.is_positive()) {
    LOG(WARNING) << "Ignoring reboot command issued " << delta
                 << " before current boot time";
    return RunAsyncCallback(std::move(result_callback_), ResultType::kSuccess,
                            FROM_HERE);
  }

  if (!power_manager_client_) {
    LOG(ERROR) << "Power manager is not initialized. Cannot reboot.";
    return RunAsyncCallback(std::move(result_callback_), ResultType::kFailure,
                            FROM_HERE);
  }

  // Make sure `power_manager_client_` is available before requesting reboot.
  // Continue from `PowerManagerBecameAvailable`. If availability state is
  // known, call is immediate and synchronous.
  power_manager_availability_observation_.Observe(power_manager_client_);
}

void DeviceCommandRebootJob::PowerManagerBecameAvailable(bool available) {
  power_manager_availability_observation_.Reset();

  if (!available) {
    LOG(ERROR) << "Power manager is not available. Cannot reboot.";
    return RunAsyncCallback(std::move(result_callback_), ResultType::kFailure,
                            FROM_HERE);
  }

  // The device is able to reboot immediately if it has no ongoing user session:
  // if it runs in kiosk mode or is on login screen.
  if (login_state_->IsKioskSession()) {
    return DoReboot(kKioskRebootDescription);
  }

  if (!login_state_->IsUserLoggedIn()) {
    return DoReboot(kLoginScreenRebootDescription);
  }

  RebootUserSession();
}

void DeviceCommandRebootJob::RebootUserSession() {
  if (user_session_delay_.is_zero()) {
    // Do not show the reboot notification if no delay.
    return OnRebootTimeoutExpired();
  }

  const auto reboot_time = clock_->Now() + user_session_delay_;
  in_session_notifications_scheduler_->SchedulePendingRebootNotifications(
      base::BindOnce(&DeviceCommandRebootJob::OnRebootButtonClicked,
                     weak_factory_.GetWeakPtr()),
      reboot_time, RebootNotificationsScheduler::Requester::kRebootCommand);
  in_session_reboot_timer_.Start(
      FROM_HERE, reboot_time,
      base::BindOnce(&DeviceCommandRebootJob::OnRebootTimeoutExpired,
                     weak_factory_.GetWeakPtr()));

  // TODO(b/265784089): Make reboot on user logout robust. If the browser
  // crashes, all the reboot information is gone while it should be preserved.
  session_termination_manager_->SetDeviceRebootOnSignoutForRemoteCommand(
      base::BindOnce(&DeviceCommandRebootJob::OnSignout,
                     weak_factory_.GetWeakPtr()));
}

void DeviceCommandRebootJob::OnSignout() {
  // `session_termination_manager_` will initiate the reboot, just report the
  // command finished.
  RunAsyncCallback(std::move(result_callback_), ResultType::kSuccess,
                   FROM_HERE);
}

void DeviceCommandRebootJob::OnRebootButtonClicked() {
  ResetTriggeringEvents();
  DoReboot(kUserSessionRebootDescription);
}

void DeviceCommandRebootJob::OnRebootTimeoutExpired() {
  ResetTriggeringEvents();
  in_session_notifications_scheduler_->SchedulePostRebootNotification();
  DoReboot(kUserSessionRebootDescription);
}

void DeviceCommandRebootJob::ResetTriggeringEvents() {
  in_session_notifications_scheduler_->CancelRebootNotifications(
      RebootNotificationsScheduler::Requester::kRebootCommand);
  in_session_reboot_timer_.Stop();
}

void DeviceCommandRebootJob::DoReboot(const std::string& reason) {
  DCHECK(result_callback_);

  // Posting the task with a callback just before reboot request does not
  // guarantee the callback reaching `RemoteCommandsService` and is very
  // unlikely to be reported to DMServer. So the callback is mostly used for
  // testing purposes.
  // The implementation relies on `RemoteCommandsQueue` running one command at
  // the time: two reboot commands cannot exist simultaneously and will not
  // compete for the notification. The
  // callback is called at the very end of execution in order to not to have two
  // commands executed simultaneously .
  // TODO(b/252980103): Come up with a mechanism to deliver the execution result
  // to DMServer.
  RunAsyncCallback(std::move(result_callback_), ResultType::kSuccess,
                   FROM_HERE);
  power_manager_client_->RequestRestart(
      power_manager::REQUEST_RESTART_REMOTE_ACTION_REBOOT, reason);
}

// static
void DeviceCommandRebootJob::RunAsyncCallback(CallbackWithResult callback,
                                              ResultType result,
                                              base::Location from_where) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      from_where, base::BindOnce(std::move(callback), result, std::nullopt));
}

}  // namespace policy