chromium/chrome/browser/ash/policy/scheduled_task_handler/device_scheduled_reboot_handler.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.

#include "chrome/browser/ash/policy/scheduled_task_handler/device_scheduled_reboot_handler.h"

#include <time.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/rand_util.h"
#include "base/system/sys_info.h"
#include "base/values.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/scheduled_task_util.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/scoped_wake_lock.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "chromeos/ash/components/settings/timezone_settings.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/user_manager/user_manager.h"
#include "third_party/cros_system_api/dbus/power_manager/dbus-constants.h"

namespace policy {

namespace {

// Description associated with requesting restart on timer expired.
constexpr char kRebootDescriptionOnTimerExpired[] = "device scheduled reboot";

// Description associated with requesting restart on button click.
constexpr char kRebootDescriptionOnButtonClicked[] = "reboot button clicked";

// Reason associated to acquire |ScopedWakeLock|.
constexpr char kWakeLockReason[] = "DeviceScheduledRebootHandler";

// Task name used for parsing ScheduledTaskData.
constexpr char kTaskTimeFieldName[] = "reboot_time";

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

}  // namespace

constexpr char DeviceScheduledRebootHandler::kRebootTimerTag[];

DeviceScheduledRebootHandler::DeviceScheduledRebootHandler(
    ash::CrosSettings* cros_settings,
    std::unique_ptr<ScheduledTaskExecutor> scheduled_task_executor,
    RebootNotificationsScheduler* notifications_scheduler)
    : DeviceScheduledRebootHandler(cros_settings,
                                   std::move(scheduled_task_executor),
                                   notifications_scheduler,
                                   base::BindRepeating(GetBootTime)) {}

DeviceScheduledRebootHandler::DeviceScheduledRebootHandler(
    ash::CrosSettings* cros_settings,
    std::unique_ptr<ScheduledTaskExecutor> scheduled_task_executor,
    RebootNotificationsScheduler* notifications_scheduler,
    GetBootTimeCallback get_boot_time_callback)
    : cros_settings_(cros_settings),
      cros_settings_subscription_(cros_settings_->AddSettingsObserver(
          ash::kDeviceScheduledReboot,
          base::BindRepeating(
              &DeviceScheduledRebootHandler::OnScheduledRebootDataChanged,
              base::Unretained(this)))),
      scheduled_task_executor_(std::move(scheduled_task_executor)),
      notifications_scheduler_(notifications_scheduler),
      get_boot_time_callback_(std::move(get_boot_time_callback)) {
  DCHECK(get_boot_time_callback_);

  ash::system::TimezoneSettings::GetInstance()->AddObserver(this);
  auto* power_manager_client = chromeos::PowerManagerClient::Get();
  if (power_manager_client) {
    observation_.Observe(power_manager_client);
  }
}

DeviceScheduledRebootHandler::~DeviceScheduledRebootHandler() {
  observation_.Reset();
  ash::system::TimezoneSettings::GetInstance()->RemoveObserver(this);
}

void DeviceScheduledRebootHandler::TimezoneChanged(
    const icu::TimeZone& time_zone) {
  // Anytime the time zone changes,
  // |scheduled_reboot_data_->next_scheduled_task_time_ticks| needs to be reset,
  // as it would be incorrect in the context of a new time zone. For this
  // purpose, treat it as a new policy and call |OnScheduledRebootDataChanged|
  // instead of |StartRebootTimer| directly.
  OnScheduledRebootDataChanged();
}

void DeviceScheduledRebootHandler::PowerManagerBecameAvailable(bool available) {
  if (!available) {
    LOG(ERROR) << "Power manager service is not available. Not possible to "
                  "schedule reboot.";
    ResetState();
    return;
  }
  // Check if policy already exists.
  OnScheduledRebootDataChanged();
}

void DeviceScheduledRebootHandler::SetRebootDelayForTest(
    const base::TimeDelta& reboot_delay) {
  reboot_delay_for_testing_ = reboot_delay;
}

std::optional<ScheduledTaskExecutor::ScheduledTaskData>
DeviceScheduledRebootHandler::GetScheduledRebootDataForTest() const {
  return scheduled_reboot_data_;
}

bool DeviceScheduledRebootHandler::IsRebootSkippedForTest() const {
  return skip_reboot_;
}

void DeviceScheduledRebootHandler::OnRebootTimerExpired() {
  // If no policy exists, state should have been reset and this callback
  // shouldn't have fired.
  DCHECK(scheduled_reboot_data_);

  // Always request restart if the device is in the kiosk mode or on the sign-in
  // screen. Once the device has rebooted, the handler will be created again and
  // the reboot will be rescheduled.
  if (user_manager::UserManager::Get()->IsLoggedInAsAnyKioskApp()) {
    RebootDevice(kRebootDescriptionOnTimerExpired);
    return;
  }

  // If the device is on the sign-in screen, skip reboot only if the grace
  // period is applied.
  if (!skip_reboot_ && !user_manager::UserManager::Get()->IsUserLoggedIn() &&
      base::FeatureList::IsEnabled(
          ash::features::kDeviceForceScheduledReboot)) {
    RebootDevice(kRebootDescriptionOnTimerExpired);
    return;
  }

  // If the device is not in the kiosk mode or on the sign-in screen, check if
  // the |kDeviceForceScheduledReboot| feature flag is enabled and if we should
  // not skip reboot due to grace period applied.
  if (!skip_reboot_ && base::FeatureList::IsEnabled(
                           ash::features::kDeviceForceScheduledReboot)) {
    // Schedule post reboot notification for the user in session.
    notifications_scheduler_->SchedulePostRebootNotification();
    RebootDevice(kRebootDescriptionOnTimerExpired);
    return;
  }

  // Start the timer again and try for the reboot next time on schedule.
  skip_reboot_ = false;
  StartRebootTimer();
}

void DeviceScheduledRebootHandler::OnRebootButtonClicked() {
  RebootDevice(kRebootDescriptionOnButtonClicked);
}

void DeviceScheduledRebootHandler::OnScheduledRebootDataChanged() {
  // If the policy is removed then reset all state including any existing
  // scheduled reboots.
  const base::Value* value =
      cros_settings_->GetPref(ash::kDeviceScheduledReboot);
  if (!value) {
    ResetState();
    return;
  }

  // Keep any old policy timers running if a new policy is ill-formed and can't
  // be used to set a new timer.
  std::optional<ScheduledTaskExecutor::ScheduledTaskData>
      scheduled_reboot_data =
          scheduled_task_util::ParseScheduledTask(*value, kTaskTimeFieldName);
  if (!scheduled_reboot_data) {
    LOG(ERROR) << "Failed to parse policy";
    return;
  }

  // Policy has been updated, calculate and set the timer again.
  scheduled_reboot_data_ = std::move(scheduled_reboot_data);
  StartRebootTimer();
}

void DeviceScheduledRebootHandler::StartRebootTimer() {
  // The device shouldn't suspend while calculating time ticks and setting the
  // timer for the next reboot. Otherwise the next reboot timer will
  // be inaccurately scheduled. Hence, a wake lock must always be held for this
  // entire task.
  // Always add delay to scheduled task time.
  scheduled_task_executor_->Start(
      &scheduled_reboot_data_.value(),
      base::BindOnce(
          &DeviceScheduledRebootHandler::OnRebootTimerStartResult,
          base::Unretained(this),
          ScopedWakeLock(device::mojom::WakeLockType::kPreventAppSuspension,
                         kWakeLockReason)),
      base::BindOnce(&DeviceScheduledRebootHandler::OnRebootTimerExpired,
                     base::Unretained(this)),
      GetExternalDelay());
}

void DeviceScheduledRebootHandler::OnRebootTimerStartResult(
    ScopedWakeLock scoped_wake_lock,
    bool result) {
  // If reboot timer failed to start, reset notifications if scheduled and
  // |scheduled_reboot_data_|. The reboot will be scheduled again when the new
  // policy comes or Chrome is restarted.
  if (!result) {
    LOG(ERROR) << "Failed to start reboot timer";
    notifications_scheduler_->CancelRebootNotifications(
        RebootNotificationsScheduler::Requester::kScheduledRebootPolicy);
    skip_reboot_ = false;
    scheduled_reboot_data_ = std::nullopt;
    return;
  }

  skip_reboot_ = scheduled_task_util::ShouldSkipRebootDueToGracePeriod(
      get_boot_time_callback_.Run(),
      scheduled_task_executor_->GetScheduledTaskTime());

  // If the flag is enabled, schedule reboot notification and dialog.
  if (base::FeatureList::IsEnabled(
          ash::features::kDeviceForceScheduledReboot)) {
    if (!skip_reboot_) {
      notifications_scheduler_->SchedulePendingRebootNotifications(
          base::BindOnce(&DeviceScheduledRebootHandler::OnRebootButtonClicked,
                         base::Unretained(this)),
          scheduled_task_executor_->GetScheduledTaskTime(),
          RebootNotificationsScheduler::Requester::kScheduledRebootPolicy);
    }
  }
}

void DeviceScheduledRebootHandler::ResetState() {
  notifications_scheduler_->CancelRebootNotifications(
      RebootNotificationsScheduler::Requester::kScheduledRebootPolicy);
  scheduled_task_executor_->Reset();
  skip_reboot_ = false;
  scheduled_reboot_data_ = std::nullopt;
}

const base::TimeDelta DeviceScheduledRebootHandler::GetExternalDelay() const {
  return reboot_delay_for_testing_.has_value()
             ? reboot_delay_for_testing_.value()
             : base::RandTimeDeltaUpTo(
                   ash::features::kDeviceForceScheduledRebootMaxDelay.Get());
}

void DeviceScheduledRebootHandler::RebootDevice(
    const std::string& reboot_description) const {
  chromeos::PowerManagerClient::Get()->RequestRestart(
      power_manager::REQUEST_RESTART_SCHEDULED_REBOOT_POLICY,
      reboot_description);
}

}  // namespace policy