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