chromium/ash/system/diagnostics/diagnostics_log_controller.cc

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

#include "ash/system/diagnostics/diagnostics_log_controller.h"

#include <memory>
#include <string>
#include <vector>

#include "ash/login_status.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/diagnostics/diagnostics_browser_delegate.h"
#include "ash/system/diagnostics/keyboard_input_log.h"
#include "ash/system/diagnostics/networking_log.h"
#include "ash/system/diagnostics/routine_log.h"
#include "ash/system/diagnostics/telemetry_log.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/sequence_checker.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "components/session_manager/session_manager_types.h"

namespace ash {
namespace diagnostics {

namespace {

DiagnosticsLogController* g_instance = nullptr;

// Default path for storing logs.
const char kDiaganosticsTmpDir[] = "/tmp/diagnostics";
const char kDiaganosticsDirName[] = "diagnostics";

// Session log headers and fallback content.
const char kRoutineLogSubsectionHeader[] = "--- Test Routines --- \n";
const char kSystemLogSectionHeader[] = "=== System === \n";
const char kNetworkingLogSectionHeader[] = "=== Networking === \n";
const char kKeyboardLogSectionHeader[] = "=== Keyboard === \n";
const char kNoRoutinesRun[] =
    "No routines of this type were run in the session.\n";

std::string GetRoutineResultsString(const std::string& results) {
  const std::string section_header =
      std::string(kRoutineLogSubsectionHeader) + "\n";
  if (results.empty()) {
    return section_header + kNoRoutinesRun;
  }

  return section_header + results;
}

// Determines if LoginStatus state update should trigger a reset of log
// pointers.
bool ShouldResetAndInitializeLogWritersForLoginStatus(
    LoginStatus previous_status,
    LoginStatus current_status) {
  if (previous_status == current_status)
    return false;

  switch (current_status) {
    case ash::LoginStatus::LOCKED:
      // User has not changed.
      return false;
    case LoginStatus::GUEST:
    case LoginStatus::PUBLIC:
    case LoginStatus::KIOSK_APP:
    case LoginStatus::USER:
    case LoginStatus::CHILD:
      // Do not reset if user has just unlocked screen.
      return previous_status != ash::LoginStatus::LOCKED;
    case LoginStatus::NOT_LOGGED_IN:
      // When status goes to not_logged_in we should clear existing logs.
      return true;
  }
}

// Determines if profile should be accessed with current session state.  If at
// sign-in screen, guest user, kiosk app, or before the profile has
// successfully loaded temporary path should be used for storing logs.
bool ShouldUseActiveUserProfileDir(session_manager::SessionState state,
                                   LoginStatus status) {
  return state == session_manager::SessionState::ACTIVE &&
         status == ash::LoginStatus::USER;
}

}  // namespace

DiagnosticsLogController::DiagnosticsLogController()
    : log_base_path_(kDiaganosticsTmpDir) {
  DCHECK_EQ(nullptr, g_instance);
  ash::Shell::Get()->session_controller()->AddObserver(this);
  g_instance = this;
  g_instance->previous_status_ =
      ash::Shell::Get()->session_controller()->login_status();
}

DiagnosticsLogController::~DiagnosticsLogController() {
  DCHECK_EQ(this, g_instance);
  ash::Shell::Get()->session_controller()->RemoveObserver(this);
  g_instance = nullptr;
}

// static
DiagnosticsLogController* DiagnosticsLogController::Get() {
  return g_instance;
}

// static
bool DiagnosticsLogController::IsInitialized() {
  return g_instance && g_instance->delegate_;
}

// static
void DiagnosticsLogController::Initialize(
    std::unique_ptr<DiagnosticsBrowserDelegate> delegate) {
  DCHECK(g_instance);
  DCHECK_CALLED_ON_VALID_SEQUENCE(g_instance->sequence_checker_);
  g_instance->delegate_ = std::move(delegate);
  g_instance->previous_status_ =
      ash::Shell::Get()->session_controller()->login_status();
  g_instance->ResetAndInitializeLogWriters();

  // Schedule removal of log directory.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&DiagnosticsLogController::RemoveDirectory,
                     g_instance->weak_ptr_factory_.GetWeakPtr(),
                     g_instance->log_base_path_));
}

std::string DiagnosticsLogController::GenerateSessionStringOnBlockingPool()
    const {
  std::vector<std::string> log_pieces;

  // Fetch system data from TelemetryLog.
  const std::string system_log_contents = telemetry_log_->GetContents();
  log_pieces.push_back(kSystemLogSectionHeader);
  if (!system_log_contents.empty()) {
    log_pieces.push_back(system_log_contents);
  }

  // Fetch system routines from RoutineLog.
  const std::string system_routines = routine_log_->GetContentsForCategory(
      RoutineLog::RoutineCategory::kSystem);
  // Add the routine section for the system category.
  log_pieces.push_back(GetRoutineResultsString(system_routines));

  // Add networking category.
  log_pieces.push_back(kNetworkingLogSectionHeader);

  // Add the network info section.
  log_pieces.push_back(networking_log_->GetNetworkInfo());

  // Add the routine section for the network category.
  const std::string network_routines = routine_log_->GetContentsForCategory(
      RoutineLog::RoutineCategory::kNetwork);
  log_pieces.push_back(GetRoutineResultsString(network_routines));

  // Add the network events section.
  log_pieces.push_back(networking_log_->GetNetworkEvents());

  std::string input_log_contents = keyboard_input_log_->GetLogContents();
  if (!input_log_contents.empty()) {
    log_pieces.push_back(kKeyboardLogSectionHeader);
    log_pieces.push_back(std::move(input_log_contents));
  }

  return base::JoinString(log_pieces, "\n");
}

bool DiagnosticsLogController::GenerateSessionLogOnBlockingPool(
    const base::FilePath& save_file_path) {
  DCHECK(!save_file_path.empty());
  return base::WriteFile(save_file_path, GenerateSessionStringOnBlockingPool());
}

void DiagnosticsLogController::ResetAndInitializeLogWriters() {
  if (!DiagnosticsLogController::IsInitialized()) {
    return;
  }

  ResetLogBasePath();
  keyboard_input_log_ = std::make_unique<KeyboardInputLog>(log_base_path_);
  networking_log_ = std::make_unique<NetworkingLog>(log_base_path_);
  routine_log_ = std::make_unique<RoutineLog>(log_base_path_);
  telemetry_log_ = std::make_unique<TelemetryLog>();
}

KeyboardInputLog& DiagnosticsLogController::GetKeyboardInputLog() {
  return *keyboard_input_log_;
}

NetworkingLog& DiagnosticsLogController::GetNetworkingLog() {
  return *networking_log_;
}

RoutineLog& DiagnosticsLogController::GetRoutineLog() {
  return *routine_log_;
}

TelemetryLog& DiagnosticsLogController::GetTelemetryLog() {
  return *telemetry_log_;
}

void DiagnosticsLogController::ResetLogBasePath() {
  const session_manager::SessionState state =
      ash::Shell::Get()->session_controller()->GetSessionState();
  // g_instance->previous_status_ is updated OnLoginStatusChanged after
  // ResetLogBasePath. To ensure we have the current status we need to query the
  // session_controller for it.
  const LoginStatus status =
      ash::Shell::Get()->session_controller()->login_status();

  // Check if there is an active user and profile is ready based on session and
  // login state.
  if (ShouldUseActiveUserProfileDir(state, status)) {
    base::FilePath user_dir = g_instance->delegate_->GetActiveUserProfileDir();

    // Update |log_base_path_| when path is non-empty. Otherwise fallback to
    // |kDiaganosticsTmpDir|.
    if (!user_dir.empty()) {
      g_instance->log_base_path_ = user_dir.Append(kDiaganosticsDirName);
      return;
    }
  }

  // Use diagnostics temporary path for Guest, KioskApp, and no user states.
  g_instance->log_base_path_ = base::FilePath(kDiaganosticsTmpDir);
}

void DiagnosticsLogController::OnLoginStatusChanged(LoginStatus login_status) {
  if (!DiagnosticsLogController::IsInitialized()) {
    return;
  }

  if (ShouldResetAndInitializeLogWritersForLoginStatus(
          g_instance->previous_status_, login_status)) {
    g_instance->ResetAndInitializeLogWriters();

    // Schedule removal of log directory as this should happen every time a user
    // logs in.
    base::ThreadPool::PostTask(
        FROM_HERE, {base::MayBlock()},
        base::BindOnce(&DiagnosticsLogController::RemoveDirectory,
                       g_instance->weak_ptr_factory_.GetWeakPtr(),
                       g_instance->log_base_path_));
  }

  g_instance->previous_status_ = login_status;
}

void DiagnosticsLogController::RemoveDirectory(const base::FilePath& path) {
  DCHECK(!path.empty());

  if (base::PathExists(path)) {
    base::DeletePathRecursively(path);
  }
}

void DiagnosticsLogController::SetKeyboardInputLogForTesting(
    std::unique_ptr<KeyboardInputLog> keyboard_input_log) {
  keyboard_input_log_ = std::move(keyboard_input_log);
}

void DiagnosticsLogController::SetNetworkingLogForTesting(
    std::unique_ptr<NetworkingLog> networking_log) {
  networking_log_ = std::move(networking_log);
}

void DiagnosticsLogController::SetRoutineLogForTesting(
    std::unique_ptr<RoutineLog> routine_log) {
  routine_log_ = std::move(routine_log);
}

void DiagnosticsLogController::SetTelemetryLogForTesting(
    std::unique_ptr<TelemetryLog> telemetry_log) {
  telemetry_log_ = std::move(telemetry_log);
}

}  // namespace diagnostics
}  // namespace ash