| // 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 "chrome/browser/chromeos/app_mode/kiosk_metrics_service.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/check.h" |
| #include "base/command_line.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/values_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/syslog_logging.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/dbus/power/power_manager_client.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/user_manager/user_manager.h" |
| |
| namespace chromeos { |
| |
| namespace { |
| |
| // Info on crash report locations: |
| // docs/website/site/chromium-os/packages/crash-reporting/faq/index.md |
| const constexpr char* kCrashDirs[] = { |
| "/home/chronos/crash", // crashes outside user session. may happen on |
| // chromium shutdown |
| "/home/chronos/user/crash" // crashes inside user/kiosk session |
| }; |
| |
| bool IsRestoredSession() { |
| // Return true for a kiosk session restored after crash. |
| // The kiosk session gets restored to a state that was prior to crash: |
| // * no --login-manager command line flag, since no login screen is shown |
| // in the middle of a kiosk session. |
| // * --login-user command line flag is present, because the session is |
| // re-started in the middle and kiosk profile is already logged in. |
| return !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| ash::switches::kLoginManager) && |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| ash::switches::kLoginUser); |
| } |
| |
| // Returns true if there is a new crash in `crash_dirs` after |
| // `previous_start_time`. |
| // |
| // crash_dirs - the list of known directories with crash related files. |
| // previous_start_time - the start time of the previous kiosk session that is |
| // suspected to end with a crash. |
| bool IsPreviousKioskSessionCrashed(const std::vector<std::string>& crash_dirs, |
| const base::Time& previous_start_time) { |
| for (const auto& crash_file_path : crash_dirs) { |
| if (!base::PathExists(base::FilePath(crash_file_path))) { |
| continue; |
| } |
| base::FileEnumerator enumerator( |
| base::FilePath(crash_file_path), /*recursive=*/true, |
| base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES); |
| while (!enumerator.Next().empty()) { |
| if (enumerator.GetInfo().GetLastModifiedTime() > previous_start_time) { |
| // A new crash after `previous_start_time`. |
| return true; |
| } |
| } |
| } |
| // No new crashes in `crash_dirs`. |
| return false; |
| } |
| |
| void ClearMetricFromPrefs(const std::string& metric_name, PrefService* prefs) { |
| ScopedDictPrefUpdate(prefs, prefs::kKioskMetrics)->Remove(metric_name); |
| prefs->CommitPendingWrite(base::DoNothing(), base::DoNothing()); |
| } |
| |
| bool IsFirstSessionAfterReboot() { |
| return user_manager::UserManager::Get()->IsFirstExecAfterBoot(); |
| } |
| |
| KioskSessionRestartReason RestartReasonWithRebootInfo( |
| const KioskSessionRestartReason& initial_reason) { |
| switch (initial_reason) { |
| case KioskSessionRestartReason::kStopped: |
| return IsFirstSessionAfterReboot() |
| ? KioskSessionRestartReason::kStoppedWithReboot |
| : KioskSessionRestartReason::kStopped; |
| case KioskSessionRestartReason::kCrashed: |
| return IsFirstSessionAfterReboot() |
| ? KioskSessionRestartReason::kCrashedWithReboot |
| : KioskSessionRestartReason::kCrashed; |
| case KioskSessionRestartReason::kLocalStateWasNotSaved: |
| return IsFirstSessionAfterReboot() |
| ? KioskSessionRestartReason::kLocalStateWasNotSavedWithReboot |
| : KioskSessionRestartReason::kLocalStateWasNotSaved; |
| case KioskSessionRestartReason::kPluginCrashed: |
| return IsFirstSessionAfterReboot() |
| ? KioskSessionRestartReason::kPluginCrashedWithReboot |
| : KioskSessionRestartReason::kPluginCrashed; |
| case KioskSessionRestartReason::kPluginHung: |
| return IsFirstSessionAfterReboot() |
| ? KioskSessionRestartReason::kPluginHungWithReboot |
| : KioskSessionRestartReason::kPluginHung; |
| case KioskSessionRestartReason::kStoppedWithReboot: |
| case KioskSessionRestartReason::kCrashedWithReboot: |
| case KioskSessionRestartReason::kPluginCrashedWithReboot: |
| case KioskSessionRestartReason::kPluginHungWithReboot: |
| case KioskSessionRestartReason::kRebootPolicy: |
| case KioskSessionRestartReason::kRemoteActionReboot: |
| case KioskSessionRestartReason::kRestartApi: |
| case KioskSessionRestartReason::kLocalStateWasNotSavedWithReboot: |
| return initial_reason; |
| } |
| } |
| |
| KioskSessionRestartReason ConvertSessionEndReasonToSessionRestartReason( |
| const KioskSessionEndReason& session_end_reason) { |
| switch (session_end_reason) { |
| case KioskSessionEndReason::kStopped: |
| return RestartReasonWithRebootInfo(KioskSessionRestartReason::kStopped); |
| case KioskSessionEndReason::kRebootPolicy: |
| return KioskSessionRestartReason::kRebootPolicy; |
| case KioskSessionEndReason::kRemoteActionReboot: |
| return KioskSessionRestartReason::kRemoteActionReboot; |
| case KioskSessionEndReason::kRestartApi: |
| return KioskSessionRestartReason::kRestartApi; |
| case KioskSessionEndReason::kPluginCrashed: |
| return RestartReasonWithRebootInfo( |
| KioskSessionRestartReason::kPluginCrashed); |
| case KioskSessionEndReason::kPluginHung: |
| return RestartReasonWithRebootInfo( |
| KioskSessionRestartReason::kPluginHung); |
| } |
| } |
| |
| // If the session termination reason was not saved, returns an empty optional. |
| std::optional<KioskSessionEndReason> GetSessionEndReason( |
| const PrefService* prefs) { |
| const base::Value::Dict& metrics_dict = prefs->GetDict(prefs::kKioskMetrics); |
| const auto* kiosk_session_stop_reason_value = |
| metrics_dict.Find(kKioskSessionEndReason); |
| if (!kiosk_session_stop_reason_value) { |
| return std::nullopt; |
| } |
| auto kiosk_session_stop_reason = kiosk_session_stop_reason_value->GetIfInt(); |
| if (!kiosk_session_stop_reason.has_value()) { |
| return std::nullopt; |
| } |
| |
| return static_cast<KioskSessionEndReason>(kiosk_session_stop_reason.value()); |
| } |
| |
| } // namespace |
| |
| const char kKioskSessionStateHistogram[] = "Kiosk.SessionState"; |
| const char kKioskSessionCountPerDayHistogram[] = "Kiosk.Session.CountPerDay"; |
| const char kKioskSessionDurationNormalHistogram[] = |
| "Kiosk.SessionDuration.Normal"; |
| const char kKioskSessionDurationInDaysNormalHistogram[] = |
| "Kiosk.SessionDurationInDays.Normal"; |
| const char kKioskSessionDurationCrashedHistogram[] = |
| "Kiosk.SessionDuration.Crashed"; |
| const char kKioskSessionDurationInDaysCrashedHistogram[] = |
| "Kiosk.SessionDurationInDays.Crashed"; |
| const char kKioskSessionRestartReasonHistogram[] = |
| "Kiosk.SessionRestart.Reason"; |
| const char kKioskSessionLastDayList[] = "last-day-sessions"; |
| const char kKioskSessionStartTime[] = "session-start-time"; |
| const char kKioskSessionEndReason[] = "session-end-reason"; |
| |
| const int kKioskHistogramBucketCount = 100; |
| const base::TimeDelta kKioskSessionDurationHistogramLimit = base::Days(1); |
| |
| KioskMetricsService::KioskMetricsService(PrefService* prefs) |
| : KioskMetricsService(prefs, |
| std::vector<std::string>(std::begin(kCrashDirs), |
| std::end(kCrashDirs))) {} |
| |
| KioskMetricsService::~KioskMetricsService() = default; |
| |
| // static |
| std::unique_ptr<KioskMetricsService> KioskMetricsService::CreateForTesting( |
| PrefService* prefs, |
| const std::vector<std::string>& crash_dirs) { |
| return base::WrapUnique(new KioskMetricsService(prefs, crash_dirs)); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionStarted() { |
| RecordKioskSessionStarted(KioskSessionState::kStarted); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionWebStarted() { |
| RecordKioskSessionStarted(KioskSessionState::kWebStarted); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionIwaStarted() { |
| RecordKioskSessionStarted(KioskSessionState::kIwaStarted); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionStopped() { |
| if (!IsKioskSessionRunning()) { |
| return; |
| } |
| SaveSessionEndReason(KioskSessionEndReason::kStopped); |
| RecordKioskSessionState(KioskSessionState::kStopped); |
| RecordKioskSessionDuration(kKioskSessionDurationNormalHistogram, |
| kKioskSessionDurationInDaysNormalHistogram); |
| } |
| |
| void KioskMetricsService::RecordPreviousKioskSessionCrashed( |
| const base::Time& start_time) const { |
| RecordKioskSessionState(KioskSessionState::kCrashed); |
| RecordKioskSessionDuration(kKioskSessionDurationCrashedHistogram, |
| kKioskSessionDurationInDaysCrashedHistogram, |
| start_time); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionRestartReason( |
| const KioskSessionRestartReason& reason) const { |
| base::UmaHistogramEnumeration(kKioskSessionRestartReasonHistogram, reason); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionPluginCrashed() { |
| SaveSessionEndReason(KioskSessionEndReason::kPluginCrashed); |
| RecordKioskSessionState(KioskSessionState::kPluginCrashed); |
| RecordKioskSessionDuration(kKioskSessionDurationCrashedHistogram, |
| kKioskSessionDurationInDaysCrashedHistogram); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionPluginHung() { |
| SaveSessionEndReason(KioskSessionEndReason::kPluginHung); |
| RecordKioskSessionState(KioskSessionState::kPluginHung); |
| RecordKioskSessionDuration(kKioskSessionDurationCrashedHistogram, |
| kKioskSessionDurationInDaysCrashedHistogram); |
| } |
| |
| void KioskMetricsService::RestartRequested( |
| power_manager::RequestRestartReason reason) { |
| switch (reason) { |
| case power_manager::REQUEST_RESTART_FOR_USER: |
| case power_manager::REQUEST_RESTART_FOR_UPDATE: |
| case power_manager::REQUEST_RESTART_OTHER: |
| case power_manager::REQUEST_RESTART_HEARTD: |
| return; |
| case power_manager::REQUEST_RESTART_SCHEDULED_REBOOT_POLICY: |
| SaveSessionEndReason(KioskSessionEndReason::kRebootPolicy); |
| return; |
| case power_manager::REQUEST_RESTART_REMOTE_ACTION_REBOOT: |
| SaveSessionEndReason(KioskSessionEndReason::kRemoteActionReboot); |
| return; |
| case power_manager::REQUEST_RESTART_API: |
| SaveSessionEndReason(KioskSessionEndReason::kRestartApi); |
| return; |
| } |
| } |
| |
| KioskMetricsService::KioskMetricsService( |
| PrefService* prefs, |
| const std::vector<std::string>& crash_dirs) |
| : prefs_(prefs), crash_dirs_(crash_dirs) { |
| auto* power_manager_client = chromeos::PowerManagerClient::Get(); |
| DCHECK(power_manager_client); |
| power_manager_client_observation_.Observe(power_manager_client); |
| } |
| |
| bool KioskMetricsService::IsKioskSessionRunning() const { |
| return !start_time_.is_null(); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionStarted( |
| KioskSessionState started_state) { |
| RecordPreviousKioskSessionEndState(); |
| if (IsRestoredSession()) { |
| RecordKioskSessionState(KioskSessionState::kRestored); |
| } else { |
| RecordKioskSessionState(started_state); |
| } |
| RecordKioskSessionCountPerDay(); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionState( |
| KioskSessionState state) const { |
| base::UmaHistogramEnumeration(kKioskSessionStateHistogram, state); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionCountPerDay() { |
| base::UmaHistogramCounts100(kKioskSessionCountPerDayHistogram, |
| RetrieveLastDaySessionCount(base::Time::Now())); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionDuration( |
| const std::string& kiosk_session_duration_histogram, |
| const std::string& kiosk_session_duration_in_days_histogram) { |
| if (!IsKioskSessionRunning()) { |
| return; |
| } |
| RecordKioskSessionDuration(kiosk_session_duration_histogram, |
| kiosk_session_duration_in_days_histogram, |
| start_time_); |
| ClearStartTime(); |
| } |
| |
| void KioskMetricsService::RecordKioskSessionDuration( |
| const std::string& kiosk_session_duration_histogram, |
| const std::string& kiosk_session_duration_in_days_histogram, |
| const base::Time& start_time) const { |
| base::TimeDelta duration = base::Time::Now() - start_time; |
| if (duration >= kKioskSessionDurationHistogramLimit) { |
| base::UmaHistogramCounts100(kiosk_session_duration_in_days_histogram, |
| std::min(100, duration.InDays())); |
| duration = kKioskSessionDurationHistogramLimit; |
| } |
| base::UmaHistogramCustomTimes( |
| kiosk_session_duration_histogram, duration, base::Seconds(1), |
| kKioskSessionDurationHistogramLimit, kKioskHistogramBucketCount); |
| } |
| |
| void KioskMetricsService::RecordPreviousKioskSessionEndState() { |
| std::optional<KioskSessionEndReason> previous_session_end_reason = |
| GetSessionEndReason(prefs_); |
| // Avoid reading the old saved reason in the future. |
| ClearMetricFromPrefs(kKioskSessionEndReason, prefs_); |
| if (previous_session_end_reason.has_value()) { |
| auto restart_reason = ConvertSessionEndReasonToSessionRestartReason( |
| previous_session_end_reason.value()); |
| RecordKioskSessionRestartReason(restart_reason); |
| } |
| |
| // Check for a previous session crash, as a crash may occur after the session |
| // end reason was saved. |
| const base::Value::Dict& metrics_dict = prefs_->GetDict(prefs::kKioskMetrics); |
| auto previous_start_time = |
| base::ValueToTime(metrics_dict.Find(kKioskSessionStartTime)); |
| if (!previous_start_time.has_value()) { |
| return; |
| } |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(&IsPreviousKioskSessionCrashed, crash_dirs_, |
| previous_start_time.value()), |
| base::BindOnce(&KioskMetricsService::OnPreviousKioskSessionResult, |
| weak_ptr_factory_.GetWeakPtr(), |
| previous_start_time.value(), |
| previous_session_end_reason.has_value())); |
| } |
| |
| void KioskMetricsService::OnPreviousKioskSessionResult( |
| const base::Time& start_time, |
| bool has_recorded_session_restart_reason, |
| bool crashed) const { |
| if (crashed) { |
| RecordPreviousKioskSessionCrashed(start_time); |
| if (!has_recorded_session_restart_reason) { |
| RecordKioskSessionRestartReason( |
| RestartReasonWithRebootInfo(KioskSessionRestartReason::kCrashed)); |
| } else { |
| SYSLOG(INFO) |
| << "Kiosk session crash happend after recording end session reason"; |
| } |
| } else if (!has_recorded_session_restart_reason) { |
| // Previous session successfully stopped, but due to a race condition |
| // local_state was not correctly updated. |
| RecordKioskSessionRestartReason(RestartReasonWithRebootInfo( |
| KioskSessionRestartReason::kLocalStateWasNotSaved)); |
| } |
| } |
| |
| void KioskMetricsService::SaveSessionEndReason( |
| const KioskSessionEndReason& reason) { |
| if (prefs_->GetDict(prefs::kKioskMetrics).contains(kKioskSessionEndReason)) { |
| // Do not override saved reason. |
| // This function is called inside `RecordKioskSessionStopped` during the |
| // destructor, but before that the actual restart reason could be saved from |
| // different place. |
| return; |
| } |
| |
| ScopedDictPrefUpdate(prefs_, prefs::kKioskMetrics) |
| ->Set(kKioskSessionEndReason, static_cast<int>(reason)); |
| prefs_->CommitPendingWrite(base::DoNothing(), base::DoNothing()); |
| } |
| |
| size_t KioskMetricsService::RetrieveLastDaySessionCount( |
| base::Time session_start_time) { |
| const base::Value::Dict& metrics_dict = prefs_->GetDict(prefs::kKioskMetrics); |
| const base::Value::List* previous_times = nullptr; |
| |
| const auto* times_value = metrics_dict.Find(kKioskSessionLastDayList); |
| if (times_value) { |
| previous_times = times_value->GetIfList(); |
| DCHECK(previous_times); |
| } |
| |
| base::Value::List times; |
| if (previous_times) { |
| for (const auto& time : *previous_times) { |
| if (base::ValueToTime(time).has_value() && |
| session_start_time - base::ValueToTime(time).value() <= |
| base::Days(1)) { |
| times.Append(time.Clone()); |
| } |
| } |
| } |
| times.Append(base::TimeToValue(session_start_time)); |
| size_t result = times.size(); |
| |
| start_time_ = session_start_time; |
| |
| ScopedDictPrefUpdate(prefs_, prefs::kKioskMetrics) |
| ->Set(kKioskSessionLastDayList, std::move(times)); |
| ScopedDictPrefUpdate(prefs_, prefs::kKioskMetrics) |
| ->Set(kKioskSessionStartTime, base::TimeToValue(start_time_)); |
| return result; |
| } |
| |
| void KioskMetricsService::ClearStartTime() { |
| start_time_ = base::Time(); |
| ClearMetricFromPrefs(kKioskSessionStartTime, prefs_); |
| } |
| |
| } // namespace chromeos |