blob: 1accf07511359eb166adca8e5b7dd1c8c8443db5 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/browser/crash_report/crash_helper.h"
#import <UIKit/UIKit.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/debug/crash_logging.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/ios/ios_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "components/crash/core/app/crashpad.h"
#include "components/crash/core/common/crash_key.h"
#include "components/crash/core/common/reporter_running_ios.h"
#include "ios/chrome/browser/chrome_paths.h"
#import "ios/chrome/browser/crash_report/crash_report_user_application_state.h"
#include "ios/chrome/browser/crash_report/crash_upload_list.h"
#include "ios/chrome/browser/crash_report/features.h"
#import "ios/chrome/browser/crash_report/main_thread_freeze_detector.h"
#include "ios/chrome/common/app_group/app_group_constants.h"
#include "ios/chrome/common/channel_info.h"
#include "ios/chrome/common/crash_report/crash_helper.h"
#import "third_party/breakpad/breakpad/src/client/ios/BreakpadController.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace crash_helper {
namespace {
const char kUptimeAtRestoreInMs[] = "uptime_at_restore_in_ms";
const char kUploadedInRecoveryMode[] = "uploaded_in_recovery_mode";
// Delete breakpad reports after 60 days.
void DeleteOldReportsInDirectory(base::FilePath directory) {
base::FileEnumerator enumerator(directory, false,
base::FileEnumerator::FILES);
base::FilePath cur_file;
while (!(cur_file = enumerator.Next()).value().empty()) {
if (cur_file.BaseName().value() != kReporterLogFilename) {
time_t now = time(nullptr);
struct stat st;
if (lstat(cur_file.value().c_str(), &st) != 0) {
continue;
}
// 60 days.
constexpr time_t max_breakpad_report_age_sec = 60 * 60 * 24 * 60;
if (st.st_mtime <= now - max_breakpad_report_age_sec) {
base::DeleteFile(cur_file);
}
}
}
}
// Tells crashpad to start processing previously created intermediate dumps and
// begin uploading when possible.
void ProcessIntermediateDumps() {
crash_reporter::ProcessIntermediateDumps();
[[MainThreadFreezeDetector sharedInstance] processIntermediateDumps];
crash_reporter::StartProcessingPendingReports();
}
// Callback for logging::SetLogMessageHandler
bool FatalMessageHandler(int severity,
const char* file,
int line,
size_t message_start,
const std::string& str) {
// Do not handle non-FATAL.
if (severity != logging::LOG_FATAL)
return false;
// In case of OOM condition, this code could be reentered when
// constructing and storing the key. Using a static is not
// thread-safe, but if multiple threads are in the process of a
// fatal crash at the same time, this should work.
static bool guarded = false;
if (guarded)
return false;
base::AutoReset<bool> guard(&guarded, true);
// Only log last path component. This matches logging.cc.
if (file) {
const char* slash = strrchr(file, '/');
if (slash)
file = slash + 1;
}
NSString* fatal_value = [NSString
stringWithFormat:@"%s:%d: %s", file, line, str.c_str() + message_start];
static crash_reporter::CrashKeyString<2550> key("LOG_FATAL");
key.Set(base::SysNSStringToUTF8(fatal_value));
// Rather than including the code to force the crash here, allow the
// caller to do it.
return false;
}
// Called after Breakpad finishes uploading each report.
void UploadResultHandler(NSString* report_id, NSError* error) {
base::UmaHistogramSparse("CrashReport.BreakpadIOSUploadOutcome", error.code);
}
// Returns the uptime, the difference between now and start time.
int64_t GetUptimeMilliseconds() {
struct timeval tv;
gettimeofday(&tv, NULL);
kinfo_proc kern_proc_info;
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
size_t len = sizeof(kern_proc_info);
if (sysctl(mib, std::size(mib), &kern_proc_info, &len, nullptr, 0) != 0)
return 0;
time_t process_uptime_seconds =
tv.tv_sec - kern_proc_info.kp_proc.p_starttime.tv_sec;
return static_cast<const int64_t>(process_uptime_seconds) *
base::Time::kMillisecondsPerSecond;
}
} // namespace
void SyncCrashpadEnabledOnNextRun() {
[app_group::GetGroupUserDefaults()
setBool:base::FeatureList::IsEnabled(kCrashpadIOS) ? YES : NO
forKey:base::SysUTF8ToNSString(common::kCrashpadStartOnNextRun)];
}
void Start() {
DCHECK(!crash_reporter::IsBreakpadRunning());
DCHECK(!crash_reporter::IsCrashpadRunning());
// Notifying the PathService on the location of the crashes so that crashes
// can be displayed to the user on the about:crashes page. Use the app group
// so crashes can be shared by plugins.
logging::SetLogMessageHandler(&FatalMessageHandler);
if (common::CanUseCrashpad()) {
base::PathService::Override(ios::DIR_CRASH_DUMPS,
common::CrashpadDumpLocation());
bool initialized = common::StartCrashpad();
if (initialized) {
crash_reporter::SetCrashpadRunning(true);
}
UMA_HISTOGRAM_BOOLEAN("Stability.IOS.Crashpad.Initialized", initialized);
} else {
NSArray* cachesDirectories = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES);
NSString* cachePath = [cachesDirectories objectAtIndex:0];
NSString* dumpDirectory =
[cachePath stringByAppendingPathComponent:@kDefaultLibrarySubdirectory];
base::PathService::Override(
ios::DIR_CRASH_DUMPS,
base::FilePath(base::SysNSStringToUTF8(dumpDirectory)));
[[BreakpadController sharedInstance] start:YES];
crash_reporter::SetBreakpadRunning(true);
// Register channel information (Breakpad only, crashpad does this
// automatically).
std::string channel_name = GetChannelString();
if (channel_name.length()) {
static crash_reporter::CrashKeyString<64> key("channel");
key.Set(channel_name);
}
}
crash_reporter::InitializeCrashKeys();
// Don't start MTFD when prewarmed, the check thread will just get confused.
if (!base::ios::IsApplicationPreWarmed()) {
[[MainThreadFreezeDetector sharedInstance] start];
}
}
void SetEnabled(bool enabled) {
// Caches the uploading flag in NSUserDefaults, so that we can access the
// value immediately on startup, such as in safe mode or extensions.
crash_helper::common::SetUserEnabledUploading(enabled);
// It is necessary to always call |MainThreadFreezeDetector setEnabled| as
// the function will update its preference based on finch.
[[MainThreadFreezeDetector sharedInstance] setEnabled:enabled];
// Crashpad is always running, don't shut it off. Using CanUseCrashpad()
// here, because if Crashpad fails to init, do not unintentionally enable
// breakpad.
if (common::CanUseCrashpad()) {
// Posts SetUploadConsent on blocking pool thread because it needs access to
// IO and cannot work from UI thread.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(^{
crash_reporter::SetUploadConsent(enabled);
}));
return;
}
if (crash_reporter::IsBreakpadRunning() == enabled)
return;
crash_reporter::SetBreakpadRunning(enabled);
if (enabled) {
[[BreakpadController sharedInstance] start:NO];
} else {
[[BreakpadController sharedInstance] stop];
}
}
void UploadCrashReports() {
if (crash_reporter::IsCrashpadRunning()) {
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&ProcessIntermediateDumps));
return;
});
}
if (crash_reporter::IsBreakpadRunning()) {
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
[[BreakpadController sharedInstance]
setUploadCallback:UploadResultHandler];
// Clean old breakpad files here. Breakpad-only as Crashpad has it's own
// database cleaner.
base::FilePath crash_directory;
base::PathService::Get(ios::DIR_CRASH_DUMPS, &crash_directory);
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&DeleteOldReportsInDirectory, crash_directory));
});
[[BreakpadController sharedInstance] setUploadingEnabled:YES];
}
}
void PauseBreakpadUploads() {
if (crash_reporter::IsBreakpadRunning()) {
[[BreakpadController sharedInstance] setUploadingEnabled:NO];
}
}
void ProcessIntermediateReportsForSafeMode() {
if (crash_reporter::IsCrashpadRunning()) {
crash_reporter::ProcessIntermediateDumps(
{{kUploadedInRecoveryMode, "yes"}});
}
}
int GetPendingCrashReportCount() {
if (crash_reporter::IsCrashpadRunning()) {
int count = 0;
std::vector<crash_reporter::Report> reports;
crash_reporter::GetReports(&reports);
for (auto& report : reports) {
if (report.state == crash_reporter::ReportUploadState::Pending ||
report.state ==
crash_reporter::ReportUploadState::Pending_UserRequested) {
count++;
}
}
return count;
}
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int outerCrashReportCount = 0;
[[BreakpadController sharedInstance] getCrashReportCount:^(int count) {
outerCrashReportCount = count;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return outerCrashReportCount;
}
bool HasReportToUpload() {
return GetPendingCrashReportCount() > 0;
}
// Records the current process uptime in the kUptimeAtRestoreInMs. This
// will allow engineers to dremel crash logs to find crash whose delta between
// process uptime at crash and process uptime at restore is smaller than X
// seconds and find insta-crashers.
void WillStartCrashRestoration() {
if (crash_reporter::IsCrashpadRunning()) {
const int64_t uptime_milliseconds = GetUptimeMilliseconds();
if (uptime_milliseconds > 0) {
static crash_reporter::CrashKeyString<16> key(kUptimeAtRestoreInMs);
key.Set(base::NumberToString((uptime_milliseconds)));
}
return;
}
if (!crash_reporter::IsBreakpadRunning())
return;
// We use gettimeofday and BREAKPAD_PROCESS_START_TIME to compute the
// uptime to stay as close as possible as how breakpad computes the
// "ProcessUptime" in order to have meaningful comparison in dremel.
struct timeval tv;
gettimeofday(&tv, NULL);
// The values stored in the breakpad log are only accessible through a
// BreakpadRef. To record the process uptime at restore, the value of
// BREAKPAD_PROCESS_START_TIME is required to compute the delta.
[[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
if (!ref)
return;
NSString* processStartTimeSecondsString =
BreakpadKeyValue(ref, @BREAKPAD_PROCESS_START_TIME);
if (!processStartTimeSecondsString)
return;
time_t processStartTimeSeconds =
[processStartTimeSecondsString longLongValue];
time_t processUptimeSeconds = tv.tv_sec - processStartTimeSeconds;
const int64_t processUptimeMilliseconds =
static_cast<const int64_t>(processUptimeSeconds) *
base::Time::kMillisecondsPerSecond;
BreakpadAddUploadParameter(
ref, base::SysUTF8ToNSString(kUptimeAtRestoreInMs),
[NSString stringWithFormat:@"%llu", processUptimeMilliseconds]);
}];
}
void StartUploadingReportsInRecoveryMode() {
if (crash_reporter::IsCrashpadRunning()) {
crash_reporter::StartProcessingPendingReports();
return;
}
if (!crash_reporter::IsBreakpadRunning())
return;
[[BreakpadController sharedInstance] stop];
[[BreakpadController sharedInstance] setParametersToAddAtUploadTime:@{
base::SysUTF8ToNSString(kUploadedInRecoveryMode) : @"yes"
}];
[[BreakpadController sharedInstance] setUploadInterval:1];
[[BreakpadController sharedInstance] start:NO];
[[BreakpadController sharedInstance] setUploadingEnabled:YES];
}
void RestoreDefaultConfiguration() {
if (!crash_reporter::IsBreakpadRunning()) {
return;
}
[[BreakpadController sharedInstance] stop];
[[BreakpadController sharedInstance] resetConfiguration];
[[BreakpadController sharedInstance] start:NO];
[[BreakpadController sharedInstance] setUploadingEnabled:NO];
}
void ClearReportsBetween(base::Time delete_begin, base::Time delete_end) {
ios::CreateCrashUploadList()->Clear(delete_begin, delete_end);
}
} // namespace crash_helper