1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/metrics/clean_exit_beacon.h"
6
7 #include <algorithm>
8 #include <cmath>
9 #include <memory>
10 #include <utility>
11
12 #include "base/check_op.h"
13 #include "base/command_line.h"
14 #include "base/files/file_util.h"
15 #include "base/json/json_file_value_serializer.h"
16 #include "base/json/json_string_value_serializer.h"
17 #include "base/logging.h"
18 #include "base/metrics/field_trial.h"
19 #include "base/metrics/histogram_functions.h"
20 #include "base/metrics/histogram_macros.h"
21 #include "base/path_service.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/stringprintf.h"
24 #include "base/threading/thread_restrictions.h"
25 #include "build/build_config.h"
26 #include "components/metrics/metrics_pref_names.h"
27 #include "components/prefs/pref_registry_simple.h"
28 #include "components/prefs/pref_service.h"
29 #include "components/variations/pref_names.h"
30 #include "components/variations/variations_switches.h"
31
32 #if BUILDFLAG(IS_WIN)
33 #include <windows.h>
34
35 #include "base/strings/string_util_win.h"
36 #include "base/strings/utf_string_conversions.h"
37 #include "base/win/registry.h"
38 #endif
39
40 namespace metrics {
41 namespace {
42
43 using ::variations::prefs::kVariationsCrashStreak;
44
45 // Denotes whether Chrome should perform clean shutdown steps: signaling that
46 // Chrome is exiting cleanly and then CHECKing that is has shutdown cleanly.
47 // This may be modified by SkipCleanShutdownStepsForTesting().
48 bool g_skip_clean_shutdown_steps = false;
49
50 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
51 // Records the the combined state of two distinct beacons' values in a
52 // histogram.
RecordBeaconConsistency(std::optional<bool> beacon_file_beacon_value,std::optional<bool> platform_specific_beacon_value)53 void RecordBeaconConsistency(
54 std::optional<bool> beacon_file_beacon_value,
55 std::optional<bool> platform_specific_beacon_value) {
56 CleanExitBeaconConsistency consistency =
57 CleanExitBeaconConsistency::kDirtyDirty;
58
59 if (!beacon_file_beacon_value) {
60 if (!platform_specific_beacon_value) {
61 consistency = CleanExitBeaconConsistency::kMissingMissing;
62 } else {
63 consistency = platform_specific_beacon_value.value()
64 ? CleanExitBeaconConsistency::kMissingClean
65 : CleanExitBeaconConsistency::kMissingDirty;
66 }
67 } else if (!platform_specific_beacon_value) {
68 consistency = beacon_file_beacon_value.value()
69 ? CleanExitBeaconConsistency::kCleanMissing
70 : CleanExitBeaconConsistency::kDirtyMissing;
71 } else if (beacon_file_beacon_value.value()) {
72 consistency = platform_specific_beacon_value.value()
73 ? CleanExitBeaconConsistency::kCleanClean
74 : CleanExitBeaconConsistency::kCleanDirty;
75 } else {
76 consistency = platform_specific_beacon_value.value()
77 ? CleanExitBeaconConsistency::kDirtyClean
78 : CleanExitBeaconConsistency::kDirtyDirty;
79 }
80 base::UmaHistogramEnumeration("UMA.CleanExitBeaconConsistency3", consistency);
81 }
82 #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
83
84 // Increments kVariationsCrashStreak if |did_previous_session_exit_cleanly| is
85 // false. Also, emits the crash streak to a histogram.
86 //
87 // If |beacon_file_contents| are given, then the beacon file is used to retrieve
88 // the crash streak. Otherwise, |local_state| is used.
MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,base::Value * beacon_file_contents,PrefService * local_state)89 void MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,
90 base::Value* beacon_file_contents,
91 PrefService* local_state) {
92 int num_crashes;
93 int local_state_num_crashes = local_state->GetInteger(kVariationsCrashStreak);
94
95 if (beacon_file_contents) {
96 std::optional<int> crash_streak =
97 beacon_file_contents->GetDict().FindInt(kVariationsCrashStreak);
98 // Any contents without the key should have been rejected by
99 // MaybeGetFileContents().
100 DCHECK(crash_streak);
101 num_crashes = crash_streak.value();
102 base::UmaHistogramCounts100(
103 "Variations.SafeMode.CrashStreakDiscrepancy",
104 std::abs(local_state_num_crashes - num_crashes));
105 } else {
106 // TODO(crbug.com/40850830): Consider not falling back to Local State for
107 // clients on platforms that support the beacon file.
108 num_crashes = local_state_num_crashes;
109 }
110
111 if (!did_previous_session_exit_cleanly) {
112 // Increment the crash streak if the previous session crashed. Note that the
113 // streak is not cleared if the previous run didn’t crash. Instead, it’s
114 // incremented on each crash until Chrome is able to successfully fetch a
115 // new seed. This way, a seed update that mostly destabilizes Chrome still
116 // results in a fallback to Variations Safe Mode.
117 //
118 // The crash streak is incremented here rather than in a variations-related
119 // class for two reasons. First, the crash streak depends on whether Chrome
120 // exited cleanly in the last session, which is first checked via
121 // CleanExitBeacon::Initialize(). Second, if the crash streak were updated
122 // in another function, any crash between beacon initialization and the
123 // other function might cause the crash streak to not be to incremented.
124 // "Might" because the updated crash streak also needs to be persisted to
125 // disk. A consequence of failing to increment the crash streak is that
126 // Chrome might undercount or be completely unaware of repeated crashes
127 // early on in startup.
128 ++num_crashes;
129 // For platforms that use the beacon file, the crash streak is written
130 // synchronously to disk later on in startup via
131 // MaybeExtendVariationsSafeMode() and WriteBeaconFile(). The crash streak
132 // is intentionally not written to the beacon file here. If the beacon file
133 // indicates that Chrome failed to exit cleanly, then Chrome got at
134 // least as far as MaybeExtendVariationsSafeMode(), which is during the
135 // PostEarlyInitialization stage when native code is being synchronously
136 // executed. Chrome should also be able to reach that point in this session.
137 //
138 // For platforms that do not use the beacon file, the crash streak is
139 // scheduled to be written to disk later on in startup. At the latest, this
140 // is done when a Local State write is scheduled via WriteBeaconFile(). A
141 // write is not scheduled here for two reasons.
142 //
143 // 1. It is an expensive operation.
144 // 2. Android WebView (which does not use the beacon file) has its own
145 // Variations Safe Mode mechanism and does not need the crash streak.
146 local_state->SetInteger(kVariationsCrashStreak, num_crashes);
147 }
148 base::UmaHistogramSparse("Variations.SafeMode.Streak.Crashes",
149 std::clamp(num_crashes, 0, 100));
150 }
151
152 // Records |file_state| in a histogram.
RecordBeaconFileState(BeaconFileState file_state)153 void RecordBeaconFileState(BeaconFileState file_state) {
154 base::UmaHistogramEnumeration(
155 "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", file_state);
156 }
157
158 // Returns the contents of the file at |beacon_file_path| if the following
159 // conditions are all true. Otherwise, returns nullptr.
160 //
161 // 1. The file path is non-empty.
162 // 2. The file exists.
163 // 3. The file is successfully read.
164 // 4. The file contents are in the expected format with the expected info.
165 //
166 // The file may not exist for the below reasons:
167 //
168 // 1. The file is unsupported on the platform.
169 // 2. This is the first session after a client updates to or installs a Chrome
170 // version that uses the beacon file. The beacon file launched on desktop
171 // and iOS in M102 and on Android Chrome in M103.
172 // 3. Android Chrome clients with only background sessions may never write a
173 // beacon file.
174 // 4. A user may delete the file.
MaybeGetFileContents(const base::FilePath & beacon_file_path)175 std::unique_ptr<base::Value> MaybeGetFileContents(
176 const base::FilePath& beacon_file_path) {
177 if (beacon_file_path.empty())
178 return nullptr;
179
180 int error_code;
181 JSONFileValueDeserializer deserializer(beacon_file_path);
182 std::unique_ptr<base::Value> beacon_file_contents =
183 deserializer.Deserialize(&error_code, /*error_message=*/nullptr);
184
185 if (!beacon_file_contents) {
186 RecordBeaconFileState(BeaconFileState::kNotDeserializable);
187 base::UmaHistogramSparse(
188 "Variations.ExtendedSafeMode.BeaconFileDeserializationError",
189 error_code);
190 return nullptr;
191 }
192 if (!beacon_file_contents->is_dict() ||
193 beacon_file_contents->GetDict().empty()) {
194 RecordBeaconFileState(BeaconFileState::kMissingDictionary);
195 return nullptr;
196 }
197 const base::Value::Dict& beacon_dict = beacon_file_contents->GetDict();
198 if (!beacon_dict.FindInt(kVariationsCrashStreak)) {
199 RecordBeaconFileState(BeaconFileState::kMissingCrashStreak);
200 return nullptr;
201 }
202 if (!beacon_dict.FindBool(prefs::kStabilityExitedCleanly)) {
203 RecordBeaconFileState(BeaconFileState::kMissingBeacon);
204 return nullptr;
205 }
206 RecordBeaconFileState(BeaconFileState::kReadable);
207 return beacon_file_contents;
208 }
209
210 } // namespace
211
212 const base::FilePath::CharType kCleanExitBeaconFilename[] =
213 FILE_PATH_LITERAL("Variations");
214
CleanExitBeacon(const std::wstring & backup_registry_key,const base::FilePath & user_data_dir,PrefService * local_state)215 CleanExitBeacon::CleanExitBeacon(const std::wstring& backup_registry_key,
216 const base::FilePath& user_data_dir,
217 PrefService* local_state)
218 : backup_registry_key_(backup_registry_key),
219 user_data_dir_(user_data_dir),
220 local_state_(local_state),
221 initial_browser_last_live_timestamp_(
222 local_state->GetTime(prefs::kStabilityBrowserLastLiveTimeStamp)) {
223 DCHECK_NE(PrefService::INITIALIZATION_STATUS_WAITING,
224 local_state_->GetInitializationStatus());
225 }
226
Initialize()227 void CleanExitBeacon::Initialize() {
228 DCHECK(!initialized_);
229
230 if (!user_data_dir_.empty()) {
231 // Platforms that pass an empty path do so deliberately. They should not
232 // use the beacon file.
233 beacon_file_path_ = user_data_dir_.Append(kCleanExitBeaconFilename);
234 }
235
236 std::unique_ptr<base::Value> beacon_file_contents =
237 MaybeGetFileContents(beacon_file_path_);
238
239 did_previous_session_exit_cleanly_ =
240 DidPreviousSessionExitCleanly(beacon_file_contents.get());
241
242 MaybeIncrementCrashStreak(did_previous_session_exit_cleanly_,
243 beacon_file_contents.get(), local_state_);
244 initialized_ = true;
245 }
246
DidPreviousSessionExitCleanly(base::Value * beacon_file_contents)247 bool CleanExitBeacon::DidPreviousSessionExitCleanly(
248 base::Value* beacon_file_contents) {
249 if (!IsBeaconFileSupported())
250 return local_state_->GetBoolean(prefs::kStabilityExitedCleanly);
251
252 std::optional<bool> beacon_file_beacon_value =
253 beacon_file_contents ? beacon_file_contents->GetDict().FindBool(
254 prefs::kStabilityExitedCleanly)
255 : std::nullopt;
256
257 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
258 std::optional<bool> backup_beacon_value = ExitedCleanly();
259 RecordBeaconConsistency(beacon_file_beacon_value, backup_beacon_value);
260 #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
261
262 #if BUILDFLAG(IS_IOS)
263 // TODO(crbug.com/40190558): For the time being, this is a no-op; i.e.,
264 // ShouldUseUserDefaultsBeacon() always returns false.
265 if (ShouldUseUserDefaultsBeacon())
266 return backup_beacon_value.value_or(true);
267 #endif // BUILDFLAG(IS_IOS)
268
269 return beacon_file_beacon_value.value_or(true);
270 }
271
IsExtendedSafeModeSupported() const272 bool CleanExitBeacon::IsExtendedSafeModeSupported() const {
273 // All platforms that support the beacon file mechanism also happen to support
274 // Extended Variations Safe Mode.
275 return IsBeaconFileSupported();
276 }
277
WriteBeaconValue(bool exited_cleanly,bool is_extended_safe_mode)278 void CleanExitBeacon::WriteBeaconValue(bool exited_cleanly,
279 bool is_extended_safe_mode) {
280 DCHECK(initialized_);
281 if (g_skip_clean_shutdown_steps)
282 return;
283
284 UpdateLastLiveTimestamp();
285
286 if (has_exited_cleanly_ && has_exited_cleanly_.value() == exited_cleanly) {
287 // It is possible to call WriteBeaconValue() with the same value for
288 // |exited_cleanly| twice during startup and shutdown on some platforms. If
289 // the current beacon value matches |exited_cleanly|, then return here to
290 // skip redundantly updating Local State, writing a beacon file, and on
291 // Windows and iOS, writing to platform-specific locations.
292 return;
293 }
294
295 if (is_extended_safe_mode) {
296 // |is_extended_safe_mode| can be true for only some platforms.
297 DCHECK(IsExtendedSafeModeSupported());
298 // |has_exited_cleanly_| should always be unset before starting to watch for
299 // browser crashes.
300 DCHECK(!has_exited_cleanly_);
301 // When starting to watch for browser crashes in the code covered by
302 // Extended Variations Safe Mode, the only valid value for |exited_cleanly|
303 // is `false`. `true` signals that Chrome should stop watching for crashes.
304 DCHECK(!exited_cleanly);
305 WriteBeaconFile(exited_cleanly);
306 } else {
307 // TODO(crbug.com/40851383): Stop updating |kStabilityExitedCleanly| on
308 // platforms that support the beacon file.
309 local_state_->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
310 if (IsBeaconFileSupported()) {
311 WriteBeaconFile(exited_cleanly);
312 } else {
313 // Schedule a Local State write on platforms that back the beacon value
314 // using Local State rather than the beacon file.
315 local_state_->CommitPendingWrite();
316 }
317 }
318
319 #if BUILDFLAG(IS_WIN)
320 base::win::RegKey regkey;
321 if (regkey.Create(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
322 KEY_ALL_ACCESS) == ERROR_SUCCESS) {
323 regkey.WriteValue(base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(),
324 exited_cleanly ? 1u : 0u);
325 }
326 #elif BUILDFLAG(IS_IOS)
327 SetUserDefaultsBeacon(exited_cleanly);
328 #endif // BUILDFLAG(IS_WIN)
329
330 has_exited_cleanly_ = std::make_optional(exited_cleanly);
331 }
332
333 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
ExitedCleanly()334 std::optional<bool> CleanExitBeacon::ExitedCleanly() {
335 #if BUILDFLAG(IS_WIN)
336 base::win::RegKey regkey;
337 DWORD value = 0u;
338 if (regkey.Open(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
339 KEY_ALL_ACCESS) == ERROR_SUCCESS &&
340 regkey.ReadValueDW(
341 base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(), &value) ==
342 ERROR_SUCCESS) {
343 return value ? true : false;
344 }
345 return std::nullopt;
346 #endif // BUILDFLAG(IS_WIN)
347 #if BUILDFLAG(IS_IOS)
348 if (HasUserDefaultsBeacon())
349 return GetUserDefaultsBeacon();
350 return std::nullopt;
351 #endif // BUILDFLAG(IS_IOS)
352 }
353 #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
354
UpdateLastLiveTimestamp()355 void CleanExitBeacon::UpdateLastLiveTimestamp() {
356 local_state_->SetTime(prefs::kStabilityBrowserLastLiveTimeStamp,
357 base::Time::Now());
358 }
359
GetUserDataDirForTesting() const360 const base::FilePath CleanExitBeacon::GetUserDataDirForTesting() const {
361 return user_data_dir_;
362 }
363
GetBeaconFilePathForTesting() const364 base::FilePath CleanExitBeacon::GetBeaconFilePathForTesting() const {
365 return beacon_file_path_;
366 }
367
368 // static
RegisterPrefs(PrefRegistrySimple * registry)369 void CleanExitBeacon::RegisterPrefs(PrefRegistrySimple* registry) {
370 registry->RegisterBooleanPref(prefs::kStabilityExitedCleanly, true);
371
372 registry->RegisterTimePref(prefs::kStabilityBrowserLastLiveTimeStamp,
373 base::Time(), PrefRegistry::LOSSY_PREF);
374
375 // This Variations-Safe-Mode-related pref is registered here rather than in
376 // SafeSeedManager::RegisterPrefs() because the CleanExitBeacon is
377 // responsible for incrementing this value. (See the comments in
378 // MaybeIncrementCrashStreak() for more details.)
379 registry->RegisterIntegerPref(kVariationsCrashStreak, 0);
380 }
381
382 // static
EnsureCleanShutdown(PrefService * local_state)383 void CleanExitBeacon::EnsureCleanShutdown(PrefService* local_state) {
384 if (!g_skip_clean_shutdown_steps)
385 CHECK(local_state->GetBoolean(prefs::kStabilityExitedCleanly));
386 }
387
388 // static
SetStabilityExitedCleanlyForTesting(PrefService * local_state,bool exited_cleanly)389 void CleanExitBeacon::SetStabilityExitedCleanlyForTesting(
390 PrefService* local_state,
391 bool exited_cleanly) {
392 local_state->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
393 #if BUILDFLAG(IS_IOS)
394 SetUserDefaultsBeacon(exited_cleanly);
395 #endif // BUILDFLAG(IS_IOS)
396 }
397
398 // static
CreateBeaconFileContentsForTesting(bool exited_cleanly,int crash_streak)399 std::string CleanExitBeacon::CreateBeaconFileContentsForTesting(
400 bool exited_cleanly,
401 int crash_streak) {
402 const std::string exited_cleanly_str = exited_cleanly ? "true" : "false";
403 return base::StringPrintf(
404 "{\n"
405 " \"user_experience_metrics.stability.exited_cleanly\":%s,\n"
406 " \"variations_crash_streak\":%s\n"
407 "}",
408 exited_cleanly_str.data(), base::NumberToString(crash_streak).data());
409 }
410
411 // static
ResetStabilityExitedCleanlyForTesting(PrefService * local_state)412 void CleanExitBeacon::ResetStabilityExitedCleanlyForTesting(
413 PrefService* local_state) {
414 local_state->ClearPref(prefs::kStabilityExitedCleanly);
415 #if BUILDFLAG(IS_IOS)
416 ResetUserDefaultsBeacon();
417 #endif // BUILDFLAG(IS_IOS)
418 }
419
420 // static
SkipCleanShutdownStepsForTesting()421 void CleanExitBeacon::SkipCleanShutdownStepsForTesting() {
422 g_skip_clean_shutdown_steps = true;
423 }
424
IsBeaconFileSupported() const425 bool CleanExitBeacon::IsBeaconFileSupported() const {
426 return !beacon_file_path_.empty();
427 }
428
WriteBeaconFile(bool exited_cleanly) const429 void CleanExitBeacon::WriteBeaconFile(bool exited_cleanly) const {
430 base::Value::Dict dict;
431 dict.Set(prefs::kStabilityExitedCleanly, exited_cleanly);
432 dict.Set(kVariationsCrashStreak,
433 local_state_->GetInteger(kVariationsCrashStreak));
434
435 std::string json_string;
436 JSONStringValueSerializer serializer(&json_string);
437 bool success = serializer.Serialize(dict);
438 DCHECK(success);
439 DCHECK(!json_string.empty());
440 {
441 base::ScopedAllowBlocking allow_io;
442 success = base::WriteFile(beacon_file_path_, json_string);
443 }
444 base::UmaHistogramBoolean("Variations.ExtendedSafeMode.BeaconFileWrite",
445 success);
446 }
447
448 } // namespace metrics
449