| // 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 |