blob: e930cce323c44aebfa044d07469097a2c8df64d8 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/chrome_browser_main_mac.h"
#import <Cocoa/Cocoa.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/param.h>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/mac/bundle_locations.h"
#import "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/mac/sdk_forward_declarations.h"
#include "base/metrics/histogram_functions.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/threading/thread_task_runner_handle.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/apps/app_shim/app_shim_host_manager_mac.h"
#include "chrome/browser/browser_process.h"
#import "chrome/browser/chrome_browser_application_mac.h"
#include "chrome/browser/first_run/first_run.h"
#include "chrome/browser/first_run/upgrade_util_mac.h"
#include "chrome/browser/mac/install_from_dmg.h"
#import "chrome/browser/mac/keystone_glue.h"
#include "chrome/browser/mac/mac_startup_profiler.h"
#include "chrome/browser/ui/cocoa/main_menu_builder.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/staging_watcher.h"
#include "chrome/grit/chromium_strings.h"
#include "components/crash/content/app/crashpad.h"
#include "components/metrics/metrics_service.h"
#include "components/os_crypt/os_crypt.h"
#include "content/public/common/main_function_params.h"
#include "content/public/common/result_codes.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_handle.h"
#include "ui/native_theme/native_theme_mac.h"
namespace {
// Writes an undocumented sentinel file that prevents Spotlight from indexing
// below a particular path in order to reap some power savings.
void EnsureMetadataNeverIndexFileOnFileThread(
const base::FilePath& user_data_dir) {
const char kMetadataNeverIndexFilename[] = ".metadata_never_index";
base::FilePath metadata_file_path =
user_data_dir.Append(kMetadataNeverIndexFilename);
if (base::PathExists(metadata_file_path))
return;
if (base::WriteFile(metadata_file_path, nullptr, 0) == -1)
DLOG(FATAL) << "Could not write .metadata_never_index file.";
}
void EnsureMetadataNeverIndexFile(const base::FilePath& user_data_dir) {
base::PostTaskWithTraits(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(&EnsureMetadataNeverIndexFileOnFileThread, user_data_dir));
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class FilesystemType {
kUnknown,
kOther,
k_acfs,
k_afpfs,
k_apfs,
k_cdd9660,
k_cddafs,
k_exfat,
k_ftp,
k_hfs,
k_hfs_rodmg,
k_msdos,
k_nfs,
k_ntfs,
k_smbfs,
k_udf,
k_webdav,
kGoogleDriveFS,
kMaxValue = kGoogleDriveFS,
};
FilesystemType FilesystemStringToType(DiskImageStatus is_ro_dmg,
NSString* filesystem_type) {
if ([filesystem_type isEqualToString:@"acfs"])
return FilesystemType::k_acfs;
if ([filesystem_type isEqualToString:@"afpfs"])
return FilesystemType::k_afpfs;
if ([filesystem_type isEqualToString:@"apfs"])
return FilesystemType::k_apfs;
if ([filesystem_type isEqualToString:@"cdd9660"])
return FilesystemType::k_cdd9660;
if ([filesystem_type isEqualToString:@"cddafs"])
return FilesystemType::k_cddafs;
if ([filesystem_type isEqualToString:@"exfat"])
return FilesystemType::k_exfat;
if ([filesystem_type isEqualToString:@"hfs"]) {
switch (is_ro_dmg) {
case DiskImageStatusFailure:
case DiskImageStatusFalse:
return FilesystemType::k_hfs;
break;
case DiskImageStatusTrue:
return FilesystemType::k_hfs_rodmg;
break;
}
}
if ([filesystem_type isEqualToString:@"msdos"])
return FilesystemType::k_msdos;
if ([filesystem_type isEqualToString:@"nfs"])
return FilesystemType::k_nfs;
if ([filesystem_type isEqualToString:@"ntfs"])
return FilesystemType::k_ntfs;
if ([filesystem_type isEqualToString:@"smbfs"])
return FilesystemType::k_smbfs;
if ([filesystem_type isEqualToString:@"udf"])
return FilesystemType::k_udf;
if ([filesystem_type isEqualToString:@"webdav"])
return FilesystemType::k_webdav;
if ([filesystem_type isEqualToString:@"dfsfuse_DFS"])
return FilesystemType::kGoogleDriveFS;
return FilesystemType::kOther;
}
void RecordFilesystemStats() {
DiskImageStatus is_ro_dmg = IsAppRunningFromReadOnlyDiskImage(nullptr);
// Note that -getFileSystemInfoForPath:... is implemented with Disk
// Arbitration and |filesystem_type_string| is the value from
// kDADiskDescriptionVolumeKindKey. Furthermore, for built-in filesystems, the
// string returned specifies which file in /System/Library/Filesystems is
// handling it.
NSString* filesystem_type_string;
BOOL success = [[NSWorkspace sharedWorkspace]
getFileSystemInfoForPath:[base::mac::OuterBundle() bundlePath]
isRemovable:nil
isWritable:nil
isUnmountable:nil
description:nil
type:&filesystem_type_string];
FilesystemType filesystem_type = FilesystemType::kUnknown;
if (success)
filesystem_type = FilesystemStringToType(is_ro_dmg, filesystem_type_string);
base::UmaHistogramEnumeration("OSX.InstallationFilesystem", filesystem_type);
}
void RecordInstanceStats() {
upgrade_util::ThisAndOtherUserCounts counts =
upgrade_util::GetCountOfOtherInstancesOfThisBinary();
base::UmaHistogramCounts100("OSX.OtherInstances.ThisUser",
counts.this_user_count);
base::UmaHistogramCounts100("OSX.OtherInstances.OtherUser",
counts.other_user_count);
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class FastUserSwitchEvent {
kUserDidBecomeActiveEvent,
kUserDidBecomeInactiveEvent,
kMaxValue = kUserDidBecomeInactiveEvent,
};
void LogFastUserSwitchStat(FastUserSwitchEvent event) {
base::UmaHistogramEnumeration("OSX.FastUserSwitch", event);
}
void InstallFastUserSwitchStatRecorder() {
NSNotificationCenter* notification_center =
[[NSWorkspace sharedWorkspace] notificationCenter];
[notification_center
addObserverForName:NSWorkspaceSessionDidBecomeActiveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note) {
LogFastUserSwitchStat(
FastUserSwitchEvent::kUserDidBecomeActiveEvent);
}];
[notification_center
addObserverForName:NSWorkspaceSessionDidResignActiveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note) {
LogFastUserSwitchStat(
FastUserSwitchEvent::kUserDidBecomeInactiveEvent);
}];
}
bool IsDirectoryWriteable(NSString* dir_path) {
NSString* file_path = [dir_path stringByAppendingPathComponent:@"tempfile"];
NSData* data = [NSData dataWithBytes:"\01\02\03\04\05" length:5];
BOOL success = [data writeToFile:file_path atomically:NO];
if (success)
[[NSFileManager defaultManager] removeItemAtPath:file_path error:nil];
return success;
}
bool IsOnSameFilesystemAsChromium(NSString* dir_path) {
static const base::Optional<fsid_t> cr_fsid = []() -> base::Optional<fsid_t> {
struct statfs buf;
int result = statfs(
[[base::mac::OuterBundle() bundlePath] fileSystemRepresentation], &buf);
if (result != 0)
return base::nullopt;
return buf.f_fsid;
}();
if (!cr_fsid)
return false;
struct statfs buf;
int result = statfs([dir_path fileSystemRepresentation], &buf);
if (result != 0)
return false;
return cr_fsid->val[0] == buf.f_fsid.val[0] &&
cr_fsid->val[1] == buf.f_fsid.val[1];
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class StagingDirectoryStep {
kFailedToFindDirectory,
kItemReplacementDirectory,
kSiblingDirectory,
kNSTemporaryDirectory,
kTMPDIRDirectory,
kTmpDirectory,
kTmpDirectoryDifferentVolume,
kMaxValue = kTmpDirectoryDifferentVolume,
};
void LogStagingDirectoryLocation(StagingDirectoryStep step) {
base::UmaHistogramEnumeration("OSX.StagingDirectoryLocation2", step);
}
void RecordStagingDirectoryStats() {
NSURL* bundle_url = [base::mac::OuterBundle() bundleURL];
NSFileManager* file_manager = [NSFileManager defaultManager];
// 1. A directory alongside Chromium.
NSURL* bundle_parent_url =
[[bundle_url URLByStandardizingPath] URLByDeletingLastPathComponent];
NSURL* sibling_dir =
[bundle_parent_url URLByAppendingPathComponent:@".GoogleChromeStaging"
isDirectory:YES];
NSString* sibling_dir_path = [sibling_dir path];
BOOL is_directory;
BOOL path_existed = [file_manager fileExistsAtPath:sibling_dir_path
isDirectory:&is_directory];
BOOL success = true;
NSError* error = nil;
if (!path_existed) {
success = [file_manager createDirectoryAtURL:sibling_dir
withIntermediateDirectories:YES
attributes:nil
error:&error];
} else if (!is_directory) {
// There is a non-directory there; don't attempt to use this location
// further.
success = false;
}
if (success) {
success &= !error && IsDirectoryWriteable(sibling_dir_path);
// Only delete this directory if this was the code that created it.
if (!path_existed)
[file_manager removeItemAtURL:sibling_dir error:nil];
}
if (success) {
LogStagingDirectoryLocation(StagingDirectoryStep::kSiblingDirectory);
return;
}
// 2. NSItemReplacementDirectory
error = nil;
NSURL* item_replacement_dir =
[file_manager URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:bundle_url
create:YES
error:&error];
if (item_replacement_dir && !error &&
IsDirectoryWriteable([item_replacement_dir path])) {
LogStagingDirectoryLocation(
StagingDirectoryStep::kItemReplacementDirectory);
return;
}
// 3. NSTemporaryDirectory()
NSString* ns_temporary_dir = NSTemporaryDirectory();
if (ns_temporary_dir && IsOnSameFilesystemAsChromium(ns_temporary_dir) &&
IsDirectoryWriteable(ns_temporary_dir)) {
LogStagingDirectoryLocation(StagingDirectoryStep::kNSTemporaryDirectory);
return;
}
// 4. $TMPDIR
const char* tmpdir_cstr = getenv("TMPDIR");
NSString* tmpdir = tmpdir_cstr ? @(tmpdir_cstr) : nil;
if (tmpdir && IsOnSameFilesystemAsChromium(tmpdir) &&
IsDirectoryWriteable(tmpdir)) {
LogStagingDirectoryLocation(StagingDirectoryStep::kTMPDIRDirectory);
return;
}
// 5. /tmp
NSString* tmp = @"/tmp";
if (IsOnSameFilesystemAsChromium(tmp) && IsDirectoryWriteable(tmp)) {
LogStagingDirectoryLocation(StagingDirectoryStep::kTmpDirectory);
return;
}
// 6. /tmp, but different volume
if (IsDirectoryWriteable(tmp)) {
LogStagingDirectoryLocation(StagingDirectoryStep::kTmpDirectory);
return;
}
// 7. Give up.
LogStagingDirectoryLocation(StagingDirectoryStep::kFailedToFindDirectory);
}
// Records various bits of information about the local Chromium installation in
// UMA.
void RecordInstallationStats() {
RecordFilesystemStats();
RecordInstanceStats();
InstallFastUserSwitchStatRecorder();
RecordStagingDirectoryStats();
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class StartupUpdateState {
kUpdateKeyNotSet,
kUpdateKeySetAndStagedCopyPresent,
kUpdateKeySetAndStagedCopyNotPresent,
kMaxValue = kUpdateKeySetAndStagedCopyNotPresent,
};
// Records about the state of Chrome updates. This is pre-emptory data
// gathering to make sure that a situation that the team thinks will be OK is
// actually OK in the field.
void RecordUpdateState() {
StartupUpdateState state = StartupUpdateState::kUpdateKeyNotSet;
NSString* staging_location = [CrStagingKeyWatcher stagingLocation];
if (staging_location) {
if ([[NSFileManager defaultManager] fileExistsAtPath:staging_location])
state = StartupUpdateState::kUpdateKeySetAndStagedCopyPresent;
else
state = StartupUpdateState::kUpdateKeySetAndStagedCopyNotPresent;
}
base::UmaHistogramEnumeration("OSX.StartupUpdateState", state);
}
} // namespace
// ChromeBrowserMainPartsMac ---------------------------------------------------
ChromeBrowserMainPartsMac::ChromeBrowserMainPartsMac(
const content::MainFunctionParams& parameters,
StartupData* startup_data)
: ChromeBrowserMainPartsPosix(parameters, startup_data) {}
ChromeBrowserMainPartsMac::~ChromeBrowserMainPartsMac() {
}
int ChromeBrowserMainPartsMac::PreEarlyInitialization() {
if (base::mac::WasLaunchedAsLoginItemRestoreState()) {
base::CommandLine* singleton_command_line =
base::CommandLine::ForCurrentProcess();
singleton_command_line->AppendSwitch(switches::kRestoreLastSession);
} else if (base::mac::WasLaunchedAsHiddenLoginItem()) {
base::CommandLine* singleton_command_line =
base::CommandLine::ForCurrentProcess();
singleton_command_line->AppendSwitch(switches::kNoStartupWindow);
}
return ChromeBrowserMainPartsPosix::PreEarlyInitialization();
}
void ChromeBrowserMainPartsMac::PreMainMessageLoopStart() {
MacStartupProfiler::GetInstance()->Profile(
MacStartupProfiler::PRE_MAIN_MESSAGE_LOOP_START);
ChromeBrowserMainPartsPosix::PreMainMessageLoopStart();
// ChromeBrowserMainParts should have loaded the resource bundle by this
// point (needed to load the nib).
CHECK(ui::ResourceBundle::HasSharedInstance());
// This is a no-op if the KeystoneRegistration framework is not present.
// The framework is only distributed with branded Google Chrome builds.
[[KeystoneGlue defaultKeystoneGlue] registerWithKeystone];
// Disk image installation is sort of a first-run task, so it shares the
// no first run switches.
//
// This needs to be done after the resource bundle is initialized (for
// access to localizations in the UI) and after Keystone is initialized
// (because the installation may need to promote Keystone) but before the
// app controller is set up (and thus before MainMenu.nib is loaded, because
// the app controller assumes that a browser has been set up and will crash
// upon receipt of certain notifications if no browser exists), before
// anyone tries doing anything silly like firing off an import job, and
// before anything creating preferences like Local State in order for the
// relaunched installed application to still consider itself as first-run.
if (!first_run::IsFirstRunSuppressed(parsed_command_line())) {
if (MaybeInstallFromDiskImage()) {
// The application was installed and the installed copy has been
// launched. This process is now obsolete. Exit.
exit(0);
}
}
// Create the app delegate. This object is intentionally leaked as a global
// singleton. It is accessed through -[NSApp delegate].
AppController* app_controller = [[AppController alloc] init];
[NSApp setDelegate:app_controller];
chrome::BuildMainMenu(NSApp, app_controller,
l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), false);
[app_controller mainMenuCreated];
// Initialize the OSCrypt.
PrefService* local_state = g_browser_process->local_state();
DCHECK(local_state);
OSCrypt::Init(local_state);
}
void ChromeBrowserMainPartsMac::PostMainMessageLoopStart() {
MacStartupProfiler::GetInstance()->Profile(
MacStartupProfiler::POST_MAIN_MESSAGE_LOOP_START);
ChromeBrowserMainPartsPosix::PostMainMessageLoopStart();
RecordInstallationStats();
RecordUpdateState();
}
void ChromeBrowserMainPartsMac::PreProfileInit() {
MacStartupProfiler::GetInstance()->Profile(
MacStartupProfiler::PRE_PROFILE_INIT);
ChromeBrowserMainPartsPosix::PreProfileInit();
// This is called here so that the app shim socket is only created after
// taking the singleton lock.
g_browser_process->platform_part()->app_shim_host_manager()->Init();
}
void ChromeBrowserMainPartsMac::PostProfileInit() {
MacStartupProfiler::GetInstance()->Profile(
MacStartupProfiler::POST_PROFILE_INIT);
ChromeBrowserMainPartsPosix::PostProfileInit();
g_browser_process->metrics_service()->RecordBreakpadRegistration(
crash_reporter::GetUploadsEnabled());
if (first_run::IsChromeFirstRun())
EnsureMetadataNeverIndexFile(user_data_dir());
// Activation of Keystone is not automatic but done in response to the
// counting and reporting of profiles.
KeystoneGlue* glue = [KeystoneGlue defaultKeystoneGlue];
if (glue && ![glue isRegisteredAndActive]) {
// If profile loading has failed, we still need to handle other tasks
// like marking of the product as active.
[glue setRegistrationActive];
}
}
void ChromeBrowserMainPartsMac::DidEndMainMessageLoop() {
AppController* appController =
base::mac::ObjCCastStrict<AppController>([NSApp delegate]);
[appController didEndMainMessageLoop];
}