chromium/ash/webui/diagnostics_ui/backend/system/system_routine_controller.cc

// Copyright 2020 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 "ash/webui/diagnostics_ui/backend/system/system_routine_controller.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/system/diagnostics/diagnostics_log_controller.h"
#include "ash/system/diagnostics/routine_log.h"
#include "ash/webui/diagnostics_ui/backend/common/histogram_util.h"
#include "ash/webui/diagnostics_ui/backend/common/routine_properties.h"
#include "ash/webui/diagnostics_ui/backend/system/cros_healthd_helpers.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "chromeos/ash/services/cros_healthd/public/cpp/service_connection.h"
#include "chromeos/ash/services/cros_healthd/public/mojom/cros_healthd_diagnostics.mojom.h"
#include "chromeos/ash/services/cros_healthd/public/mojom/nullable_primitives.mojom.h"
#include "content/public/browser/device_service.h"
#include "services/device/public/mojom/wake_lock_provider.mojom.h"

namespace ash::diagnostics {

namespace {

namespace healthd = cros_healthd::mojom;

constexpr uint32_t kBatteryDurationInSeconds = 30;
constexpr uint32_t kBatteryChargeMinimumPercent = 0;
constexpr uint32_t kBatteryDischargeMaximumPercent = 100;
constexpr uint32_t kRoutineResultRefreshIntervalInSeconds = 1;

constexpr char kChargePercentKey[] = "chargePercent";
constexpr char kDischargePercentKey[] = "dischargePercent";
constexpr char kResultDetailsKey[] = "resultDetails";

const char kWakeLockReason[] = "DiagnosticsMemoryRoutine";

mojom::RoutineResultInfoPtr ConstructStandardRoutineResultInfoPtr(
    mojom::RoutineType type,
    mojom::StandardRoutineResult result) {
  auto routine_result = mojom::RoutineResult::NewSimpleResult(result);
  return mojom::RoutineResultInfo::New(type, std::move(routine_result));
}

// Converts a cros_healthd::mojom::DiagnosticRoutineStatusEnum to a
// mojom::StandardRoutineResult. Should only be called to construct the final
// response. Should not be called for in-progess statuses.
mojom::StandardRoutineResult TestStatusToResult(
    healthd::DiagnosticRoutineStatusEnum status) {
  switch (status) {
    case healthd::DiagnosticRoutineStatusEnum::kPassed:
      return mojom::StandardRoutineResult::kTestPassed;
    case healthd::DiagnosticRoutineStatusEnum::kFailed:
      return mojom::StandardRoutineResult::kTestFailed;
    case healthd::DiagnosticRoutineStatusEnum::kCancelled:
    case healthd::DiagnosticRoutineStatusEnum::kError:
      return mojom::StandardRoutineResult::kExecutionError;
    case healthd::DiagnosticRoutineStatusEnum::kFailedToStart:
    case healthd::DiagnosticRoutineStatusEnum::kUnsupported:
    case healthd::DiagnosticRoutineStatusEnum::kNotRun:
      return mojom::StandardRoutineResult::kUnableToRun;
    case healthd::DiagnosticRoutineStatusEnum::kReady:
    case healthd::DiagnosticRoutineStatusEnum::kRunning:
    case healthd::DiagnosticRoutineStatusEnum::kWaiting:
    case healthd::DiagnosticRoutineStatusEnum::kRemoved:
    case healthd::DiagnosticRoutineStatusEnum::kCancelling:
    case healthd::DiagnosticRoutineStatusEnum::kUnknown:
      NOTREACHED();
  }
}

mojom::RoutineResultInfoPtr ConstructPowerRoutineResultInfoPtr(
    mojom::RoutineType type,
    mojom::StandardRoutineResult result,
    double percent_change,
    uint32_t seconds_elapsed) {
  auto power_result =
      mojom::PowerRoutineResult::New(result, percent_change, seconds_elapsed);
  auto routine_result =
      mojom::RoutineResult::NewPowerResult(std::move(power_result));
  return mojom::RoutineResultInfo::New(type, std::move(routine_result));
}

bool IsPowerRoutine(mojom::RoutineType routine_type) {
  return routine_type == mojom::RoutineType::kBatteryCharge ||
         routine_type == mojom::RoutineType::kBatteryDischarge;
}

std::string ReadMojoHandleToJsonString(mojo::PlatformHandle handle) {
  base::File file(handle.ReleaseFD());
  std::vector<uint8_t> contents;
  contents.resize(file.GetLength());
  if (!file.ReadAndCheck(0, contents)) {
    return std::string();
  }
  return std::string(contents.begin(), contents.end());
}

bool IsLoggingEnabled() {
  return diagnostics::DiagnosticsLogController::IsInitialized();
}

}  // namespace

SystemRoutineController::SystemRoutineController() {
  inflight_routine_timer_ = std::make_unique<base::OneShotTimer>();
}

SystemRoutineController::~SystemRoutineController() {
  if (inflight_routine_runner_) {
    // Since SystemRoutineController is torn down at the same time as the
    // frontend, there's no guarantee that the disconnect handler will be
    // called. If there's a routine inflight, cancel it but do not pass a
    // callback.
    BindCrosHealthdDiagnosticsServiceIfNeccessary();
    diagnostics_service_->GetRoutineUpdate(
        inflight_routine_id_, healthd::DiagnosticRoutineCommandEnum::kCancel,
        /*should_include_output=*/false, base::DoNothing());
    if (IsLoggingEnabled() && inflight_routine_type_.has_value()) {
      DiagnosticsLogController::Get()->GetRoutineLog().LogRoutineCancelled(
          inflight_routine_type_.value());
    }
  }

  // Emit the total number of routines run.
  metrics::EmitRoutineRunCount(routine_count_);
}

void SystemRoutineController::RunRoutine(
    mojom::RoutineType type,
    mojo::PendingRemote<mojom::RoutineRunner> runner) {
  if (IsRoutineRunning()) {
    // If a routine is already running, alert the caller that we were unable
    // to start the routine.
    mojo::Remote<mojom::RoutineRunner> routine_runner(std::move(runner));
    auto result = ConstructStandardRoutineResultInfoPtr(
        type, mojom::StandardRoutineResult::kUnableToRun);
    routine_runner->OnRoutineResult(std::move(result));
    return;
  }

  ++routine_count_;

  inflight_routine_runner_ =
      mojo::Remote<mojom::RoutineRunner>(std::move(runner));
  inflight_routine_runner_.set_disconnect_handler(base::BindOnce(
      &SystemRoutineController::OnInflightRoutineRunnerDisconnected,
      base::Unretained(this)));
  ExecuteRoutine(type);
}

void SystemRoutineController::GetSupportedRoutines(
    GetSupportedRoutinesCallback callback) {
  if (!supported_routines_.empty()) {
    std::move(callback).Run(supported_routines_);
    return;
  }
  BindCrosHealthdDiagnosticsServiceIfNeccessary();
  diagnostics_service_->GetAvailableRoutines(
      base::BindOnce(&SystemRoutineController::OnAvailableRoutinesFetched,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void SystemRoutineController::BindInterface(
    mojo::PendingReceiver<mojom::SystemRoutineController> pending_receiver) {
  receiver_.reset();
  receiver_.Bind(std::move(pending_receiver));
  receiver_.set_disconnect_handler(
      base::BindOnce(&SystemRoutineController::OnBoundInterfaceDisconnect,
                     base::Unretained(this)));
}

bool SystemRoutineController::IsReceiverBoundForTesting() {
  return receiver_.is_bound();
}

void SystemRoutineController::OnBoundInterfaceDisconnect() {
  receiver_.reset();
}

void SystemRoutineController::OnAvailableRoutinesFetched(
    GetSupportedRoutinesCallback callback,
    const std::vector<healthd::DiagnosticRoutineEnum>& available_routines) {
  base::flat_set<healthd::DiagnosticRoutineEnum> healthd_routines(
      available_routines);
  for (size_t i = 0; i < kRoutinePropertiesLength; i++) {
    const RoutineProperties& routine = kRoutineProperties[i];
    if (base::Contains(healthd_routines, routine.healthd_type)) {
      supported_routines_.push_back(routine.type);
    }
  }
  std::move(callback).Run(supported_routines_);
}

void SystemRoutineController::ExecuteRoutine(mojom::RoutineType routine_type) {
  BindCrosHealthdDiagnosticsServiceIfNeccessary();

  switch (routine_type) {
    case mojom::RoutineType::kArcDnsResolution:
      diagnostics_service_->RunArcDnsResolutionRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;

    case mojom::RoutineType::kArcHttp:
      diagnostics_service_->RunArcHttpRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;

    case mojom::RoutineType::kArcPing:
      diagnostics_service_->RunArcPingRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;

    case mojom::RoutineType::kBatteryCharge:
      diagnostics_service_->RunBatteryChargeRoutine(
          GetExpectedRoutineDurationInSeconds(routine_type),
          kBatteryChargeMinimumPercent,
          base::BindOnce(&SystemRoutineController::OnPowerRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));

      break;
    case mojom::RoutineType::kBatteryDischarge:
      diagnostics_service_->RunBatteryDischargeRoutine(
          GetExpectedRoutineDurationInSeconds(routine_type),
          kBatteryDischargeMaximumPercent,
          base::BindOnce(&SystemRoutineController::OnPowerRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));

      break;
    case mojom::RoutineType::kCaptivePortal:
      diagnostics_service_->RunCaptivePortalRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kCpuCache:
      diagnostics_service_->RunCpuCacheRoutine(
          healthd::NullableUint32::New(
              GetExpectedRoutineDurationInSeconds(routine_type)),
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kCpuFloatingPoint:
      diagnostics_service_->RunFloatingPointAccuracyRoutine(
          healthd::NullableUint32::New(
              GetExpectedRoutineDurationInSeconds(routine_type)),
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kCpuPrime:
      diagnostics_service_->RunPrimeSearchRoutine(
          healthd::NullableUint32::New(
              GetExpectedRoutineDurationInSeconds(routine_type)),
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kCpuStress:
      diagnostics_service_->RunCpuStressRoutine(
          healthd::NullableUint32::New(
              GetExpectedRoutineDurationInSeconds(routine_type)),
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kDnsLatency:
      diagnostics_service_->RunDnsLatencyRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kDnsResolution:
      diagnostics_service_->RunDnsResolutionRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kDnsResolverPresent:
      diagnostics_service_->RunDnsResolverPresentRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kGatewayCanBePinged:
      diagnostics_service_->RunGatewayCanBePingedRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kHasSecureWiFiConnection:
      diagnostics_service_->RunHasSecureWiFiConnectionRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kHttpFirewall:
      diagnostics_service_->RunHttpFirewallRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kHttpsFirewall:
      diagnostics_service_->RunHttpsFirewallRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kHttpsLatency:
      diagnostics_service_->RunHttpsLatencyRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kLanConnectivity:
      diagnostics_service_->RunLanConnectivityRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
    case mojom::RoutineType::kMemory:
      AcquireWakeLock();
      diagnostics_service_->RunMemoryRoutine(
          std::nullopt,
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      memory_routine_start_timestamp_ = base::Time::Now();
      break;
    case mojom::RoutineType::kSignalStrength:
      diagnostics_service_->RunSignalStrengthRoutine(
          base::BindOnce(&SystemRoutineController::OnRoutineStarted,
                         weak_factory_.GetWeakPtr(), routine_type));
      break;
  }
  if (IsLoggingEnabled()) {
    DiagnosticsLogController::Get()->GetRoutineLog().LogRoutineStarted(
        routine_type);
  }
}

void SystemRoutineController::OnRoutineStarted(
    mojom::RoutineType routine_type,
    healthd::RunRoutineResponsePtr response_ptr) {
  DCHECK(!IsPowerRoutine(routine_type));
  // Check for error conditions.
  // TODO(baileyberro): Handle additional statuses.
  if (response_ptr->status ==
          healthd::DiagnosticRoutineStatusEnum::kFailedToStart ||
      response_ptr->id == healthd::kFailedToStartId) {
    OnStandardRoutineResult(routine_type,
                            TestStatusToResult(response_ptr->status));
    return;
  }
  DCHECK_EQ(healthd::DiagnosticRoutineStatusEnum::kRunning,
            response_ptr->status);

  DCHECK_EQ(kInvalidRoutineId, inflight_routine_id_);
  inflight_routine_id_ = response_ptr->id;
  inflight_routine_type_ = routine_type;

  // Sleep for the length of the test using a one-shot timer, then start
  // querying again for status.
  ScheduleCheckRoutineStatus(GetExpectedRoutineDurationInSeconds(routine_type),
                             routine_type);
}

void SystemRoutineController::OnPowerRoutineStarted(
    mojom::RoutineType routine_type,
    healthd::RunRoutineResponsePtr response_ptr) {
  DCHECK(IsPowerRoutine(routine_type));
  // TODO(baileyberro): Handle additional statuses.
  if (response_ptr->status != healthd::DiagnosticRoutineStatusEnum::kWaiting) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    return;
  }

  DCHECK_EQ(kInvalidRoutineId, inflight_routine_id_);
  inflight_routine_id_ = response_ptr->id;
  inflight_routine_type_ = routine_type;

  ContinuePowerRoutine(routine_type);
}

void SystemRoutineController::ContinuePowerRoutine(
    mojom::RoutineType routine_type) {
  DCHECK(IsPowerRoutine(routine_type));

  BindCrosHealthdDiagnosticsServiceIfNeccessary();
  diagnostics_service_->GetRoutineUpdate(
      inflight_routine_id_, healthd::DiagnosticRoutineCommandEnum::kContinue,
      /*should_include_output=*/true,
      base::BindOnce(&SystemRoutineController::OnPowerRoutineContinued,
                     weak_factory_.GetWeakPtr(), routine_type));
}

void SystemRoutineController::OnPowerRoutineContinued(
    mojom::RoutineType routine_type,
    healthd::RoutineUpdatePtr update_ptr) {
  DCHECK(IsPowerRoutine(routine_type));

  const healthd::NonInteractiveRoutineUpdate* update =
      GetNonInteractiveRoutineUpdate(*update_ptr);

  if (!update ||
      update->status != healthd::DiagnosticRoutineStatusEnum::kRunning) {
    DVLOG(2) << "Failed to resume power routine.";
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    return;
  }

  ScheduleCheckRoutineStatus(GetExpectedRoutineDurationInSeconds(routine_type),
                             routine_type);
}

void SystemRoutineController::CheckRoutineStatus(
    mojom::RoutineType routine_type) {
  DCHECK_NE(kInvalidRoutineId, inflight_routine_id_);
  BindCrosHealthdDiagnosticsServiceIfNeccessary();
  const bool should_include_output = IsPowerRoutine(routine_type);
  diagnostics_service_->GetRoutineUpdate(
      inflight_routine_id_, healthd::DiagnosticRoutineCommandEnum::kGetStatus,
      should_include_output,
      base::BindOnce(&SystemRoutineController::OnRoutineStatusUpdated,
                     weak_factory_.GetWeakPtr(), routine_type));
}

void SystemRoutineController::OnRoutineStatusUpdated(
    mojom::RoutineType routine_type,
    healthd::RoutineUpdatePtr update_ptr) {
  if (IsPowerRoutine(routine_type)) {
    HandlePowerRoutineStatusUpdate(routine_type, std::move(update_ptr));
    return;
  }

  const healthd::NonInteractiveRoutineUpdate* update =
      GetNonInteractiveRoutineUpdate(*update_ptr);

  if (!update) {
    DVLOG(2) << "Invalid routine update";
    OnStandardRoutineResult(routine_type,
                            mojom::StandardRoutineResult::kExecutionError);
    return;
  }

  const healthd::DiagnosticRoutineStatusEnum status = update->status;

  switch (status) {
    case healthd::DiagnosticRoutineStatusEnum::kRunning:
      // If still running, continue to repoll until it is finished.
      // TODO(baileyberro): Consider adding a timeout mechanism.
      ScheduleCheckRoutineStatus(kRoutineResultRefreshIntervalInSeconds,
                                 routine_type);
      return;
    case healthd::DiagnosticRoutineStatusEnum::kPassed:
    case healthd::DiagnosticRoutineStatusEnum::kFailed:
      OnStandardRoutineResult(routine_type, TestStatusToResult(status));
      return;
    case healthd::DiagnosticRoutineStatusEnum::kCancelled:
    case healthd::DiagnosticRoutineStatusEnum::kError:
    case healthd::DiagnosticRoutineStatusEnum::kFailedToStart:
    case healthd::DiagnosticRoutineStatusEnum::kUnsupported:
    case healthd::DiagnosticRoutineStatusEnum::kReady:
    case healthd::DiagnosticRoutineStatusEnum::kWaiting:
    case healthd::DiagnosticRoutineStatusEnum::kRemoved:
    case healthd::DiagnosticRoutineStatusEnum::kCancelling:
    case healthd::DiagnosticRoutineStatusEnum::kNotRun:
    case healthd::DiagnosticRoutineStatusEnum::kUnknown:
      // Any other reason, report failure.
      DVLOG(2) << "Routine failed: " << update->status_message;
      OnStandardRoutineResult(routine_type, TestStatusToResult(status));
      return;
  }
}

void SystemRoutineController::HandlePowerRoutineStatusUpdate(
    mojom ::RoutineType routine_type,
    healthd::RoutineUpdatePtr update_ptr) {
  DCHECK(IsPowerRoutine(routine_type));

  const healthd::NonInteractiveRoutineUpdate* update =
      GetNonInteractiveRoutineUpdate(*update_ptr);

  if (!update) {
    DVLOG(2) << "Invalid routine update";
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    return;
  }

  const healthd::DiagnosticRoutineStatusEnum status = update->status;

  // If still running, continue to repoll until it is finished.
  // TODO(baileyberro): Consider adding a timeout mechanism.
  if (status == healthd::DiagnosticRoutineStatusEnum::kRunning) {
    ScheduleCheckRoutineStatus(kRoutineResultRefreshIntervalInSeconds,
                               routine_type);
    return;
  }

  // If test passed, report result.
  if (status == healthd::DiagnosticRoutineStatusEnum::kPassed) {
    ParsePowerRoutineResult(routine_type,
                            mojom::StandardRoutineResult::kTestPassed,
                            std::move(update_ptr->output));
    return;
  }

  // If test failed, report result.
  if (status == healthd::DiagnosticRoutineStatusEnum::kFailed) {
    ParsePowerRoutineResult(routine_type,
                            mojom::StandardRoutineResult::kTestFailed,
                            std::move(update_ptr->output));
    return;
  }

  // Any other reason, report failure.
  DVLOG(2) << "Routine failed: " << update->status_message;
  OnPowerRoutineResult(routine_type,
                       mojom::StandardRoutineResult::kExecutionError,
                       /*percent_change=*/0, /*seconds_elapsed=*/0);
}

bool SystemRoutineController::IsRoutineRunning() const {
  return inflight_routine_runner_.is_bound();
}

void SystemRoutineController::ScheduleCheckRoutineStatus(
    uint32_t duration_in_seconds,
    mojom::RoutineType routine_type) {
  inflight_routine_timer_->Start(
      FROM_HERE, base::Seconds(duration_in_seconds),
      base::BindOnce(&SystemRoutineController::CheckRoutineStatus,
                     weak_factory_.GetWeakPtr(), routine_type));
}

void SystemRoutineController::ParsePowerRoutineResult(
    mojom::RoutineType routine_type,
    mojom::StandardRoutineResult result,
    mojo::ScopedHandle output_handle) {
  if (!output_handle.is_valid()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    return;
  }

  mojo::PlatformHandle platform_handle =
      mojo::UnwrapPlatformHandle(std::move(output_handle));
  if (!platform_handle.is_valid()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    return;
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&ReadMojoHandleToJsonString, std::move(platform_handle)),
      base::BindOnce(&SystemRoutineController::OnPowerRoutineResultFetched,
                     weak_factory_.GetWeakPtr(), routine_type));
}

void SystemRoutineController::OnPowerRoutineResultFetched(
    mojom::RoutineType routine_type,
    const std::string& file_contents) {
  if (file_contents.empty()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    DVLOG(2) << "Empty Power Routine Result File.";
    return;
  }

  data_decoder::DataDecoder::ParseJsonIsolated(
      file_contents,
      base::BindOnce(&SystemRoutineController::OnPowerRoutineJsonParsed,
                     weak_factory_.GetWeakPtr(), routine_type));
  return;
}

void SystemRoutineController::OnPowerRoutineJsonParsed(
    mojom::RoutineType routine_type,
    data_decoder::DataDecoder::ValueOrError result) {
  if (!result.has_value()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    DVLOG(2) << "JSON parsing failed: " << result.error();
    return;
  }

  if (!result->is_dict()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    DVLOG(2) << "Malformed Routine Result File.";
    return;
  }

  const base::Value::Dict& parsed_json = result->GetDict();
  const base::Value::Dict* result_details_dict =
      parsed_json.FindDict(kResultDetailsKey);
  if (!result_details_dict) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    DVLOG(2) << "Malformed Routine Result File.";
    return;
  }

  std::optional<double> charge_percent_opt =
      routine_type == mojom::RoutineType::kBatteryCharge
          ? result_details_dict->FindDouble(kChargePercentKey)
          : result_details_dict->FindDouble(kDischargePercentKey);
  if (!charge_percent_opt.has_value()) {
    OnPowerRoutineResult(routine_type,
                         mojom::StandardRoutineResult::kExecutionError,
                         /*percent_change=*/0, /*seconds_elapsed=*/0);
    DVLOG(2) << "Malformed Routine Result File.";
    return;
  }

  OnPowerRoutineResult(routine_type, mojom::StandardRoutineResult::kTestPassed,
                       *charge_percent_opt, kBatteryDurationInSeconds);
}

void SystemRoutineController::OnStandardRoutineResult(
    mojom::RoutineType routine_type,
    mojom::StandardRoutineResult result) {
  DCHECK(IsRoutineRunning());
  auto result_info =
      ConstructStandardRoutineResultInfoPtr(routine_type, result);
  SendRoutineResult(std::move(result_info));
  metrics::EmitRoutineResult(routine_type, result);
  if (routine_type == mojom::RoutineType::kMemory) {
    ReleaseWakeLock();
    metrics::EmitMemoryRoutineDuration(base::Time::Now() -
                                       memory_routine_start_timestamp_);
  }
  if (IsLoggingEnabled()) {
    DiagnosticsLogController::Get()->GetRoutineLog().LogRoutineCompleted(
        routine_type, result);
  }
}

void SystemRoutineController::OnPowerRoutineResult(
    mojom::RoutineType routine_type,
    mojom::StandardRoutineResult result,
    double percent_change,
    uint32_t seconds_elapsed) {
  DCHECK(IsRoutineRunning());
  auto result_info = ConstructPowerRoutineResultInfoPtr(
      routine_type, result, percent_change, seconds_elapsed);
  SendRoutineResult(std::move(result_info));
  metrics::EmitRoutineResult(routine_type, result);
  if (IsLoggingEnabled()) {
    DiagnosticsLogController::Get()->GetRoutineLog().LogRoutineCompleted(
        routine_type, result);
  }
}

void SystemRoutineController::SendRoutineResult(
    mojom::RoutineResultInfoPtr result_info) {
  if (inflight_routine_runner_ && !result_info.is_null() &&
      !result_info->result.is_null()) {
    inflight_routine_runner_->OnRoutineResult(std::move(result_info));
  } else {
    LOG(ERROR) << (inflight_routine_runner_
                       ? "Do not send routine result since it's null."
                       : "Not able to call OnRoutineResult() since the "
                         "inflight_routine_runner_ is null.");
  }

  inflight_routine_runner_.reset();
  inflight_routine_id_ = kInvalidRoutineId;
  inflight_routine_type_.reset();
}

void SystemRoutineController::BindCrosHealthdDiagnosticsServiceIfNeccessary() {
  if (!diagnostics_service_ || !diagnostics_service_.is_connected()) {
    cros_healthd::ServiceConnection::GetInstance()->BindDiagnosticsService(
        diagnostics_service_.BindNewPipeAndPassReceiver());
    diagnostics_service_.set_disconnect_handler(base::BindOnce(
        &SystemRoutineController::OnDiagnosticsServiceDisconnected,
        weak_factory_.GetWeakPtr()));
  }
}

void SystemRoutineController::OnDiagnosticsServiceDisconnected() {
  diagnostics_service_.reset();
}

void SystemRoutineController::OnInflightRoutineRunnerDisconnected() {
  // Reset `inflight_routine_runner_` since the other side of the pipe is
  // already disconnected.
  inflight_routine_runner_.reset();

  // Stop `inflight_routine_timer_` so that we do not attempt to fetch the
  // status of a cancelled routine.
  inflight_routine_timer_->Stop();

  // Release `wake_lock_` if necessary.
  if (wake_lock_) {
    ReleaseWakeLock();
  }

  // Make a best effort attempt to remove the routine.
  BindCrosHealthdDiagnosticsServiceIfNeccessary();
  diagnostics_service_->GetRoutineUpdate(
      inflight_routine_id_, healthd::DiagnosticRoutineCommandEnum::kCancel,
      /*should_include_output=*/false,
      base::BindOnce(&SystemRoutineController::OnRoutineCancelAttempted,
                     weak_factory_.GetWeakPtr()));

  // Reset `inflight_routine_id_` to maintain invariant.
  inflight_routine_id_ = kInvalidRoutineId;

  if (IsLoggingEnabled() && inflight_routine_type_.has_value()) {
    DiagnosticsLogController::Get()->GetRoutineLog().LogRoutineCancelled(
        inflight_routine_type_.value());
  }
}

void SystemRoutineController::OnRoutineCancelAttempted(
    healthd::RoutineUpdatePtr update_ptr) {
  const healthd::NonInteractiveRoutineUpdate* update =
      GetNonInteractiveRoutineUpdate(*update_ptr);

  if (!update ||
      update->status != healthd::DiagnosticRoutineStatusEnum::kCancelled) {
    DVLOG(2) << "Failed to cancel routine.";
    return;
  }
}

void SystemRoutineController::AcquireWakeLock() {
  if (!wake_lock_) {
    if (!wake_lock_provider_) {
      content::GetDeviceService().BindWakeLockProvider(
          wake_lock_provider_.BindNewPipeAndPassReceiver());
    }

    wake_lock_provider_->GetWakeLockWithoutContext(
        device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming,
        device::mojom::WakeLockReason::kOther, kWakeLockReason,
        wake_lock_.BindNewPipeAndPassReceiver());
  }

  wake_lock_->RequestWakeLock();
}

void SystemRoutineController::ReleaseWakeLock() {
  DCHECK(wake_lock_);
  wake_lock_->CancelWakeLock();
}

}  // namespace ash::diagnostics